Flutter+Firestoreで作るときのあれこれ(Riverpod+StateNotifierも)
FlutterでFirestoreを扱うときに使うパッケージ
Firestoreから取ってきたドキュメントを扱うときは、
- freezed
- json_serializable
あたりを使うと楽。
また、monoさんが作っている、タイプセーフにFirestoreを扱える - firestore_ref
なども存在する。(破壊的な変更が多いこと、型制約が厳しめなことから、ガッツリ導入することはオススメされていない)
freezed | Dart Package
json_serializable | Dart Package
firestore_ref | Flutter Package
Firstoreから受け取ったTimestamp型をDateTime型に変換する方法
FirestoreからTimestamp型フィールドを含むドキュメントを取ってくると、iOSではTimestamp
型に、AndroidだとDateTime
型にシリアライズされる。
json_serializableで変換用JsonKey
を作ったり、firestore_refでtimestampJsonKey
を使うと、両プラットフォームでDateTime
型として扱えるようになる。
Firestoreからデータを読み取る2つの方法
Firestoreでコレクションやドキュメントを取得する方法は2つ。
- Get → 指定したコレクション、ドキュメントを取得。返り値はFuture。
- Snapshot → コレクションやドキュメントの更新を随時受け取る。返り値はStream。
https://speakerdeck.com/ryunosukeheaven/20201202-port-flutter-firebase-architecture?slide=12
https://github.com/1amageek/Ballcap-iOS/blob/master/Ballcap/DataRepresentable.swift#L215-L282
迷ったら富豪的にSnapshotを使うと良いが、次の点に注意
- Read課金
- スナップショットリスナーが100を超えると通知レイテンシが増加する
https://speakerdeck.com/ryunosukeheaven/20201202-port-flutter-firebase-architecture?slide=17
Flutterでドキュメント変更に際する再描画を最適化する方法
スナップショットで更新があるたびに全ドキュメントを再描画せず、更新時に変更があったドキュメントだけ再描画するには、QuerySnapshot. docChanges
プロパティ+UnmodifiableListView
を使う。
docChanges property - QuerySnapshot class - cloud_firestore library - Dart API
Firestoreでクエリが遅くなった場合
Firestoreのクエリが遅くなるのはこういう時
Google Developers Japan: Cloud Firestore のクエリが遅くなる理由
スナップショットをリッスンする場合の課金について
クエリの結果をリッスンする場合、結果セット内のドキュメントを追加または更新するたびに、1 回の読み取りとして課金されます。また、ドキュメントが結果セットから除去される場合も、ドキュメントが変更されることになるため、1 回の読み取りとして課金されます(これとは対照的に、ドキュメントが削除される場合、これは読み取りとして課金されません)。
リッスンしているドキュメントが更新された場合、変更点があったドキュメント数分Read課金が発生する。
変更がなかったドキュメントはキャッシュから読まれるので、Read課金対象ではない。
Firestoreから取得したデータで無限スクロール
limit()
とstartAfter()
を併用してできそう?
クエリカーソルを使用したデータのページ設定 | Firebase
dart - Create infinite list with Cloud Firestore in flutter - Stack Overflow
FirestoreとRealtimeDatabaseとの使い分け
チャットくらいの、多少遅延があっても気にならない程度のリアルタイム性ならFirestoreで良い。。。とどこかで読んだけど忘れた。
(2021/01/05追記)
いや、公式ユースケースだと、チャットはRealTimeDatabaseを使っているっぽい?
Firebase Realtime Database で数百万人のユーザーのチャット メッセージを同期。
…と思ったけど、そもそもFirestoreの例がないのと、他の項目だとリンク先ではRaltimeDatabaseでなくFirestoreを使っていたり、このページ自体が古い。
Firebase Use Cases
むしろ料金サンプルだと、チャットでもFirestoreを使っている。
Cloud Firestore の料金サンプルを見る | Firebase
NoSQL設計に関する資料
テスト環境用セキュリティルール
とりあえず、ログイン済ユーザなら全ドキュメントへアクセスを許す例。
本番環境でやってはいけない。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if
request.auth.uid != null;
}
}
}
Firestore+freezedでどうやってドキュメントIDを参照するか
freezedでエンティティを定義するとき、次のようにすると思う。
abstract class Todo with _$Todo {
factory Todo({
String title,
() DateTime createdAt,
}) = _Todo;
factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
}
ただ、これを削除・更新したいとき、エンティティはDocumentIdを含んでない。
どのようにDocumentIdを参照するか?
TodoがDocumentIdを含むように拡張するのも気持ち悪い。
firestore_refを使い、エンティティの代わりにxxDocを使えば綺麗に書けそう?
class TodosRef extends CollectionRef<Todo, TodoDoc, TodoRef> {
TodosRef() : super(FirebaseFirestore.instance.collection('todos'));
Map<String, dynamic> encode(Todo data) =>
replacingTimestamp(json: data.toJson());
TodoDoc decode(DocumentSnapshot snapshot, TodoRef docRef) {
assert(docRef != null);
return TodoDoc(
docRef,
Todo.fromJson(snapshot.data()),
);
}
TodoRef docRef(DocumentReference ref) => TodoRef(
ref: ref,
todosRef: this,
);
}
class TodoRef extends DocumentRef<Todo, TodoDoc> {
const TodoRef({
DocumentReference ref,
this.todosRef,
}) : super(ref: ref, collectionRef: todosRef);
final TodosRef todosRef;
}
class TodoDoc extends Document<Todo> {
const TodoDoc(
this.todoRef,
Todo entity,
) : super(todoRef, entity);
final TodoRef todoRef;
}
mono0926/flutter_firestore_ref: Cross-platform(including web) Firestore type-safe wrapper.
Todoアプリを作るときのFirestore設計
Firebaseの中の人曰く、階層型データモデルよりも、フラットなデータモデルの方が、後から拡張しやすくていいらしい。
StateNotifier+Firestoreでスナップショットを使う
Firestoreでsnapshots()
を使いつつ、リアルタイムにstate
を更新するにはsnapshots().listen()
内へ処理を書く。
class TodoController extends StateNotifier<TodoState> {
TodoController(this._read) : super(TodoState()) {
todoQuery.snapshots().listen((snapshot) {
final newTodos = snapshot.docs
.map(
(doc) => Todo.fromJson(
doc.data(),
),
)
.toList();
state = state.copyWith(todos:newTodos);
});
}
}
firestore_refを使うなら
todoDocStream = todosRef.documents(
(r) => r
.where(
'userId',
isEqualTo: 'hogefuga',
)
);
todoDocStream.listen((docs){
state=state.copyWith(todos:docs);
});
Firestoreで読み取り回数が異様に多い
アクティブな接続は1、スナップショットリスナー2。
ドキュメントの総数も対して多くないのに、急に100くらい読み取り回数が増えることがある。
アプリからドキュメント操作してみて、ドキュメント追加だと読み取り回数+1、削除だと読み取り回数に変化ないことは確認していて、実装ミスはないっぽい。
で、気づいたが、ブラウザでFirestoreコンソール>データを開くときに、ドキュメント読み取りカウントをされているっぽい。
とりあえず _
という空のコレクションを作り、デフォルトでそっちを読み込むようにしてみる。
The member 'state' can only be used within instance members of ...
RiverpodでStateNotifierを使っていて、hogeController.state
的な形でstate
へアクセスすると次の警告が出る。
info: The member 'state' can only be used within instance members of subclasses of 'package:state_notifier/state_notifier.dart'. (invalid_use_of_protected_member at [hoge_fuga] lib/ui/pages/home/home_page.dart:122)
Remiさん曰く
No this is expected
It allows reading the notifier without listening to the state, to avoid pointless rebuilds.
(state
をリッスンしないことで)リビルドを避けながら、StateNotifier(hogeController
)を読むのが正しいとのこと。
つまり
final hogeController = useProvider(hogeControllerProvider);
hogeController.state.name;
ではなく、
final hogeController = useProvider(hogeControllerProvider);
final hogeState = useProvider(hogeControllerProvider.state);
hogeState.name;
として、状態を読むのが正しい。
freezed中でインスタンスを使う
@lateをつければ、freezed中でも、インスタンスが使える
get()
exists()
getAfter()
を使った場合の課金額
セキュリティルールで セキュリティルールを評価する際に、get()
exists()
を使うと、Firestore内の別ドキュメントを使ったルール評価ができる。
ただ、get()
やexists()
を使うと、ドキュメントに対する読み取りコストも発生する。
なので、例えば collection('post').get()
で100個ドキュメントを読み取った際、セキュリティルール評価にget()
を使ってるから、さらに100回追加で読み取りコストがかかる、だと嫌だなと思った。
ただ、実際には、get()
等でセキュリティルールを評価する際に発生する読み取りコストは、1リクエストあたり1回らしい。
(上記例だと、100+1回分の読み取りコストで済む)