【Flutter】非同期処理を伴うデータ初期化の話

## 前書き

Flutterで初期データを非同期処理で取ってくる場合にどう書けばいいか悩んだのでまとめておきます。登場するのは主に`initState`と`FutureBuilder`です。追加情報をいただければ随時書き足しますので、コメントよろしくお願いします。

## サンプルコード
今回の検証コードのベースには、よくあるカウンターアプリの簡易版を使います。

```dart:counter_app.dart
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

```

```dart:counter_app.dart

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(
          '$_counter',
          style: Theme.of(context).textTheme.headline4,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}


```

実行画面はこんな感じ。「＋ボタン」を押すと数字が一つずつ上がっていく。

![Simulator Screen Shot - iPhone SE (2nd generation) - 2020-08-23 at 13.48.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542224/9e128271-725c-c8e0-1882-928170874b91.png)


以下では、このコードにデータ初期化処理を追加して、検証していきます。

## 検証: 非同期なデータ取得処理をinitStateに書いてみる
先にいいますがこれは失敗パターンです。データ取得はビルド前に済ませてしまいたいのでinitState()に非同期なデータ取得処理を書いたとしましょう。

### コード
- `_MyHomePageState`にinitStateMethodと、その中で呼ぶための非同期処理を追加。

```dart:counter_app.dart
class _MyHomePageState extends State<MyHomePage> {

//略

  @override
  void initState() {
    super.initState();
    asyncSampleMethod();
  }

  // 非同期でデータを取ってくるサンプルメソッド
  Future<int> asyncSampleMethod() async{
    await Future.delayed(const Duration(seconds: 5), (){
      _counter = 99;
    });
    return _counter;
  }

//略

```

非同期処理は5秒待ったあと`_counter`の値を99にするというものです。データベースとの通信に5秒かかったというのを想定しています。


### 結果
予想できる通り`asyncSampleMethod`の完了を待たずにビルドされてしまうので実行結果は0から始まって5秒後に99が代入されるという形になる。


![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542224/4792a124-d36c-f303-026d-589a3b218cd1.gif)

`asyncSampleMethod`の完了を待たないので、`_counter`は0のままビルドされ、5秒後`asyncSampleMethod`が完了した際に99が代入されるという仕組みです。ちなみにinitStateにasyncを付けようとするとエラーが出ます。

```
_MyHomePageState.initState() returned a Future.
State.initState() must be a void method without an `async` keyword.
```

## 検証: FutureBuilderを使う
こちらも失敗パターンです。FutureBuilderはfuture引数に書いた非同期な処理の完了前と後に分けてウィジェットを書ける便利な関数です。データ取得処理にはFutureBuilderを使うといいよと書いてあったので、何も考えずとりあえず使ってみましょう。

### コード
```dart:counter_app.dart
class _MyHomePageState extends State<MyHomePage> {

//略

// さっきのinitStateは一旦コメントアウト
// @override
//  void initState() {
//    super.initState();
//    asyncSampleMethod();
//  }

//略

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        // FutureBuilderを使って書き換える
        child: FutureBuilder(
            future: asyncSampleMethod(),
            builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
              Widget childWidget;
              if (snapshot.hasData) {
                childWidget = Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.headline4,
                );
              } else {
                childWidget = const CircularProgressIndicator();
              }
              return childWidget;
            }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

今回はFutureBuilderの`future`に渡した`asyncSampleMethod()`が完了する前は` CircularProgressIndicator()`(グルグル)を表示して、完了後はTextウィジェットで数字を表示するようにしています。

###実行結果
![タイトルなし23.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542224/5b3d1a12-f8a9-323b-5fb4-b586b2464b42.gif)

futureに渡した非同期処理が完了して99が表示され、「＋ボタン」を押すと1ずつ上がっていくので一見大丈夫そうに見えます。しかし、setStateで状態が変わる（「＋ボタン」を押す）度にFutureBuilderのfutureに渡した非同期な初期化処理が行われ、5秒後に値が99に戻ってしまいます。

## setStateとFutureBuilderを両方使う
これなら正しく動作します。
### コード

```dart:counter_app.dart
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter;
  Future<int> _future;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  void initState() {
    _future = asyncSampleMethod();
    super.initState();
  }

  Future<int> asyncSampleMethod() async {
    await Future.delayed(const Duration(seconds: 5), () {
      _counter = 99;
    });
    return _counter;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: FutureBuilder(
            future: _future,
            builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
              Widget childWidget;
              if (snapshot.hasData) {
                childWidget = Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.headline4,
                );
              } else {
                childWidget = const CircularProgressIndicator();
              }
              return childWidget;
            }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

```
ポイントは、initStateで一度だけ実行する非同期なデータ取得処理の返り値を格納するための変数（ここでは`_future`）を用意して、FutureBuilderからはその変数を参照するようにします。こうすることでFutureBuilderがリビルドされても、データ取得処理が再度呼ばれることはなくなります。


### 実行結果
![z.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542224/99d68c8e-7066-6bf7-c7be-11a6ac3042ad.gif)

うまく動作しました！


## まとめ
データ初期化処理について悩んだところをまとめました。少しでも誰かの参考になれば幸いです。今回は、FutureBuilderとsetStateを使いましたが他にも実装方法はあると思います。他の書き方があればコメントに書いていただけると助かります！