[Flutter] FutureBuilderを使って非同期でWidgetを生成する


### はじめに
最近Flutterはじめました。
FutureBuilderはとてもシンプルで使いやすい機能ですが、初見で何やってるのかピンとこなかったのでまとめます。

2019/12/15 追記: かなり雑だったので少し修正しました。


まずはFutureBuilderを使って表示するためのデータを返してくれるメソッドをテスト用に作成しましょう。
`_getFutureValue()`です、APIで通信して文字列を取得するケースを擬似的に再現しています。
1秒後にFuture型の"データの取得に成功しました"という文字列を返します。

```dart
  Future<String> _getFutureValue() async {
    // 擬似的に通信中を表現するために１秒遅らせる
    await Future.delayed(
      Duration(seconds: 1),
    );
    return Future.value("データの取得に成功しました");
  }
```


### FutureBuilderを追加　 

次は本来表示したいWidgetの階層にFutureBuilderを追加します、今回はCenter直下です。

futureの引数に先程作った`_getFutureValue()`を代入、
builderには`(BuildContext context, AsyncSnapshot snapshot) { }`を

futureにセットしたメソッドがreturnしてくれる値(今回の場合は`"データの取得に成功しました"`)を`snapshot.data`で取得できます。

しかし、ただbuilder内で`return Text(snapshot.data);`としてしまうとエラーになってしまいます。
最初の１秒間は_getFutureValue()は何もデータを返さないのでsnapshot.dataがnulになってしまうからです。

snapshotに本当にデータが格納されているかをsnapshot.hasDataで確認してからreturnしましょう。

```dart
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('FutureBuilder Demo'),
      ),
      body: Center(
        child: FutureBuilder(
          future: _getFutureValue(),
          builder: (BuildContext context, AsyncSnapshot<String> snapshot) {　
            if (snapshot.hasData) {
              return Text(snapshot.data);
            } else {
              return Text("データが存在しません");
            }
          },
        ),
      ),
    );
  }
```

builderを`(BuildContext context, AsyncSnapshot<指定したい型> snapshot)`とすれば`snapshot.data`の型を指定できます。

### 通信状態を考慮する

上記のケースだとデータがあるかないかの分岐しかないので
通信中でデータがない場合でも、通信完了したがデータの取得には失敗した場合でも"データが存在しません"とだけ表示されてしまいます。

通信が完了するまではスピナーを表示させましょう。
通信状態は`snapshot.connectionState`で取得できます
ConnectionStateがdoneになるまではCircularProgressIndicatorを表示させます。

```dart
            if (snapshot.connectionState != ConnectionState.done) {
              return CircularProgressIndicator();
            }
```

### エラーハンドリング 

通信に失敗した場合などを想定しましょう。

まずは先程作った_getFutureValue()でExceptionを返すように修正します

```dart

  Future<String> _getFutureValue() async {
    // 擬似的に通信中を表現するために１秒遅らせる
    await Future.delayed(
      Duration(seconds: 1),
    );

    try {
      // 必ずエラーを発生させる
　　　 throw Exception("データの取得に失敗しました");
    } catch (error) {
      return Future.error(error);
    }
  }
```

エラーかどうかは`snapshot.hasError`で、
エラーメッセージは`snapshot.error`で取得できます。
エラーの場合はエラーメッセージを表示させるようにしましょう。

```dart

            if (snapshot.hasError) {
              return Text(snapshot.error.toString());
            }
```

ここまで全ての実装でbuildメソッド全体はこのようになります

```dart
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('FutureBuilder Demo'),
      ),
      body: Center(
        child: FutureBuilder(
          future: _getFutureValue(),
          builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
            // 通信中はスピナーを表示
            if (snapshot.connectionState != ConnectionState.done) {
              return CircularProgressIndicator();
            }

            // エラー発生時はエラーメッセージを表示
            if (snapshot.hasError) {
              return Text(snapshot.error.toString());
            }

            // データがnullでないかチェック
            if (snapshot.hasData) {
              return Text(snapshot.data);
            } else {
              return Text("データが存在しません");
            }
          },
        ),
      ),
    );
  }


```



### さいごに
正直Dart良くわかってない感まだあるので間違ってたらぜひおしえてください。

[コード全体はこちらです](https://gist.github.com/ysknsn/13bb6aa775dea88965cc20621a448b48)