👻

FlutterでFirestoreのデータを取得するキホン

2021/09/20に公開約9,200字

Flutterでアプリケーションを開発する際にmBaaSとしてFirebaseが良く選択されます。また、その流れでデータベースとしてFirestoreを使う選択も自然だと思います。今回は、初めてFlutterでFirestoreを利用する際に必要となる「データの読み取り」の基本について記しました。「Flutterを始めて、Widgetツリーの構築に慣れてきたのでFirebaseをこれから使いたい」「DocumentSnapshot云々の違いがよくわからないまま機械的に使用している」などといった読者が対象です。

はじめに

FlutterのFirebase SDKはFlutterFireと称されます。
※本記事ではSetUp手順は割愛しておりますので、まだの方は下記ドキュメントを見ながら進めてもらえると良いです。ちなみにドキュメントを見ていただくと分かる通り、Firestore操作を行うcloud_firestore以外にもFirebaseに関するプラグインが集約されています。一部Betaのプラグインもありますが、Authenticationなどの主要サービスはStableで利用できます。

https://firebase.flutter.dev/

2020年8月頃に、FlutterFireの開発・メンテナンスはInvertaseが引き継ぐ形となり、それまでは中々メンテナンスされていない様子でしたが、以降大幅なリニューアルをしたり改善スピードがあがったりと活発に動いている印象です。ちなみにInvertaseは、既に4年間React NativeのFirebase SDKの開発も行ってきた実績もあり今後の改善にも期待が持てますね。

https://twitter.com/mikediarmid/status/1297703434632798208?s=20

データ取得の流れ

Firestoreからデータを取得する際に、Flutter側でどういった手順を踏むのかの全体像を先に記載します。

  1. データの参照を作成する
    • ex: CollectionReference, DocumentReference, Query
// CollectionReference
db.collection('cities');

// DocumentReference
db.collection('cities').doc('SF');

// Query
db.collectionGroup('cities');
  1. 参照からsnapshotを取得する
    • ex: get() or snapshots()
    • これは非同期に行われます
// get -> Future
() async {
    await ref.get();
}

// snapshots -> Stream
ref.snapshots();
  1. snapshotからデータを取得する
    • data()
snaspshot.data();

データの参照

CollectionReference, DocumentReference, Queryはいずれもデータへの参照です。あくまで参照ですのでこの時点では実体となるデータは入っていません。また、CollectionReferenceQueryのサブクラスという関係となっており、どちらも複数のドキュメントに対する参照で、ほとんど同じ感覚で使うことができます。対してDocumentReferenceは単一ドキュメントへの参照となります。「あるコレクションのデータを一覧で取得したい」などといった一般的なケースでは、複数ドキュメントに対する参照となるためCollectionReferenceまたはQueryが使われるということになります。参照自体はややこしいポイントがないので次に進みます。

snapshotを取得する

作成したデータの参照からsnapshotを取得します。まず、Firestoreからデータを取得する方法として、snapshots()get()の2つメソッドが用意されています。こちらもFirebaseのドキュメントに詳しく書かれているので、一度目を通すことをおすすめします(SDKによってonSnapshot()などメソッド名が一部異なりますが内容は一緒です)。

Cloud Firestore に格納されているデータを取得するには 2 つの方法があり、どちらの方法でも、ドキュメント、ドキュメントのコレクション、クエリの結果に対して使用することができます。

https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja
用途 リファレンス
get リクエスト1度につき1回のみのドキュメント取得 Future Cloud Firestore でデータを取得する
snapshots 初期スナップショットとその後のドキュメントの変更分のみを取得 Stream Cloud Firestore でリアルタイム アップデートを入手する

Dartは非同期処理のFuture,Streamを言語レベルで扱うことができるため、簡単な実装であればReactiveXなどのパッケージを入れる必要もなく実装ができます。特にStreamの扱いも容易で、snapshotを簡単に受け取れる点からもFlutter(Dart)とFirebaseの親和性の高さが伺えます。

https://dart.dev/tutorials/language/streams

参照から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()GetOptionssouceプロパティを指定しない限り常にサーバからデータを取得する挙動
    • ただし、オフラインキャッシュを無効にしている場合は一部挙動の確認が必要
  • 初期スナップショット以降は変更差分のみのREADとなる点
    • お財布に優しい

snapshotの種類

