🫠

riverpod generatorで.familyを使ったらハマった!

2024/01/22に公開5

これは何?

いつもの様に、技術の探究をしてそれを新しいコードに書き換えたらエラーでハマった方法を解消する記事です。これであってるのかな....

このスクラップを使って遊んでました。
https://zenn.dev/joo_hashi/scraps/28f11d3dac1209

これが書き換える前のコードです。昔のプロバイダーを定義するriverpodの書き方。

プロバイダーを書くコード
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:todo_fire/model/task_state.dart';

final firebaseProvider =
    Provider<FirebaseFirestore>((ref) => FirebaseFirestore.instance);

// タスク用のwithConverterを作成
final taskConverterProvider = Provider((ref) {
  final firebase = ref.watch(firebaseProvider);
  return firebase.collection('tasks').withConverter<TaskState>(
        fromFirestore: (snapshot, _) {
          final data = snapshot.data()!;
          data['id'] = snapshot.id;
          return TaskState.fromJson(data).copyWith(id: snapshot.id);
        },
        toFirestore: (value, _) => value.toJson(),
      );
});

final taskContentConverterProvider = Provider.family((ref, String taskId) {
  final firebase = ref.watch(firebaseProvider);
  return firebase.collection('tasks').doc(taskId).collection('content').withConverter<TaskStateContent>(
        fromFirestore: (snapshot, _) {
          final data = snapshot.data()!;
          data['id'] = snapshot.id;
          return TaskStateContent.fromJson(data);
        },
        toFirestore: (value, _) => value.toJson(),
      );
});

// タスク用のStreamProviderを作成
final taskStreamProvider = StreamProvider.autoDispose<List<TaskState>>((ref) {
  final todoConverter = ref.watch(taskConverterProvider);
  return todoConverter
      .snapshots()
      .map((e) => e.docs.map((e) => e.data()).toList());
});

// サブコレクションのcontent用のwithConverterを作成
final contentConverterProvider = Provider.family((ref, String taskId) {
  final firebase = ref.watch(firebaseProvider);
  return firebase.collection('tasks').doc(taskId).collection('contents').withConverter<TaskStateContent>(
        fromFirestore: (snapshot, _) => TaskStateContent.fromJson(snapshot.data()!),
        toFirestore: (value, _) => value.toJson(),
      );
});

// サブコレクションのcontent用のStreamProviderを作成
final contentStreamProvider = StreamProvider.autoDispose.family<List<TaskStateContent>, String>((ref, String taskId) {
  final contentConverter = ref.watch(contentConverterProvider(taskId));
  return contentConverter
      .snapshots()
      .map((e) => e.docs.map((e) => e.data()).toList());
});

riverpod generatorに書き換えたコード

自動生成するコード
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:todo_fire/model/task_state.dart';
part 'firebase_provider.g.dart';


FirebaseFirestore firebaseFirestore(FirebaseFirestoreRef ref) {
  return FirebaseFirestore.instance;
}

// タスク用のwithConverterを作成

CollectionReference<TaskState> taskConverter(TaskConverterRef ref) {
  final firebase = ref.watch(firebaseFirestoreProvider);
  return firebase.collection('tasks').withConverter<TaskState>(
        fromFirestore: (snapshot, _) {
          final data = snapshot.data()!;
          data['id'] = snapshot.id;
          return TaskState.fromJson(data).copyWith(id: snapshot.id);
        },
        toFirestore: (value, _) => value.toJson(),
      );
}


CollectionReference<TaskStateContent> taskContentConverter(TaskContentConverterRef ref,
{required String taskId}) {
  final firebase = ref.watch(firebaseFirestoreProvider);
  return firebase.collection('tasks').doc(taskId).collection('content').withConverter<TaskStateContent>(
        fromFirestore: (snapshot, _) {
          final data = snapshot.data()!;
          data['id'] = snapshot.id;
          return TaskStateContent.fromJson(data);
        },
        toFirestore: (value, _) => value.toJson(),
      );
}

// タスク用のStreamProviderを作成

Stream<List<TaskState>> taskStream(TaskStreamRef ref) {
  final todoConverter = ref.watch(taskConverterProvider);
  return todoConverter
      .snapshots()
      .map((e) => e.docs.map((e) => e.data()).toList());
}

// サブコレクションのcontent用のwithConverterを作成

CollectionReference<TaskStateContent> contentConverter(ContentConverterRef ref, {required String taskId}) {
  final firebase = ref.watch(firebaseFirestoreProvider);
  return firebase.collection('tasks').doc(taskId).collection('contents').withConverter<TaskStateContent>(
        fromFirestore: (snapshot, _) => TaskStateContent.fromJson(snapshot.data()!),
        toFirestore: (value, _) => value.toJson(),
      );
}

// サブコレクションのcontent用のStreamProviderを作成

Stream<List<TaskStateContent>> contentStream(ContentStreamRef ref, {required String taskId}) {
  final contentConverter = ref.watch(contentConverterProvider(taskId: taskId));
  return contentConverter
      .snapshots()
      .map((e) => e.docs.map((e) => e.data()).toList());
}

