Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

【Dart/Flutter】async libraryでFutureとStreamを使った非同期処理についてまとめてみた

More than 1 year has passed since last update.

Dart/Flutterの開発環境

  • Flutter 1.9.1+hotfix.6
  • Dart 2.5.0

Dartのasync libraryで非同期処理の実装

Dartで非同期処理を実装するに、async libraryを利用します。

そもそもの非同期処理について理解があまりない人は、Dartではありませんが下の記事がわかりやすいので参考にしましょう。

参考:https://qiita.com/keitarou/items/79a038a29e1f8e39573b

また、概念だけでなく非同期処理を体感したい人は、下のページで実行結果を見てみましょう。

参考:https://lab.syncer.jp/Web/JavaScript/Reference/Global_Object/Promise/all/

まずは、async libraryの概要は以下です。

原文

dart:async library
Support for asynchronous programming, with classes such as Future and Stream.
Understanding Futures and Streams is a prerequisite for writing just about any Dart program.

直訳

dart:asyncライブラリ
FutureやStreamなどのクラスを使用した非同期プログラミングのサポート。
FuturesとStreamsを理解することは、ほぼすべてのDartプログラムを書くための前提条件です。

引用:https://api.flutter.dev/flutter/dart-async/dart-async-library.html

BLoCパターンでは、Streamを利用するため、アーキテクチャ導入を検討している人は、async libraryについて理解しておく必要があると思っています。

※上記のBLoCパターンでは、Dart標準のStreamではなくRxDartを利用している

FlutterでFutureを使った非同期処理の実装

Futureを使った非同期処理の実装は、FlutterのWidgetのFutureBuilderを使うと分かりやすいかもしれません。

FutureBuilderについての概要は以下です。

原文

Widget that builds itself based on the latest snapshot of interaction with a Future.

翻訳

Futureと対応する最新スナップショットに基づいて自身を構築するウィジェット。

※直訳だとニュアンスがやや気持ち悪かったので、自分で翻訳しました。

実際にFutureを理解するために、FutureBuilderを使って、非同期で記事を取得してWidgetで描画する場合のソースコードを見てみましょう。

Future _getArticle() async {
  final articleResponse = await http.get('https://sample.com/articles/1');
  final decodedJson = await json.decode(articleResponse.body);
  // オブジェクトをマッピングするModel層は省略
  Articles article = Articles.fromJson(decodedJson);
  return article;
}

@override
Widget build(BuildContext context) {
  return FutureBuilder(
    future: _getArticle(),
    builder: (context, snapshot) {
      // 非同期処理が完了している場合にWidgetの中身を呼び出す
      if(snapshot.hasData) {
        return _widgetBody(snapshot.data);
      // 非同期処理が未完了の場合にインジケータを表示する
      } else {
        return Center(child: CircularProgressIndicator());
      }
    },
  );
}

FutureBuilderでWidgetを描画してる間に、futureプロパティ(future: _getArticle())のところで、非同期で処理が走っています。

非同期で走らせている_getArticle()の処理が終わるまで、CircularProgressIndicator()でローディングを表示されるといった実装ですね。

Futureのざっくりとしたイメージは下のような感じです。

非同期処理の図.001.jpeg

非同期処理を走らせているFutureBuilderfutureプロパティは、以下の役割を果たしています。

原文

The asynchronous computation to which this builder is currently connected, possibly null.

直訳

このビルダーが現在接続されている非同期処理。nullの可能性があります。

引用:https://api.flutter.dev/flutter/widgets/FutureBuilder/future.html

おさらいすると、FutureBuilderが現在接続している非同期処理は、_getArticle()ということになりますね。

FutureBuilderの扱いの注意点

The builder must not be null.

引用:https://api.flutter.dev/flutter/widgets/FutureBuilder/FutureBuilder.html

ちなみに、FutureBuilderbuildernullであってはいけません。

nullの場合は、以下のようにエラーで画面が真っ赤っかになります。

スクリーンショット_2019-11-09_23_09_25.png

Futureの非同期処理でsnapshotを取得する際には、buildernullの状態を避ける必要があります。

私の場合は、if(snapshot.hasData)等を使って、非同期処理が完了するまでの間は、CircularProgressIndicator()や空のContainer等の適当なWidgetを出すような実装にすることで、真っ赤な画面は避けられました。

FlutterでStreamを使った非同期処理の実装

Streamのイメージは、実際のソースコードを見た方が理解しやすいと思います。

main.dart
import 'dart:async';

final streamController = StreamController<String>();

void main() async {
  streamController.stream.listen((addData){
    print(addData);
  });

  streamController.sink.add("DIO");
  streamController.sink.add("承太郎");
  streamController.sink.add("花京院");
}

sinkaddでオブジェクト(ここで言うジョジョのキャラクター)を渡してあげると、streamlistenしている側がaddを検知して、中の処理が動きます。

その中の処理は、受け取った引数をprint()しているだけなので、コンソールの出力結果は以下となりました。

# 出力結果
flutter: DIO
flutter: 承太郎
flutter: 花京院

また、listenaddして初めて動きます。

main.dart
import 'dart:async';

final streamController = StreamController<String>();

void main() async {
  streamController.stream.listen((addData){
    print("貴様見ているな!!");
    print(addData);
  });
}

上のような実装だと、コンソールには以下のように何も出力されません。

# 出力結果

多分、ここの理解はStreamのざっくりとしたイメージを見た方が分かりやすいかと思いました。(桃太郎をイメージしています。)

Streamのイメージ図.001.jpeg

