🔥

Riverpod2.6 WithConverter

2025/01/24に公開

Ref refに対応して書いてみた。

RiverpodがRef refという引数に変更された。これに対応したWithConverter Providerを自作してみた。

Refに対応してみたけどまた変更が出て対応が必要になりそう?
https://x.com/remi_rousselet/status/1882405052310339709

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()),
      }
    );
  }
}

おまけ

昔書いた記事もついでにご紹介しておきます。
https://zenn.dev/joo_hashi/articles/17f9fad30426dd

Discussion