Dart(Flutter)における非同期処理入門(Firestoreのデータ取得サンプルつき)


こんにちは最近趣味でFlutterを書いている@glassmonkeyです。
今回は自分の勉強メモを兼ねて、Dartにおける非同期処理の書き方に関してまとめます。
おまけ程度ですが、それらを活用したFirestoreのデータの取得のサンプルも載せています。

間違ってたり、表現不足な内容などがありましたらぜひご指摘ください。

Dartにおける非同期処理

パッケージ dart:asyncで定義されており、1.9から使える機能。
下記の2種類が存在します。
非同期で扱うデータが単数か、複数かで使い分けする形です。

  • Future (単体)
  • Stream (複数)

それぞれの使い方に関して説明します。

Futureについて

いわゆる async/await。
FutureそのものはJSでいうところのPromiseなものだと私は認識している。(間違ってたらすみません)
メソッドにasyncキーワードを付与すれば、そのメソッドはFutureを意識せずに書けるのでとても見やすいです。
個人的には値のマークアップはよりFuture<型>としてたほうが非同期感がわかりやすくていいかなとは思います。(void main() は対比のためFutureをつけていません)


import 'dart:async';
Future<int> zero() async {
  return 0;
}

void main() async {
  print(zero()); // Instance of 'Future<int>'
  print(await zero()); // 0
}

awaitする場合はFuture処理系でないとだめなので注意。


import 'dart:async';
Future<int> zero() async {
  return 0;
}

//asyncを外してFuture処理系ではなくす。
void main() {
  print(await zero()); // Future処理系ではないのでawaitできない。
}

Streamについて

Futureが単発の非同期データを扱うのならStreamは複数の非同期データを扱う方法です。例を先に見てもらったほうがわかりやすいです。


import 'dart:async';
Stream<int> countStream(int to) async* {
  for (int i = 1; i <= to; i++) {
    yield i;
  }
}

void main() {
  countStream(3).listen((value) => print(value)); 
} 

// 出力
// 1
// 2
// 3

Futureasyncキーワードを使ったものに対して Streamasync *を使います。
yield 値で値をストリームに流します。(*がついてて配列感あるのはポインタっぽい)

この例では引数で加えた値3をfor分で回して、3回ストリームに加えています。

for (int i = 1; i <= to; i++) {
    yield i;
  }

値の取り出しは


ストリーム.listen(() => 処理);

で行っています。ジェネレータで与えられた値が都度流れてくるため、イベント駆動的な挙動が言語的にも組みやすくなっています。

返り値を同期的に扱いたいケースもあると思われます。その場合はasyncキーワードと組み合わせてawait forを使うと良いです。


import 'dart:async';
Stream<int> countStream(int to) async* {
  for (int i = 1; i <= to; i++) {
    yield i;
  }
}

void main() async {
  await for (var value in countStream(3)) {
    print(value);
  }
}
// 出力
// 1
// 2
// 3

これらの内容はStreamの使い方の一端なので、詳細は公式をご確認ください。

応用(Firestoreからリストデータを取得)

上記の内容を使うとFirestoreのデータ取得処理を同期的扱うこともできます。

  • Firestoreからデータを取得するをawait (データ型Hogeは適当に読み替えてください)
Future<List<Map<String, Dynamic>>> fetch () async {
  final Stream<List<Hoge>> stream = Firestore.instance
      .collection('コレクション名')
      .snapshots()
      .map((QuerySnapshot snapshot) {
    return snapshot.documents.map((DocumentSnapshot document) {
      return document.data; //Documentの実態。型はMap<String, Dynamic>なのでオブジェクトに変換したい場合はここで行うと良い。
    }).toList();
  });
  await for (var data in stream) {
    return data;
  }
}

このようにしておくと、awaitでデータを受け取れるようになります。
データを取得する箇所はstreamのままで扱うよりは、個人的にはこのほうが直感的で好みです。
テスト用のダミーも作りやすいですし。


final items = await fetch()

また、非同期処理用のビルダーのStreamBuilder, FutureBuilderと組み合わせると描画コストを抑えつつ非同期にウィジェットを扱うこともできます。(今回はFlutterを主題としてないので割愛します。)

おわりに

実際はRxDartとか使うのと思うのでStreamを生で触る機会はあまりないのかもしれませんが、これらを知っておくとちょっとした非同期処理を書くときに便利かなと思います。

たまにFlutter(Dart)関係のことつぶやいていたりするので、@glassmonekeyをフォローしてくれると嬉しいです。