riverpod generatorに変更すると、引数の渡し方が変わってるみたい???
taskId: taskIdと書く!

final contentConverter = ref.watch(contentConverterProvider(taskId: taskId));

コードジャンプして内部のコードをというか、自動生成されたコードを見るとこんなのができてる。これを使えということらしい。

ないとどうなるかというと、エラーが出る!

コードアンドレで紹介されてた。この方法でやるみたいだ。
https://codewithandrea.com/articles/flutter-riverpod-generator/

公式にもそれっぽいの書いてあるけど、rivepod generatorじゃないよ🫠
https://riverpod.dev/ja/docs/concepts/modifiers/family

pab.devには書いてある?
https://pub.dev/packages/riverpod_generator

AsyncValue<List<Product>> products = ref.watch(fetchProductProvider(page: 1));

View側のコードも修正しておこう

フォルダのページ:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:todo_fire/provider/firebase_provider.dart';
import 'package:todo_fire/view/conten_view.dart';
import 'package:todo_fire/view/create_task_view.dart';

class TaskView extends ConsumerWidget {
  const TaskView({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final taskState = ref.watch(taskStreamProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Task'),
      ),
      body: taskState.when(
        data: (tasks) {
          return ListView.builder(
            itemCount: tasks.length,
            itemBuilder: (context, index) {
              final task = tasks[index];
              return ListTile(
                // 画面左にフォルダのアイコンを表示
                leading: const Icon(Icons.folder),
                title: Text(task.title),
                onTap: () {
                  Navigator.of(context).push(
                    MaterialPageRoute(
                      builder: (context) => ContentView(task: task),
                    ),
                  );
                },
              );
            },
          );
        },
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (e, s) => Center(child: Text(e.toString())),
      ),
      // tasksにドキュメントを作成するForm付きのDialogを表示
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.of(context).push(
            MaterialPageRoute(
              builder: (context) => const CreateTaskView(),
            ),
          );
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

フォルダの中のページ:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:todo_fire/model/task_state.dart';
import 'package:todo_fire/provider/firebase_provider.dart';

class ContentView extends ConsumerWidget {
  const ContentView({super.key, required this.task});

  final TaskState task;

  
  Widget build(BuildContext context, WidgetRef ref) {
    final content = ref.watch(contentStreamProvider(taskId: task.id));
    final contentController = TextEditingController();

    return Scaffold(
      appBar: AppBar(
        title: Text(task.title),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              controller: contentController,
              decoration: const InputDecoration(
                labelText: 'Content',
                hintText: 'Input content',
              ),
            ),
          ),
          Expanded(
            child: content.when(
              data: (contents) {
                return ListView.builder(
                  itemCount: contents.length,
                  itemBuilder: (context, index) {
                    final content = contents[index];
                    return ListTile(
                      // 左にファイルのiconを表示
                      leading: const Icon(Icons.file_copy),
                      title: Text(content.content),
                    );
                  },
                );
              },
              loading: () => const Center(child: CircularProgressIndicator()),
              error: (e, s) => Center(child: Text(e.toString())),
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // doumentIDを取得する
          final id = ref.read(contentConverterProvider(taskId: task.id)).doc().id;
          final content = TaskStateContent(
            id: id,
            content: contentController.text,
            createdAt: Timestamp.now().toDate(),
          );
          // contentを作成する
          ref.read(contentConverterProvider(taskId: task.id)).add(content);
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

まとめ

まあ、こん感じでエラーにハマって苦しみました💦
自動生成形はこれだから嫌いなんですよ。SwiftUIとJetpack Composeは自動生成ないから、そんな人たちがFlutterやるとエラーで混乱するらしいです。

Discussion

osakiosaki

公式にもそれっぽいの書いてあるけど、rivepod generatorじゃないよ🫠

この辺りでしょうか。
https://riverpod.dev/ja/docs/essentials/passing_args#updating-our-ui-to-pass-arguments

osakiosaki

いえ、そうではなく。。。

But now that our provider receives arguments, the syntax to consume it is slightly different. The provider is now a function, which needs to be invoked with the parameters requested.

The parameters passed to the provider corresponds to the parameters of the annotated function, minus the "ref" parameter.

とありますよね?


Stream<List<TaskStateContent>> contentStream(ContentStreamRef ref, {required String taskId}) {

そして上記はNamed Parameterを使用しているのですから、

ref.watch(contentConverterProvider(taskId: taskId))

となりますが、以下のように


Stream<List<TaskStateContent>> contentStream(ContentStreamRef ref, String taskId) {

とすれば、

ref.watch(contentConverterProvider(taskId))

ではないのでしょうか。

riverpod generatorに変更すると、引数の渡し方が変わってるみたい???
taskId: taskIdと書く!

つまり、riverpod_generatorを使用したことでNamed Parameterで呼び出さなければならなくなったわけではなく、contentConverterProviderがNamed Parameterを使用しているから、なのではありませんか?

JboyHashimotoJboyHashimoto

required これつけてるからかな。まあ、以前taskId: taskIdなりませんでしたからね。

osakiosaki

Named Parameterか否かの違いだと思います。