🍣

【Flutter Widget of the Week #14】StreamBuilderを使ってみた

2022/10/16に公開

はじめに

Flutter Widget of the Week #14 StreamBuilder についてまとめましたので、紹介します。
https://youtu.be/MkKEWHfy99Y

StreamBuilder とは

最近のアプリは非同期性が高く、様々なことが時と順序を選ばずに起こり得ます。
今この瞬間にも誰かが文章を投稿しているかもしれないし、いいねを押したかもしれません。
こうしたいつ、どこで、どんなイベントが発生するか分からないイベントを Stream (データの流れ)と考えることができます。
Flutter では Stream を扱う場合に今回紹介する StreamBuilder を使用します。
それでは、実際に使ってみましょう。

StreamBuilder サンプル

まずは API Document にあるサンプルを動かしてみましょう。
サンプル実行画面

main.dart
import 'dart:async'; // ← StreamController を使うために import する必要あり

class StreamBuilderSample extends StatefulWidget {
  const StreamBuilderSample({super.key});

  
  State<StreamBuilderSample> createState() => _StreamBuilderSampleState();
}

class _StreamBuilderSampleState extends State<StreamBuilderSample> {
  // StreamBuilder に渡す stream を作成
  final Stream<int> _bids = (() {
    late final StreamController<int> controller;
    controller = StreamController<int>(
      onListen: () async {
        await Future<void>.delayed(const Duration(seconds: 1));
        controller.add(1);
        await Future<void>.delayed(const Duration(seconds: 1));
        await controller.close();
      },
    );
    return controller.stream;
  })();

  
  Widget build(BuildContext context) {
    return DefaultTextStyle(
      style: Theme.of(context).textTheme.headline2!,
      textAlign: TextAlign.center,
      child: Container(
        alignment: FractionalOffset.center,
        color: Colors.white,
        child: StreamBuilder<int>(
          stream: _bids,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
            List<Widget> children;
	    // Error が発生したとき
            if (snapshot.hasError) {
              children = <Widget>[
                const Icon(
                  Icons.error_outline,
                  color: Colors.red,
                  size: 60,
                ),
                Padding(
                  padding: const EdgeInsets.only(top: 16),
                  child: Text('Error: ${snapshot.error}'),
                ),
                Padding(
                  padding: const EdgeInsets.only(top: 8),
                  child: Text('Stack trace: ${snapshot.stackTrace}'),
                ),
              ];
            } else {
              switch (snapshot.connectionState) {
	        // 初期時(今回はすぐ waiting に移るので表示されているようには見えない)
                case ConnectionState.none:
                  children = const <Widget>[
                    Icon(
                      Icons.info,
                      color: Colors.blue,
                      size: 60,
                    ),
                    Padding(
                      padding: EdgeInsets.only(top: 16),
                      child: Text('Select a lot'),
                    ),
                  ];
                  break;
		// 接続中の時(waiting が終わったら非同期処理が開始される)
                case ConnectionState.waiting:
                  children = const <Widget>[
                    SizedBox(
                      width: 60,
                      height: 60,
                      child: CircularProgressIndicator(),
                    ),
                    Padding(
                      padding: EdgeInsets.only(top: 16),
                      child: Text('Awaiting bids...'),
                    ),
                  ];
                  break;
		// 非同期処理実行中の時 
		//snapshot から取得したデータをもとに画面を作成
                case ConnectionState.active:
                  children = <Widget>[
                    const Icon(
                      Icons.check_circle_outline,
                      color: Colors.green,
                      size: 60,
                    ),
                    Padding(
                      padding: const EdgeInsets.only(top: 16),
                      child: Text('\$${snapshot.data}'),
                    ),
                  ];
                  break;
		// 非同期処理が終わった時
		// 最後に取得したデータをもとに画面を作成
                case ConnectionState.done:
                  children = <Widget>[
                    const Icon(
                      Icons.info,
                      color: Colors.blue,
                      size: 60,
                    ),
                    Padding(
                      padding: const EdgeInsets.only(top: 16),
                      child: Text('\$${snapshot.data} (closed)'),
                    ),
                  ];
                  break;
              }
            }

            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: children,
            );
          },
        ),
      ),
    );
  }
}

使い方として、
StreamBuilder に stream を渡してデータを流すイベントを設定します。
そして builder の中の snapshot からデータを取得し、そのデータをもとに画面を構成します。
StreamBuilder のプロパティには initialData があり、initialData には最初の snapshot を取得できるまでの間に使う値を指定します。
initialData を使わなくても、 snapshot.hasData でデータが取得できたかを判別し、false/true によって表示する画面を設定することもできます。
snapshot.hasError を使えばエラー時の処理を作ることができます。
他にも非同期処理の状況を確認する方法に snapshot.connectionState を使うこともできます。
connectionState は上記のサンプルコードでも使われているので参考にしてみてください。

connectionState の種類

connectionState enum には以下の4種類があります。

① none

初期時/接続してない時

② waiting

非同期接続未完了 == 接続中
よくこの状態のときにローディング中の画面を表示している

③ active

非同期処理で値が移り変わっている時

④ done

非同期処理がすべて完了した時

StreamBuilder のプロパティについて

StreamBuilder のプロパティは上記で記載していますが改めて紹介します。

(new) StreamBuilder<int> StreamBuilder({
  Key? key,
  int? initialData,
  Stream<int>? stream,
  required Widget Function(BuildContext, AsyncSnapshot<int>) builder,
})

①initialData

初期 snapshot の作成に使用されるデータを指定する
型は int 型

②stream

データを流すイベントを指定する
StreamBuilder はこの stream から流れるイベントを取得しています
型は Stream<int> 型

③builder

この中で取得したデータをもとに作る画面を設定する
型は Widget Function(BuildContext, AsyncSnapshot<int>) 型

最後に

今回は StreamBuilder を紹介しました。
この StreamBuilder は個人的には Firebase を使うとよく見る Widget だなと思います。 以前アウトプットした FutureBuilder と同様に、アプリを作るなら非同期処理を扱うことはよくあることなので、勉強して損はないと思いました。
本記事が少しでも為になれば嬉しいです。
次は #15 InheritedModel です。またお会いしましょう。

参考記事

https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html

Discussion