📌

StateNotifierProviderで.familyを使おうとしたら沼った話

2023/01/05に公開

StateNotifierProviderで.familyを使おうとしたら沼った話


providerから乗り換えようと思い、riverpodを学び始めました。StateNotifierProviderでfamilyの使い方を練習中にうまくいかなかったので、その原因をこの記事に残しておきます。

原因は、familyのパラメータがあべこべになっていたことでした。「間違った使い方」の章で説明しています。
同じプロバイダに対して、familyのパラメータに異なる値を入れて複数のデータを扱うと、familyのパラメータ毎に状態を管理するみたいです。ややこしい文章ですみません。例を見せます。

Screenshot_20230105_005710.png

StateNotifierProviderを使ってtodoリストみたいなものを作りました。
元ネタは公式です。(https://riverpod.dev/ja/docs/providers/state_notifier_provider)

コード

main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_state_notifier/todo.dart';

void main() {
 runApp(
   const ProviderScope(
     child: MyApp(),
   ),
 );
}

class MyApp extends StatelessWidget {
 const MyApp({super.key});

 
 Widget build(BuildContext context) {
   return const MaterialApp(
     home: HomePage(),
   );
 }
}

class HomePage extends ConsumerWidget {
 const HomePage({Key? key}) : super(key: key);

 
 Widget build(BuildContext context, WidgetRef ref) {
   return Scaffold(
     appBar: AppBar(),
     body: Row(
       children: [
         Expanded(
           child: TodoListView(text: "first",),
           ),
         Expanded(
           child: TodoListView(text: "second",),
         )
       ],
     ),
   );
 }
}

todo.dart
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class Todo {
 const Todo({
   required this.id,
   required this.description,
   required this.completed,
 });

 final String id;
 final String description;
 final bool completed;

 Todo copyWith({String? id, String? description, bool? completed}) {
   return Todo(
     id: id ?? this.id,
     description: description ?? this.description,
     completed: completed ?? this.completed,
   );
 }
}

class TodoListView extends ConsumerWidget {
 const TodoListView({Key? key, required this.text}) : super(key: key);

 final String text;

 
 Widget build(BuildContext context, WidgetRef ref) {
   List<Todo> todos = ref.watch(todosProvider(text));
   var random = Random();

   return ListView(
     padding: const EdgeInsets.all(8.0),
     children: [
       Text(text),
       ElevatedButton(
         onPressed: () {
           ref.read(todosProvider(text).notifier).addTodo(
                 Todo(
                   id: random.nextInt(100).toString(),
                   description: random.nextInt(100).toString(),
                   completed: false,
                 ),
               );
         },
         child: const Text("add"),
       ),
       for (final todo in todos)
         CheckboxListTile(
           value: todo.completed,
           onChanged: (value) =>
               ref.read(todosProvider(text).notifier).toggle(todo.id),
           title: Text(todo.description),
         )
     ],
   );
 }
}

class TodosNotifier extends StateNotifier<List<Todo>> {
 TodosNotifier({
   required this.text,
 }) : super([]) {
   addTodo(
     Todo(
       id: "0",
       description: "0",
       completed: false,
     ),
   );
 }

 final String text;

 void addTodo(Todo todo) {
   state = [...state, todo];
 }

 void toggle(String todoId) {
   state = [
     for (final todo in state)
       if (todo.id == todoId)
         todo.copyWith(completed: !todo.completed)
       else
         todo,
   ];
 }
}

final todosProvider =
   StateNotifierProvider.family<TodosNotifier, List<Todo>, String>(
       (ref, text) {
 return TodosNotifier(text: text);
});


説明

familyのパラメータは左右で異なります。
   左 first
   右 second
 右も左も同じtodosProviderを使ってデータを管理していますが、familyのパラメータが異なるため、右と左は別のデータとして管理されています。
左のaddを押せば左のデータが増え、
右のaddを押せば右のデータが増えます。

間違った使い方

todoListView内のwatchやreadでProviderの値を参照する際に、familyのパラメータが別々になっていると、全く思い通りに動作してくれません。
下のコードだと、"first","second","third"という3種類のパラメータが与えられているので、3箇所の異なる場所でデータが管理されています。この状態だと、addを押してもチェックボックスを押しても見た目は何も変わりません。

todoListView
class TodoListView extends ConsumerWidget {
  const TodoListView({Key? key, required this.text}) : super(key: key);

  final String text;

  
  Widget build(BuildContext context, WidgetRef ref) {
    //注意!
    List<Todo> todos = ref.watch(todosProvider("first"));
    var random = Random();

    return ListView(
      padding: const EdgeInsets.all(8.0),
      children: [
        Text(text),
        ElevatedButton(
          onPressed: () {
                   //注意!
                  ref.read(todosProvider("second").notifier).addTodo(
                  Todo(
                    id: random.nextInt(100).toString(),
                    description: random.nextInt(100).toString(),
                    completed: false,
                  ),
                );
          },
          child: const Text("add"),
        ),
        for (final todo in todos)
          CheckboxListTile(
            value: todo.completed,
            onChanged: (value) =>
            //注意!
            ref.read(todosProvider("third").notifier).toggle(todo.id),
            title: Text(todo.description),
          )
      ],
    );
  }
}

あとがき

Flutter初心者の大学生の備忘録なので、生暖かい目で見守っていてください。何か間違った理解などがあれば、教えていただけると嬉しいです。

Discussion