おそらくはじめてFirestoreを扱う際に苦戦するのが、取得するsnapshotの種類だと思います。

  • DocumentSnapshot
  • QuerySnapshot
  • QueryDocumentSnapshot

私も最初Firestoreを触った際に苦しんだのがこの部分でしたので、私なりに理解したプロセスを参考に記します。snapshotの種類についてはCloud FirestoreのSnapshot三種や他の型のまとめ - Qiitaの記事がよくまとまっていると思います。

そして、実際にsnapshotを取得する方法としては、下記の2パターンが挙げられます。なにかしらのデータをリスト表示したりなど後者を使う機会が多い感覚です。

  1. DocumentReferenceを指定しDocumentを取得するパターン
  2. Collectionを指定し複数のDocumentを取得するパターン
    • Queryを使って複数のDocumentを取得するパターン」とも言えます
    • Queryを使うので返却されるsnapshotの型はQueryXxxとなります

それぞれについて見ていきます。

1. DocumentReferenceを指定しDocumentを取得するパターン

こちらは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になることもあり得ます。

2. Collectionを指定し複数のDocumentを取得するパターン

こちらは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を取得しているに過ぎないので、他の挙動は一緒です。

以上で、参照からデータを取得することができるようになりました。他の詳細は全てドキュメントに記載されておりますので、ご参照下さい。

https://pub.dev/documentation/cloud_firestore/latest/cloud_firestore/cloud_firestore-library.html

※上記1.2.で挙げたソースコードは説明のために冗長に記載しておりますが、実際はワンラインで書くことができます。


(補足)DocumentSnapshotとQueryDocumentSnapshotの違いについて

少し前にこちらのツイートを拝見したので書き足しました。まず、QueryDocumentSnapshotはDocumentSnapshotのサブクラスです。そして違いの結論は「QueryDocumentSnapshotexistsが必ずtrueとなる」ですが、上記の挙動を追っていればその原理は明白です。

Documentを直接参照する1.のケースでは下記の通り、外的操作によって対象のDocumentが削除またはdocumentIDが変更になるなどして取得できない場合にfalseになり得ますが、

複数端末など何らかの影響でサーバ側のDocumentが外部から削除されているケースなどではfalseになることもあり得ます。

Queryを使った2.のケースではQuerySnapshotList<QueryDocumentSnapshot>の形で受け取るため、「QueryDocumentSnapshotを1件ずつ取り出すことができる=Documentのデータがある」ことが保証されるため、QueryDocumentSnapshotexistsは必ずtrueとなります。他は意識すること無くDocumentSnapshotと同様に使えると思います。

withConverterメソッドで型を明示する

実はcloud_firestoreプラグインの2.0.0以降では前述の内容のみではコンパイルエラーになり、型の明示が必要です。

dart
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

詳細につきましては、別記事で執筆しましたのでご査収ください。

https://zenn.dev/tsuruo/articles/23894990188653

(おまけ)iOSのビルドが遅くなる件

本記事を執筆するに当たって、どんな記事が既にあるかなと思い適当に「Flutter, Firestore」で検索してみたところ、この話題が多かったのでおまけで載せておきました。今ではFlutterFireのドキュメントにしっかり明記されております。

https://firebase.flutter.dev/docs/overview#improve-ios-build-times

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ですが、バージョンアップデートする度に書き換える必要があるので注意です(ドキュメントの更新が遅いこともしばしばあります)。ただ、ビルド時に明らかなエラーが出力されるので都度気づいたタイミングで更新する対応で十分かと思います。

ios/Podfile
# ...
target 'Runner' do
  pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => 'x.x.x'
# ...
end

該当イシューはこちらです。

https://github.com/FirebaseExtended/flutterfire/issues/2751

まとめ

今回は、FlutterでFirestoreのデータ取得する流れとその方法について記載しました。
初めてFirestoreを使う際にまず実装する部分の大半がデータ取得に関するところだと思いましたので、その内容にフォーカスしてみました。
他にもデータの追加/削除/クエリはもちろん、オフラインキャッシュ、セキュリティルール、バッチ書き込み、FieldValue、Data BundlesなどFirestoreにはまだまだたくさんの機能があります。
私もまだ勉強の身ですので、すべてを網羅できているわけではありませんが今回の記事の反応を見て少しずつ発信していければと思っています。誤っている箇所などございましたらTwitterのリプやDMなどでご指摘下さい🙇‍♂️

参考

Discussion

ログインするとコメントできます