先ほどのaddしないとlistenが動かないということを桃太郎流に解説すると、そもそも川に対して桃を流して(sink.add)あげないとおばあさんは桃を拾う(stream.listen)ことが出来ませんよね。ということになります。

ただ、これまでの説明だと非同期処理感が全くないので、同期処理も一緒に織り交ぜた実装と出力結果を見てみましょう。

main.dart
import 'dart:async';

final streamController = StreamController<String>();

void main() async {
  streamController.stream.listen((addData){
    print(addData);
  });

  print("ザ・ワールド!!");
  streamController.sink.add("DIO");
  print("スタープラチナ!!");
  streamController.sink.add("承太郎");
  print("ハイエロファントグリーン!!");
  streamController.sink.add("花京院");
}

一見、ジョジョのスタンド名とのキャラクター名が交互に出力されそうに見えますが、以下のような出力結果になりました。

# 出力結果
flutter: ザ・ワールド!!
flutter: スタープラチナ!!
flutter: ハイエロファントグリーン!!
flutter: DIO
flutter: 承太郎
flutter: 花京院

非同期で処理をしている間に、同期処理がどんどん進んでいってる証拠ですね。

ザ・ワールドもスタープラチナもいるのに、全然時が止まっていません。

?「だめだね」

ちゃんと交互に出力させたい場合は、以下のような実装になります。

main.dart
import 'dart:async';

final streamController = StreamController<String>();

void main() async {
  streamController.stream.listen((addData){
    print(addData);
  });

  print("ザ・ワールド!!");
  streamController.sink.add("1回目のadd");
  await Future.delayed(Duration(seconds: 9));
  print("スタープラチナ!!");
  streamController.sink.add("2回目のadd");
  await Future.delayed(Duration(seconds: 5));
  print("ハイエロファントグリーン!!");
  streamController.sink.add("3回目のadd");
}

上のような実装をすると、出力結果は以下の通りです。

# 出力結果
flutter: ザ・ワールド!!
flutter: 1回目のadd
// 9秒時が止まる
flutter: スタープラチナ!!
flutter: 2回目のadd
// 5秒時が止まる
flutter: ハイエロファントグリーン!!
flutter: 3回目のadd

※この実装をすると、本当に時が止まったかのように感じます。

ジョジョが好きではない人からすると「言ってることがわからない…イカレてるのか?」と思う内容でゴメンなさい。

TIPS1:DIOは最大9秒時を止められて、承太郎は最大5秒時を止められます。(確か)
TIPS2:花京院は時を止められて腹パンされます。

Streamのbroadcastで複数回listen()する

Streamは、普通に使っててもlistenは1回しか出来ません。

main.dart
import 'dart:async';

final streamController = StreamController<String>();

void main() async {
  streamController.stream.listen((addData){
    print(addData);
  });

  streamController.stream.listen((addData){
    print(addData);
  });

  streamController.sink.add("DIO");
  streamController.sink.add("承太郎");
  streamController.sink.add("花京院");
}

上のような実装だと、複数個のlistenを用意すると、以下のようなエラーが出てしまいます。

# 出力結果
[VERBOSE-2:ui_dart_state.cc(148)] Unhandled Exception: Bad state: Stream has already been listened to.
#0      _StreamController._subscribe (dart:async/stream_controller.dart:668:7)
#1      _ControllerStream._createSubscription (dart:async/stream_controller.dart:818:19)
#2      _StreamImpl.listen (dart:async/stream_impl.dart:472:9)
#3      main (package:application_name/main.dart:34:27)
#4      _asyncThenWrapperHelper.<anonymous closure> (dart:async-patch/async_patch.dart:71:64)
#5      _rootRunUnary (dart:async/zone.dart:1132:38)
#6      _CustomZone.runUnary (dart:async/zone.dart:1029:19)
#7      _FutureListener.handleValue (dart:async/future_impl.dart:137:18)
#8      Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:678:45)
#9      Future._propagateToListeners (dart:async/future_impl.dart:707:32)
#10     Future._completeWithValue (dart:async/future_impl.dart:522:5)
#11     _AsyncAwaitCompleter.complete (dart:async-patch/async_patch.dart:30:15)
#12     _completeOnAsyncReturn (dart:<…>

しかし、broadcastを使えば、その問題は解消されます。

main.dart
import 'dart:async';

final streamController = StreamController<String>.broadcast();

void main() async {
  streamController.stream.listen((addData){
    print(addData);
  });

  streamController.stream.listen((addData){
    print(addData);
  });

  streamController.sink.add("DIO");
  streamController.sink.add("承太郎");
  streamController.sink.add("花京院");
}

上のような実装だと、出力結果は以下のようになります。

# 出力結果
flutter: DIO
flutter: DIO
flutter: 承太郎
flutter: 承太郎
flutter: 花京院
flutter: 花京院

これで、Streamさえ定義してしまえば、複数回listen出来ますね。

説明がこれだけだと使い道がイマイチ思いつかないかと思いますが、少なくともBLoCパターンアーキテクチャBLoC層で、Streambroadcastで定義しておいて、色んなStreamBuilderで使い回したい(listenしたい)場合に、役に立つかなと思ってます。

RxDartSubjectは最初からbroadcastなので、この記事で解説した実装は気にならないかもしれませんが。

ちなみに、RxDartについても、実装と実行結果とイメージ図をまとめているので、参考にしていただけると幸いです。

参考:https://qiita.com/arthur_foreign/items/a10d3d4e303b2f77e87d

arthur_foreign
備忘録をまとめるようにしてます。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away