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
のざっくりとしたイメージは下のような感じです。
非同期処理を走らせているFutureBuilder
のfuture
プロパティは、以下の役割を果たしています。
原文
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
ちなみに、FutureBuilder
のbuilder
はnull
であってはいけません。
null
の場合は、以下のようにエラーで画面が真っ赤っかになります。
Future
の非同期処理でsnapshot
を取得する際には、builder
がnull
の状態を避ける必要があります。
私の場合は、if(snapshot.hasData)
等を使って、非同期処理が完了するまでの間は、CircularProgressIndicator()
や空のContainer
等の適当なWidgetを出すような実装にすることで、真っ赤な画面は避けられました。
FlutterでStreamを使った非同期処理の実装
Stream
のイメージは、実際のソースコードを見た方が理解しやすいと思います。
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("花京院");
}
sink
にadd
でオブジェクト(ここで言うジョジョのキャラクター)を渡してあげると、stream
をlisten
している側がadd
を検知して、中の処理が動きます。
その中の処理は、受け取った引数をprint()
しているだけなので、コンソールの出力結果は以下となりました。
# 出力結果
flutter: DIO
flutter: 承太郎
flutter: 花京院
また、listen
はadd
して初めて動きます。
import 'dart:async';
final streamController = StreamController<String>();
void main() async {
streamController.stream.listen((addData){
print("貴様見ているな!!");
print(addData);
});
}
上のような実装だと、コンソールには以下のように何も出力されません。
# 出力結果
多分、ここの理解はStream
のざっくりとしたイメージを見た方が分かりやすいかと思いました。(桃太郎をイメージしています。)
先ほどのadd
しないとlisten
が動かないということを桃太郎流に解説すると、そもそも川に対して桃を流して(sink.add)あげないとおばあさんは桃を拾う(stream.listen)ことが出来ませんよね。ということになります。
ただ、これまでの説明だと非同期処理感が全くないので、同期処理も一緒に織り交ぜた実装と出力結果を見てみましょう。
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: 花京院
非同期で処理をしている間に、同期処理がどんどん進んでいってる証拠ですね。
ザ・ワールドもスタープラチナもいるのに、全然時が止まっていません。
?「だめだね」
ちゃんと交互に出力させたい場合は、以下のような実装になります。
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回しか出来ません。
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
を使えば、その問題は解消されます。
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層
で、Stream
をbroadcast
で定義しておいて、色んなStreamBuilder
で使い回したい(listen
したい)場合に、役に立つかなと思ってます。
RxDart
のSubject
は最初からbroadcast
なので、この記事で解説した実装は気にならないかもしれませんが。
ちなみに、RxDart
についても、実装と実行結果とイメージ図をまとめているので、参考にしていただけると幸いです。
参考:https://qiita.com/arthur_foreign/items/a10d3d4e303b2f77e87d