はじめに
こんにちは、 @tacksman です。
Diverse Advent Calendar 2019 22日目の記事です。
自分がDiverseにジョインしてから一ヶ月ほど経ったので、その間にFlutterやFirestoreを使っていて躓いた点などを紹介しようかと思います。
前提
自分はここ数年はKotlinでのAndroidアプリ開発がすべてでしたが、それより前には Cordovaとreact-redux
を使用したiOSアプリの開発に参加していたこともありました。
今回は非同期処理となるFirestoreへのアクセスにStreamBuilderを使用していますが、こちらの使い方の詳細などについては取り扱わないでいきます。
FlutterとFirestoreの組み合わせ
おなじみのFirestoreをFlutterで扱う際のポイントです。
自分も久しぶりにFirestoreを扱うこともあり、Web上のドキュメントを読んだりしましたが、Flutter+Firestoreについて解説している日本語のドキュメントはあまりない上、公式のドキュメントを読むほうが参考になることが多い気がしています。
そこで、そういった部分であまりふれられていない箇所を中心に取り上げておきます。
Firestoreからのデータの取得
ここではFlutterでの非同期処理にStream
とStreamBuilder
を使用しています。
StreamBuilder
を使用してFirestoreからデータを取得し、それをもとに表示するコンポーネントを作成するには以下のようになります。
StreamBuilder(
stream: Firestore.instance.collection("messages").snapshots(),
builder: (context, snapshot) {
/// snapshot.dataでQuerySnapshotを取り出して求める型に置換したりする
},
)
Repositoryパターンを使用しているので、画面側のロジックで型変換したくないよお……という方は、Repositoryで置換後にStreamに包んで返し、それをStreamBuilderのstreamにいれましょう。
Firestoreからのデータ購読
次は購読です。
Firestore上のデータを購読するには以下のようなコードを実行することで、/messages
のパスにあたるドキュメント(もしくはコレクション)の購読を開始できます。
Firestore.instance.collection("messages").snapshots().listen((data) {
/// DocumentSnapshotな引数dataとして、購読したいドキュメントのスナップショットが落ちてくる
})
listen内のFunctionでスナップショットから実用する型へ変換することになります。
listen()
は返り値としてStreamSubscription<QuerySnapshot>
を返しますが、こちらは購読の終了を行うときにcancel()
をコールするためのものです。
購読を行っている画面を閉じるときなど、購読を止めたいタイミングでcancel()
を実行するようにしておきましょう。
さて、それではどのように画面側へ購読したデータを届けましょうか。
PublishSubject<T>
使用するなどいくつかの方法がありますが、dispose
し忘れに注意をはらないといけなかったり、データの流れを追いにくいです。
しかし画面側でStreamBuilderを使っていると、listen
を使わず素直なデータの流れで実装できます。
class Repository {
...
Stream<List<Message>> watch(String messageId) {
Firestore.instance.document("messages/${messageId}")
.snapshots()
.map((querySnapshot) { // MessageのQuerySnapshot
// List<Message>に変換する
});
}
...
}
class MessagePage extends StatefulWidget {
...
}
class MessagePageState extends State<MessagePage> {
String messageId;
Repository repository = Repository;
...
@override
Widget build(BuildContext context) {
repository.watch(messageId);
return StreamBuilder(
stream: repository.watch(),
builder: ...
);
}
...
}
Stream<QuerySnapshot>
に生えているlisten()
をコールして手動で購読を行ってもいいですが、そちらでは変換したデータを画面にもっていく部分や購読のキャンセルをしっかり行う必要が出てくるなど煩雑になってしまいます。
しかしStreamBuilderを使用しているときはStreamBuilder内部でlisten()
やcancel()
を行ってくれるため、比較的簡単に購読を実装できます。
Firestoreへのデータ追加
取得、購読ときたら次はFirestoreへのデータ追加についてです。
しかしsetData
するだけなら公式ドキュメントを読めばいいので、それ以外の部分について書いていきます。
データ取得のところで使ったMessageデータで考えてみましょう。
例えばチャットをする機能の場合、チャットルームに対してメッセージのリストを持つ形を考えて、以下のような構造にしてみます。
lastUpdatedAt
は最後にチャットが投稿された日時を示すTimestampです。
class ChatRoom {
List<Message> messages;
Timestamp lastUpdatedAt;
....
}
class Message {
String text;
...
}
そして特定のチャットルームのFirestore上のパスを"chatRooms/${chatRoomId}"
、特定のメッセージまでのパスを"chatRooms/${chatRoomId}/messages/${messageId}"
としておきます。
上記の前提の場合だと、lastUpdatedAt
の更新とmessages
への追加を同時に行わなければなりません。
このようなときはbatch
を使用しましょう。
final batch = Firestore.instance.batch();
batch.setData(
Firestore.instance
.document("chatRooms/${chatRoomId}")
.collection("messages")
.document(),
{
/// Messageを表現するMap
...
}
);
batch.update(
Firestore.instance
.document("chatRooms/${chatRoomId}/lastUpdatedAt"),
Timestamp.now() // Timestampにnow()は生えていないので雰囲気です
);
await batch.commit();
このあたりは公式のドキュメントにもありますが、batch
の使い方は上記の通りです。
更新、追加、削除したいデータへのDocumentReference
と追加、更新ならそれを行うデータのMap
の組み合わせでリクエストを準備し、batch.commit()
でまとめて実行します。
ところで、messages
への追加をコレクションの新しいドキュメントとなるように行っているのがわかったでしょうか。
データの追加・更新時、messages
をFirestore上においてList型のドキュメントとしているかコレクションとして扱っているかによって変わります。
messages
をコレクションとして扱う利点は、クエリを引くことができるという一点が大きいです。
特定のboolパラメータがtrueになっているものはフィルタするなどの使い方がありますが、本記事の趣旨から外れるため割愛します。
本来messages
はChatRoom
のもつリストですが、こちらをサブコレクションとしてFirestoreに登録するには、ChatRoom
を作成するときにbatch
を用いて以下のようなリクエストを行いましょう。
final batch = Firestore.instance.batch();
Map<String, dynamic> chatRoom; // setDataするMap
chatRoom = {
... // コレクションとして登録するパラメータ以外をMapのkey, valueとして追加
}
final newRoomDocument = Firestore.instance
.collection("chatRooms")
.document():
batch.setData(newRoomDocument, chatRoom);
batch.setData(Firestore.instance
.document("chatRooms/${newRoomDocument.documentId}")
.collection("messages")
.document(),
{
// ルーム作成時の最初のメッセージのデータを作成する
...
}
);
await batch.commit();
このように、コレクションとして登録したいデータを抜いたMapのsetData
と別で、messages
コレクションへのデータ作成という形でバッチに処理を依頼しましょう。
こういうときのために、Firebaseで使う型定義を行ったクラスにはMapに変換するメソッドを作成するなどすると便利ですね。
おわりに
データの取得と購読、追加と更新までのあれこれを紹介しました。
ここまでのところがわかれば、Flutter+Firestoreを実践で使い始める障害はだいぶ消えるのではないでしょうか。
自分がFlutterとFirestoreを使い始めてから一ヶ月ほどたち、いろいろなドキュメントを探したりしましたが日本語でいい感じのドキュメントがあまりに少ないため(見出しよさそう……ってなってもspeakerdeckのスライドだったりして詳しく見たいとこが見れない)、おれたちが作っていくしかないんやという気持ちです。
みんなでいい感じにドキュメント整備していこうな!!
ありがとうございました。
おまけ
12月初め頃、ようやくFlutterのFirestoreにwhere-in
クエリが実装されました
FlutterのFirestoreプラグインはまだまだ整備途中な感じなので、コミットチャンスを狙ってみても面白いかなと思います。