Flutter-非同期関数(async)の結果をウィジェットに表示する方法

Teratail見ているとデータベース(より正確に言うとasync関数の結果)から取得した情報を使ってウィジェットを構築するのに問題が出ているという質問が多かった。

ウィジェットを構築する基準となるStatelessWidget, StatefulWidget(こちらはState<T>のものになるが)はbuild関数でWidgetを構築する。
このbuild関数はasyncではない関数とする必要がある。
これに対してデータベー等からデータを収集する関数は通常asyncな非同期関数かFuture<T>を返す関数になる。

ここでbuild関数と非同期の取得関数の間でインターフェースの齟齬が起きてしまう。

Asyncな関数もしくはFutureから結果を受け取る(もしくは処理を完了するまで待つ)にはawaitしないといけないのだけど、awaitするにはそれを利用する関数をasyncにしないといけない。しかしbuildメソッドをasyncにはできない。

ここでFlutter初心はつまずいてしまうのだろう。

本当は以下の様な実装にできたらいいのだろうけど。

Widget build(BuildContext context) {
 await データ取得関数();  // ここでエラーになる
 return 表示用Widget作成();
}

しかしawait部分で構文エラーになる。

基本的な実装方法

基本的に上の様な処理ではFutureBuilderを使うというのがFlutter的な推奨実装になる。

FutureBuilderはFutureオブジェクトを引数にそのFutureオブジェクトの状態変化をキーとしてWidgetの再構築を行ってくれるというもの。

詳細はクラスのヘルプを見てもらうとして、状態としてはデータが未確定・データが確定・エラーの3種類の状態を判断してウィジェットの構築をすればいいと思う。もっと詳細に言うと状態としてはConnectionStateを参照する必要があるが。

実装1

よくある実装としては、StatefulWidgetでウィジェットクラスを構築。initState内でFutureの呼び出しを実施、FutureBuilderのfutureにはinitStateで呼び出したFutureを与えるという実装。これでウィジェット構築初回にデータベースからのデータを取得するという処理ができる。

class Future2 extends StatefulWidget {
  const Future2({Key? key}) : super(key: key);

  @override
  State<Future2> createState() => _Future2State();
}

class _Future2State extends State<Future2> {
  late Future<String> future;
  @override
  void initState() {
    super.initState();
    future = Future(
      () async {
        await Future.delayed(const Duration(seconds: 1));
        return "hoge";
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: future,
      builder: (context, snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.none:
            return const Text("none");
          case ConnectionState.waiting:
            return const Text("waiting");
          case ConnectionState.active:
            return const Text("active");
          case ConnectionState.done:
            return Text(snapshot.data!);
        }
      },
    );
  }
}

実装2

次のはFutureBuilderのfutureにFutureの関数を入れるという方法。

ただこの方法StatefulWidgetのsetState呼び出しで毎回データベース取得を行うことになりかねないので注意が必要。

class Future1 extends StatefulWidget {
  const Future1({Key? key}) : super(key: key);

  @override
  State createState() => _Future1State();
}

class _Future1State extends State {
  Future future() async {
    await Future.delayed(const Duration(seconds: 1));
    return "hoge";
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: future(),
      builder: (context, snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.none:
            return const Text("none");
          case ConnectionState.waiting:
            return const Text("waiting");
          case ConnectionState.active:
            return const Text("active");
          case ConnectionState.done:
            return Text(snapshot.data!);
        }
      },
    );
  }
}
  • 注意点
    Future1をconst Future1()と定義する場合、Future2と同じような動きをする。
    そのため、この実装でも問題ないかなと思うのだけど、constを外すと呼び出し側のbuild処理が呼び出されるたびにFutureの再実行が走るようになる。
    これはconst指定されるとオブジェクトの中身は変化がないオブジェクトとして認識されbuildが実行されないから。
    Future2はconstを付けても外してもbuild内でFutureの再実行は行われないので、Futureの結果は確定した状態のまま。

FutureBuilder補足

またここでもう一工夫あるとしたら、このページの内容も参考にしてもらいたい。

データが確定されるまでの時間とその表示方法についての考察をしている。

実装3

なおFutureBuilderを使わない方法もある。

StatefulWidget内でデータベース取得のFutureを呼び出し、データ取得完了後setStateを行うという方法。

class Future3 extends StatefulWidget {
  const Future3({Key? key}) : super(key: key);

  @override
  State<Future3> createState() => _Future3State();
}

class _Future3State extends State<Future3> {
  String value = "none";
  @override
  void initState() {
    super.initState();
    Future(
      () async {
        await Future.delayed(const Duration(seconds: 1));
        value = "hoge";
        setState(() {});
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Text(value);
  }
}

上の例はエラーとか処理時間が長かったらどうするかとか外して、とっても簡略化したもの。
もし処理時間が長ったらとかエラーがあったらとか、それら仕様を満たすとしたらFutureBuilderを自前実装するような考えになるのだと思う。

コメント

タイトルとURLをコピーしました