😢

Flutterで非同期をかっこよく扱う

2021/08/06に公開

目次

1. 【チートシート】FutureBuilder と StreamBuilderの比較

FutureBuilder
: Futureを返す処理を用いてウィジェットを作成するときに使用する。

Future<String> calculation;

FutureBuilder<String>(
  future: calculation,
  builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
    List<Widget> children;
    if (snapshot.hasData) {
      children = <Widget>[
        const Icon(
          Icons.check_circle_outline,
          color: Colors.green,
        ),
        Text('Result: ${snapshot.data}'),
      ];
    } else if (snapshot.hasError) {
      children = <Widget>[
        const Icon(
          Icons.error_outline,
          color: Colors.red,
        ),
        Text('Error: ${snapshot.error}'),
      ];
    } else {
      children = const <Widget>[
        CircularProgressIndicator(),
        Text('Awaiting result...'),
      ];
    }
    return Column(
        children: children,
    );
  },
)

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

StreamBuilder
: Streamを返す処理を用いてウィジェットを作成するときに使用する。Stream値が変化した時に再描画される。

Stream<int> bids; // ストリーム

StreamBuilder<int>(
  stream: bids,
  builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
    List<Widget> children;
    if (snapshot.hasError) {
      // エラーが発生した場合
      children = <Widget>[
        const Icon(
          Icons.error_outline,
          color: Colors.red,
        ),
        Text('Error: ${snapshot.error}'),
        Text('Stack trace: ${snapshot.stackTrace}'),
      ];
    } else {
      switch (snapshot.connectionState) {
        case ConnectionState.none:
          children = const <Widget>[
            Icon(
              Icons.info,
              color: Colors.blue,
            ),
            Text('Select a lot'),
          ];
          break;
        case ConnectionState.waiting:
          children = const <Widget>[
            CircularProgressIndicator(),
            Text('Awaiting bids...'),
          ];
          break;
        case ConnectionState.active:
          children = <Widget>[
            const Icon(
              Icons.check_circle_outline,
              color: Colors.green,
            ),
            Text('\$${snapshot.data}'),
          ];
          break;
        case ConnectionState.done:
          children = <Widget>[
            const Icon(
              Icons.info,
              color: Colors.blue,
            ),
            Text('\$${snapshot.data} (closed)'),
          ];
          break;
      }
    }

    return Column(
      children: children,
    );
  },
)

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

2. Async/AwaitでのFutureの処理

FirebaseやAPIをFlutter内で呼び出すときにはローディングなど、非同期処理を行う必要がある。
Async/Awaitを利用して非同期処理を行うとどのようになるだろう?
シンプルに書くと以下のようになるだろう。

Future<String> calculation() async {...};

class _MyHomePageState extends State<MyHomePage> {
  _MyHomePageState({required this.eventId}) : super();

  dynamic _calc = null;

  // initStateで値を更新している
  
  void initState() async {
    super.initState();

    setState(() {
      _calc = await calculation();
    });
  }

  
  Widget build(BuildContext context) {
    if (_calc == null) {
        // まだinitStateが完了していない
        return const CircularProgressIndicator();
    }

    // 以下はinitStateが完了している場合
    return Text("calc result is $_calc");
  }
}

このくらいだったらまだ理解できるが、エラー処理や複数の非同期処理を扱うようになると可読性が悪くなる。

3. FutureBuilder の使い方

FutureBuilderは非同期が完了したあとに、Widgetを生成する。
setStateなどをコードに書く必要がなくなったため、可読性が増したと思う。

Future<String> calculation() async {...};

class _MyHomePageState extends State<MyHomePage> {
  _MyHomePageState({required this.eventId}) : super();

  
  Widget build(BuildContext context) {
    // 以下はinitStateが完了している場合
    return FutureBuilder(
      future: calculation(),
      builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
              if (snapshot.hasError) {
                  return Text("Error: ${snapshot.error}");
              }
              if (!snapshot.hasData) {
                  return Text("データが見つかりません");
              }
              // データ表示
              return Text('${snapshot.data}');
          } else {
              // 処理中の表示
              return const CircularProgressIndicator();
          }
      },
    ):
  }
}

4. StreamBuilder の使い方

これらが本当に力を発揮するのはStreamBuilderの方だろう。
同じような記述でStreamを扱うことができる。

Stream<String> calculation() async {...};

class _MyHomePageState extends State<MyHomePage> {
  _MyHomePageState({required this.eventId}) : super();

  
  Widget build(BuildContext context) {
    // 以下はinitStateが完了している場合
    return StreamBuilder(
      stream: calculation(),
      builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
              if (snapshot.hasError) {
                  return Text("Error: ${snapshot.error}");
              }
              if (!snapshot.hasData) {
                  return Text("データが見つかりません");
              }
              // データ表示
              return Text('${snapshot.data}');
          } else {
              // 処理中の表示
              return const CircularProgressIndicator();
          }
      },
    ):
  }
}

Streamが変化したときにリアルタイムで画面も変化するので、動的なUIを作成するのに有利だろう。
FireStoreではFirebaseFirestore.instance.collection('コレクション名').snapshots()でストリームを取得できる。

実際の利用例は以下。

class _MyHomePageState extends State<MyHomePage> {
  final Stream<QuerySnapshot<Map<String, dynamic>>> _eventListStream =
      FirebaseFirestore.instance.collection('event-list').snapshots();

  
  Widget build(BuildContext context) {
    return StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
      stream: _eventListStream,
      builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
        if (snapshot.hasError) {
          return Text('error:${snapshot.error}');
        }

        if (snapshot.connectionState == ConnectionState.waiting) {
          // ローディング
          return CircularProgressIndicator(
            semanticsLabel: 'Linear progress indicator',
          );
        }

        return ListView(
            children: snapshot.data!.docs.map((DocumentSnapshot document) {
          Map<String, dynamic> data = document.data() as Map<String, dynamic>;
          return ListTile(
            title: _messageItem('${data['title']}', document.id),
          );
        }).toList());
      },
    );
  }
}

まとめ

Flutterでの非同期処理は、StreamBuilderやFutureBuilderを使うことで、可読性を上げるやり方を紹介した。
データの取得が一度でいい場合はFutureBuilderを使う。
動的にデータを更新する場合はStreamBuilderを使うとよいだろう。

Discussion