FlutterでFirestoreのデータを取得するキホン
Flutter でアプリケーションを開発する際に mBaaS として Firebase が良く選択されます。また、その流れでデータベースとして Firestore を使う選択も自然だと思います。今回は、はじめて Flutter で Firestore を利用する際に必要となる「データの読み取り」の基本について記しました。「Flutter を始めて、Widgetツリーの構築に慣れてきたので Firebase をこれから使いたい」「DocumentSnapshot 云々の違いがよくわからないまま機械的に使用している」などといった読者が対象です。
はじめに
Flutter の Firebase SDK は FlutterFire
と称されます。
※本記事では SetUp 手順は割愛しておりますので、まだの方は下記ドキュメントを見ながら進めてもらえると良いです。ちなみにドキュメントを見ていただくとわかる通り、Firestore 操作をする cloud_firestore
以外にも Firebase に関するプラグインが集約されています。一部 Beta のプラグインもありますが、Authentication などの主要サービスは Stable で利用できます。
2020年8月頃に、FlutterFire の開発・メンテナンスは Invertase が引き継ぐ形となり、それまでは中々メンテナンスされていない様子でしたが、以降大幅なリニューアルをしたり改善スピードがあがったりと活発に動いている印象です。ちなみに Invertase は、すでに4年間 React NativeのFirebase SDKの開発も行ってきた実績 もあり今後の改善にも期待が持てますね。
データ取得の流れ
Firestore からデータを取得する際に、Flutter 側でどういった手順を踏むのかの全体像を先に記載します。
- データの参照を作成する
- ex:
CollectionReference
,DocumentReference
,Query
- ex:
// CollectionReference
db.collection('cities');
// DocumentReference
db.collection('cities').doc('SF');
// Query
db.collectionGroup('cities');
- 参照から snapshot を取得する
- ex:
get()
orsnapshots()
- これは非同期に行われます
- ex:
// get -> Future
() async {
await ref.get();
}
// snapshots -> Stream
ref.snapshots();
- snapshot からデータを取得する
data()
snaspshot.data();
データの参照
CollectionReference
, DocumentReference
, Query
はいずれもデータへの参照です。あくまで参照ですのでこの時点では実体となるデータは入っていません。また、CollectionReference
は Query
のサブクラスという関係となっており、どちらも複数のドキュメントに対する参照で、ほとんど同じ感覚で使うことができます。対して DocumentReference は単一ドキュメントへの参照となります。「あるコレクションのデータを一覧で取得したい」などといった一般的なケースでは、複数ドキュメントに対する参照となるため CollectionReference
または Query
が使われるということになります。参照自体はややこしいポイントがないので次に進みます。
snapshotを取得する
作成したデータの参照から snapshot を取得します。まず、Firestore からデータを取得する方法として、snapshots()
と get()
の2つメソッドが用意されています。こちらも Firebase のドキュメントに詳しく書かれているので、一度目を通すことをオススメします(SDK によって onSnapshot()
などメソッド名が一部異なりますが内容は一緒です)。
Cloud Firestore に格納されているデータを取得するには 2 つの方法があり、どちらの方法でも、ドキュメント、ドキュメントのコレクション、クエリの結果に対して使用することができます。
用途 | 型 | リファレンス | |
---|---|---|---|
get | リクエスト1度につき1回のみのドキュメント取得 | Future | Cloud Firestore でデータを取得する |
snapshots | 初期スナップショットとその後のドキュメントの変更分のみを取得 | Stream | Cloud Firestore でリアルタイム アップデートを入手する |
Dart は非同期処理の Future,Stream を言語レベルで扱うことができるため、簡単な実装であれば ReactiveX などのパッケージを入れる必要もなく実装ができます。とくに Stream の扱いも容易で、snapshot を簡単に受け取れる点からも Flutter(Dart)と Firebase の親和性の高さが伺えます。
参照から snapshot の取得(+データの取得)の一連の流れをコードにすると下記の通り記述できます(コード内の型については後述します)。
// get
Future<Hoge> fetchHoge({required DocumentReference<Hoge> ref}) async =>
(await ref.get()).data()!;
// snapshots
Stream<Hoge> hogeStream({required DocumentReference<Hoge> ref}) =>
ref.snapshots().map(
(snapshot) => snapshot.data()!,
);
どちらを使うベきかについてはサービスの要件次第という前提はありますが、個人的には下記の理由で snapshots()
を使うことが多いです。
- ほぼノーコストでリアルタイムな自動更新ができる点
- キャッシュの振る舞いが優秀な点
-
get()
はGetOptions
のsouce
プロパティを指定しない限り常にサーバからデータを取得する挙動 - ただし、オフラインキャッシュを無効にしている場合は一部挙動の確認が必要
-
- 初期スナップショット以降は変更差分のみの READ となる点
- お財布に優しい
snapshotの種類
おそらくはじめて Firestore を扱う際に苦戦するのが、取得する snapshot の種類だと思います。
- DocumentSnapshot
- QuerySnapshot
- QueryDocumentSnapshot
私も最初 Firestore を触った際に苦しんだのがこの部分でしたので、私なりに理解したプロセスを参考に記します。snapshot の種類については Cloud FirestoreのSnapshot三種や他の型のまとめ - Qiita の記事がよくまとまっていると思います。
そして、実際に snapshot を取得する方法としては、下記の2パターンが挙げられます。なにかしらのデータをリスト表示したりなど後者を使う機会が多い感覚です。
-
DocumentReference
を指定しDocument
を取得するパターン -
Collection
を指定し複数のDocument
を取得するパターン- 「
Query
を使って複数のDocument
を取得するパターン」とも言えます -
Query
を使うので返却される snapshot の型はQueryXxx
となります
- 「
それぞれについて見ていきます。
DocumentReference
を指定し Document
を取得するパターン
1. こちらは Document を指定してデータを取得するパターンです。冗長に書くと下記となります。
final docRef = FirebaseFirestore.instance.doc('xxx') // DocumentReference
final docSnapshot = await docRef.get(); // DocumentSnapshot
final data = documentSnapshot.exists ? documentSnapshot.data() : null; // `data()`で中身を取り出す
- 1行目: パスを明示しドキュメントの参照である
DocumentReference
を返却 - 2行目: DocumentReference による参照のため、
Document
+snapshot
つまりDocumentSnapshot
を返却 - 3行目:
DocumentSnapshot
からdata()
で中身を取り出す
ちなみに DocumentSnapshot
型では exists
プロパティが用意 されており、存在有無を確認してデータを取り出すことができます。基本的に DocumentReference から取得するケースでは exists
が true となることが多いはずですが、複数端末など何らかの影響でサーバ側の Document が外部から削除されているケースなどでは false になることもあり得ます。
Collection
を指定し複数の Document
を取得するパターン
2. こちらは Collection や CollectionGroup を指定して(Query を活用して)データを取得するパターンです。
final collectionRef = FirebaseFirestore.instance.collection('xxx'); // CollectionReference
final querySnapshot = await collectionRef.get(); // QuerySnapshot
final queryDocSnapshot = querySnapshot.docs; // List<QueryDocumentSnapshot>
for (final snapshot in queryDocSnapshot) {
final data = snapshot.data(); // `data()`で中身を取り出す
}
- 1行目: Collection の参照である
CollectionReference
を返却-
CollectionGroup
の場合はQuery
型を返却
-
- 2行目:
CollectionReference
から snapshot を取得しQuerySnapshot
を返却- 「
Collection
で発行されたQuery
+snapshot
」でQuerySnapshot
- 「
- 3行目:
QuerySnapshot
からdocs
で Document を取得しList<QueryDocumentSnapshot>
となる- ここでは1行目で指定した Collection に属する Document がリスト形式で格納されている
- もちろん Collection に属する Document がない場合には
List<QueryDocumentSnapshot>
は0件となる
- 4行目以降:
QueryDocumentSnapshot
がリスト形式で格納されているだけなので、1。のDocumentSnapshot
で中身を取り出した時同様data()
で取り出せます。
CollectionGroup
では1行目のみ異なりますが、結局 Query
を使って Document を取得しているに過ぎないので、他の挙動は一緒です。
以上で、参照からデータを取得できるようになりました。他の詳細はすべてドキュメントに記載されておりますので、ご参照ください。
※上記1.2。で挙げたソースコードは説明のために冗長に記載しておりますが、実際はワンラインで書くことができます。
(補足)DocumentSnapshotとQueryDocumentSnapshotの違いについて
少し前に こちらのツイート を拝見したので書き足しました。まず、QueryDocumentSnapshotはDocumentSnapshotのサブクラス です。そして違いの結論は「QueryDocumentSnapshot
は exists
が必ず true
となる」ですが、上記の挙動を追っていればその原理は明白です。
Document を直接参照する1。のケースでは下記の通り、外的操作によって対象の Document が削除または documentID が変更になるなどして取得できない場合に false
となり得ますが、
複数端末など何らかの影響でサーバ側のDocumentが外部から削除されているケースなどではfalseになることもあり得ます。
Query を使った2。のケースでは QuerySnapshot
が List<QueryDocumentSnapshot>
の形で受け取るため、「QueryDocumentSnapshot
を1件ずつ取り出すことができる=Document のデータがある」ことが保証されるため、QueryDocumentSnapshot
の exists
は必ず true
となります。他は意識すること無く DocumentSnapshot
と同様に使えると思います。
withConverterメソッドで型を明示する
実は cloud_firestore
プラグインの2.0.0以降では前述の内容のみではコンパイルエラーになり、型の明示が必要です。
Future<void> example(
- DocumentReference documentReference,
+ DocumentReference<Map<String, dynamic>> documentReference,
- CollectionReference collectionReference,
+ CollectionReference<Map<String, dynamic>> collectionReference,
- Query query,
+ Query<Map<String, dynamic>> query,
) {
// ...
}
(参考)Migration to cloud_firestore 2.0.0 | FlutterFire
詳細につきましては、別記事で執筆しましたのでご査収ください。
(おまけ)iOSのビルドが遅くなる件
本記事を執筆するに当たって、どんな記事がすでにあるかなと思い適当に「Flutter, Firestore」で検索してみたところ、この話題が多かったのでおまけで載せておきました。今では FlutterFire のドキュメントにしっかり明記されております。
Currently the Firestore iOS SDK depends on some 500k lines of mostly C++ code which can take upwards of 5 minutes to build in XCode.
原因は iOS SDK が50万行の C++コードで書かれていることで、Xcode でのビルドに最大5分ほどかかると記載されております。ドキュメントに記載されている通り、invertase の ios-sdk-framework のリポジトリとバージョンの tag を指定しプリコンパイルする形にすると改善されます。ちなみにこの tag ですが、バージョンアップデートする度に書き換える必要があるので注意です(ドキュメントの更新が遅いこともしばしばあります)。ただ、ビルド時に明らかなエラーが出力されるので都度気づいたタイミングに更新する対応で十分かと思います。
# ...
target 'Runner' do
pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => 'x.x.x'
# ...
end
該当イシューはこちらです。
まとめ
今回は、Flutter で Firestore のデータ取得する流れとその方法について記載しました。
はじめて Firestore を使う際にまず実装する部分の大半がデータ取得に関するところだと思いましたので、その内容にフォーカスしてみました。
他にもデータの追加/削除/クエリはもちろん、オフラインキャッシュ、セキュリティルール、バッチ書き込み、FieldValue、Data Bundles など Firestore にはまだまだたくさんの機能があります。
私もまだ勉強の身ですので、すべてを網羅できているわけではありませんが今回の記事の反応を見て少しずつ発信していければと思っています。誤っている箇所などございましたら Twitter のリプや DM などでご指摘ください🙇♂️
Discussion