Riverpod2.6 WithConverter
Ref refに対応して書いてみた。
RiverpodがRef refという引数に変更された。これに対応したWithConverter Providerを自作してみた。
Refに対応してみたけどまた変更が出て対応が必要になりそう?
CollectionReference型を指定するらしい。
How to WithConverter
よくデータを型安全に扱えるのだと言われてますね。どんなメリットがあるのか?
内部実装を見てみた。長いですね(^^;;
Riverpod2.6.0~変更があった(Ref ref)に対応。
// `extends Object?` so that type inference defaults to `Object?` instead of `dynamic`
abstract class CollectionReference<T extends Object?> implements Query<T> {
/// Returns the ID of the referenced collection.
String get id;
/// Returns the parent [DocumentReference] of this collection or `null`.
///
/// If this collection is a root collection, `null` is returned.
// This always returns a DocumentReference even when using withConverter
// because we do not know what is the correct type for the parent doc. @override
DocumentReference<Map<String, dynamic>>? get parent;
/// A string containing the slash-separated path to this CollectionReference
/// (relative to the root of the database).
String get path;
/// Returns a `DocumentReference` with an auto-generated ID, after
/// populating it with provided [data].
///
/// The unique key generated is prefixed with a client-generated timestamp
/// so that the resulting list will be chronologically-sorted.
Future<DocumentReference<T>> add(T data);
/// {@template cloud_firestore.collection_reference.doc}
/// Returns a `DocumentReference` with the provided path.
///
/// If no [path] is provided, an auto-generated ID is used.
///
/// The unique key generated is prefixed with a client-generated timestamp
/// so that the resulting list will be chronologically-sorted.
/// {@endtemplate}
DocumentReference<T> doc([String? path]);
/// Transforms a [CollectionReference] to manipulate a custom object instead
/// of a `Map<String, dynamic>`.
///
/// This makes both read and write operations type-safe.
///
/// ```dart
/// final modelsRef = FirebaseFirestore
/// .instance
/// .collection('models')
/// .withConverter<Model>(
/// fromFirestore: (snapshot, _) => Model.fromJson(snapshot.data()!),
/// toFirestore: (model, _) => model.toJson(),
/// );
///
/// Future<void> main() async {
/// // Writes now take a Model as parameter instead of a Map
/// await modelsRef.add(Model());
///
/// // Reads now return a Model instead of a Map
/// final Model model = await modelsRef.doc('123').get().then((s) => s.data());
/// }
/// ```
// `extends Object?` so that type inference defaults to `Object?` instead of `dynamic`
CollectionReference<R> withConverter<R extends Object?>({
required FromFirestore<R> fromFirestore,
required ToFirestore<R> toFirestore,
});
}
解説すると
- 内部実装のコメントを翻訳
-
Map<String,dynamic>
の代わりにカスタムオブジェクトを操作するように変換する。 - これにより、読み取りと書き込みの両方の操作が型安全になる。
- 型推論のデフォルトが
dynamic
ではなくObject?
になるようにObject?
を拡張する。
-
Riverpodで定義するときのデータ型は、CollectionReferenceを指定します。freezedと組み合わせた例をご紹介します。
CartStateというドメインオブジェクトを作成。カートというドメイン知識を持ったクラス。カートモデルクラスといった方がいいですね。モデルクラスを定義する。
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'cart.freezed.dart';
part 'cart.g.dart';
class CartState with _$CartState {
const factory CartState({
('') String id,
('') String item
}) = _CartState;
factory CartState.fromJson(Map<String, Object?> json)
=> _$CartStateFromJson(json);
}
CollectionReference<CartState> cartCollection(Ref ref) {}
という感じで定義すればcart collection idを扱うことができるデータコンバーターを作成できます。
ドキュメントIDを使いたいときに、モデルクラスは、@Default('') String id,
のままだとデータを更新・削除するイベントで使えません。データの整合性がないから。key valueで指定する。fromFirestoreで、読み取りのときに、DocumentSnapshotを取得する必要があります。
toFirestoreで書き込みのときに、json.remove('id')でキーを指定してStringのidを削除も必要。
こんな感じで使えると覚えてくれれば大丈夫です。
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_firestore/domain/cart.dart';
part 'cart_stream.g.dart';
(keepAlive: true)
FirebaseFirestore firebaseFirestore(Ref ref) => FirebaseFirestore.instance;
(keepAlive: true)
CollectionReference<CartState> cartCollection(Ref ref) {
return ref.read(firebaseFirestoreProvider).collection('cart').withConverter(
fromFirestore: (snapshot, _) {
final data = snapshot.data()!;
return CartState.fromJson({
'id': snapshot.id,
...data,
});
},
toFirestore: (value, _) {
final json = value.toJson();
json.remove('id');
return json;
},
);
}
Stream<List<CartState>> cartStream(Ref ref) async* {
final collection = ref.read(cartCollectionProvider);
yield* collection.snapshots().map((snapshot) => snapshot.docs.map((doc) => doc.data()).toList());
}
View側でデータを取得する場合は、Riverpod2.6の場合は、switch式で記載します。deleteメソッドを使用しているところで、Stringのidを指定してCloud Firestoreの特定のデータを削除することができます。ここで正しいデータ型として扱うことができてないとエラーが出ます。例えばString idで指定するだけの場合。
freezedとWithConverterを組み合わせると安全にidを指定して削除できる。
ref.read(cartCollectionProvider).doc(value[index].id).delete();
Viewのコードでの使用例)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_firestore/infra/cart_stream.dart';
class CartList extends ConsumerWidget {
const CartList({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final cartList = ref.watch(cartStreamProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Cart List'),
),
body: switch(cartList) {
AsyncData(:final value) => ListView.builder(
itemCount: value.length,
itemBuilder: (context, index) {
return ListTile(
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
ref.read(cartCollectionProvider).doc(value[index].id).delete();
},
),
title: Text(value[index].item),
);
},
),
AsyncError(:final error) => Center(child: Text('error: $error')),
_=> const Center(child: CircularProgressIndicator()),
}
);
}
}
おまけ
昔書いた記事もついでにご紹介しておきます。
Discussion