👻

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

2021/09/20に公開

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