【Flutter】それ、FutureBuilderだったら綺麗に書けるよ？

[元ブログ - 技術は熱いうちに打て！ | 【Flutter】それ、FutureBuilderだったら綺麗に書けるよ？](http://blog.dalt.me/1652)

# 概要
今日は [FutureBuilder](https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html)について解説したいと思います。

**アプリを作ってると非同期処理が頻出するけど、どうやったら綺麗に書けるか分からない！**
そんなあなたに届けたい記事となっています。

先日、[非同期処理をasync/awaitを用いて綺麗に書く](http://blog.dalt.me/1629)と言う趣旨のブログを書きました。
こちらも合わせて読んでいただく事で、だいぶ非同期処理周りがすっきりするのではないかと思います。

# 取得した本の内容を描画する処理を書いてみよう

## 本一覧の取得処理

```dart:books.dart
import 'dart:convert';

import 'packages:flutter/material.dart';
import 'packages:http/http.dart';

class Book {
  final String id;
  final String title;
  final double price;

  const Book(this.id, this.title, this.price);
}

class Books with ChangeNotifier {
  List<Book> _books = [];

  List<Book> get books {
    return [..._books];
  }

  Future<void> fetchBooks() async {
    const url = '...';
    final response = await http.get(url);
    final decodedData = json.decode(response.body) as Map<String, dynamic>;
    if (decodedData == null) {
      return;
    }
    List<Book> responseBooks = [];
    // パース
    _books = responseBooks;
    notifyListeners();
  }
}
```

## 本一覧の描画処理

```dart:book_list_screen.dart
import 'packages:flutter/material.dart';
import 'packages:provider/provider.dart';

import '../providers/books.dart';

class BookListScreen extends StatefulWidget {
  @Override 
  _BookListScreen createState() => _BookListScreenState();
}

class _BookListScreenState extends State<BookListScreen> {

  @override
  void initState() {
    Future.delayed(Duration.zero).then((_) {
      Provider.of<Books>(context, listen: false).fetchBooks();
    });
    super.initState();
  }

  @override 
  Widget build(BuildContext context) {
    final books = Provider.of<Books>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text('Books'),
      ),
      body: ListView.builder(
        itemCount: books.items.length,
        itemBuilder: (ctx, index) => BookItem( // あるものとして
          books.items[index],
        ),
      ),
    );
  }
}
```

### 解説

こんな感じでしょうか。
(直書きしてるのでコンパイル通らなかったらすみません。)

これだと、確かに取得し終わったら描画はされると思いますが、取得してる最中は
真っ白い画面を見せられることになると思います。

UX的に良くないので、ローディングを表示しましょう。

## ローディングの表示

```dart:book_list_screen.dart
import 'packages:flutter/material.dart';
import 'packages:provider/provider.dart';

class BookListScreen extends StatefulWidget {
  @Override 
  _BookListScreen createState() => _BookListScreenState();
}

class _BookListScreenState extends State<BookListScreen> {
  bool _isLoading = false;

  @override
  void initState() {
    Future.delayed(Duration.zero).then((_) async {
      setState(() {
        _isLoading = true;
      });
      await Provider.of<Books>(context, listen: false).fetchBooks();
      setState(() {
        _isLoading = false;
      });
    });
    super.initState();
  }

  @override 
  Widget build(BuildContext context) {
    final books = Provider.of<Books>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text('Books'),
      ),
      body: _isLoading
        ? Center(
          child: CircularProgressIndicator(),
        )
        : ListView.builder(
        itemCount: books.items.length,
        itemBuilder: (ctx, index) => BookItem( // あるものとして
          books.items[index],
        ),
      ),
    );
  }
}
```

### 解説

さて、これで通信が返ってくるまではローディングを中心に表示する事ができました。
が、状態変化で再ビルドしたいためStatefulWidgetに変更しなければいけなくなりました。

async/awaitが分からない人は[前回の記事](http://blog.dalt.me/1629)を
見てから戻ってきていただけると嬉しいです。


これを少し改善したい。

そもそも何をしたいか？
このWidgetがbuildされたら、
*1. 通信が終わるまではローディングを表示したい
2. 正常に終わったらListViewを表示したい*
この2つが主ですね。

これを踏まえた上で、 今回の主役`FutureBuilder`を紹介します。

## FutureBuilderを使って書き換える

```dart:book_list_screen.dart
import 'packages:flutter/material.dart';
import 'packages:provider/provider.dart';

import '../providers/books.dart';

class BookListScreen extends StatelessWidget {
  @override 
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Books'),
      ),
      body: FutureBuilder(
        future: Provider.of<Books>(context, listen: false).fetchBooks(),
        builder: (ctx, dataSnapshot) {
          if (dataSnapshot.connectionState == ConnectionState.waiting) {
            // 非同期処理未完了 = 通信中
            return Center(
              child: CircularProgressIndicator(),
            );
          }

          if (dataSnapshot.error != null) {
            // エラー
            return Center(
              child: Text('エラーがおきました');
            };
          }

          // 成功処理
          return Consumer<Books>(
            builder: (cctx, books, child) => ListView.builder(
              itemCount: books.items.length,
              itemBuilder: (lctx, index) => BookItem( // あるものとして
                books.items[index],
              ),
            ),
          );
        },
      ),
    );
  }
}
```

### 解説

何が起こったかわかりませんね笑

一つずつ解説していきます。

#### FutureBuilderを使う

`FutureBuilder`はWidgetの小クラスです。
なので、bodyに直接渡す事ができます。

さて、`FutureBuilder`をインスタンス化するために、まず`future`と言うパラメタに`Future<void>`を返す処理を渡します。

```dart:book_list_screen.dart
  return FutureBuilder(
    future: Provider.of<Books>(context, listen: false).fetchBooks(),
    builder: ...
  );
```

この処理結果を見て、FutureBuilderはbuilder内の処理を呼び出してくれます。

builderは `Widget Function(BuildContext, AsyncSnapshot<void>)`型です。

#### ローディング処理

```dart:book_list_screen.dart
if (dataSnapshot.connectionState == ConnectionState.waiting) {
  // 非同期処理未完了 = 通信中
  return Center(
    child: CircularProgressIndicator(),
  );
}
```

まず、ローディング中かどうか(非同期処理が終わったかどうか)は`AsyncSnapshot`が持つconnectionStateで判定します。

[ConnectionState enum](https://api.flutter.dev/flutter/widgets/ConnectionState-class.html)

そして、ローディング中に表示したいWidgetをreturnします。

#### エラーか否か

```dart:book_list_screen.dart
if (dataSnapshot.error != null) {
  // エラー
  return Center(
    child: Text('エラーがおきました');
  };
}
```

エラーは、`AsyncSnapshot`が持つ`error`プロパティで確認できます。

最初の例ではエラー時の表示はなかったですが、中央にエラー文を表示するWidgetをreturnをしました。

#### 通信成功時の処理

```dart:book_list_screen.dart
// 成功処理
return Consumer<Books>(
  builder: (cctx, books, child) => ListView.builder(
    itemCount: books.items.length,
    itemBuilder: (lctx, index) => BookItem( // あるものとして
      books.items[index],
    ),
  ),
);
```

通信成功時は、Consumerを使って無駄な再ビルドを防ぎつつリスト表示部分だけ描画します。

#### StatefulWidgetはStatelessWidgetへ

さて、お気づきの方もいらっしゃると思いますがなんとStatefulWidgetだったのがStatelessWidgetになっています。
`FutureBuilder` が状況に応じて再ビルドをしてくれるのでScaffoldやAppBarを再ビルドする必要がなくなりました。

昨日の記事と合わせると非同期処理が非常に綺麗に書ける様になっているのではないでしょうか？

誰かのお役に立てば。