💫

【Riverpod学習】Providerの種類〜(Async)NotifierProviderについて〜

2024/10/13に公開

(Async)NotifierProvider

以下の公式を見ながら学習したのでその備忘録です
https://riverpod.dev/ja/docs/providers/notifier_provider

NotifierProviderとAsyncNotifierProviderは、Riverpodで状態管理を行うための仕組みです。この2つは、それぞれ同期的・非同期的にデータを扱うために使います。

StateNotifierProviderというプロバイダも存在しますが、現在はStateNotifierProviderよりもNotifierProviderを使うことが推奨されています。NotifierProviderの基本的な動作はStateNotifierProviderとほぼ同じですが、より簡単で新しいAPIです。

NotifierProviderとは?

NotifierProviderは、状態管理を行うクラス(Notifier)を他の場所で使えるようにするプロバイダです。Notifierは、アプリの状態(例: やることリスト)を持ち、その状態を変更するためのメソッド(関数)を含みます。

例: やることリスト(Todoリスト)の管理
まず、Todoという「やること」を表すクラスを定義します。


class Todo with _$Todo {
  factory Todo({
    required String id,           // やることのID
    required String description,  // やることの内容
    required bool completed,      // 完了したかどうかのフラグ
  }) = _Todo;
}

次に、NotifierProviderを使って、このTodoリストを管理します。NotifierProviderを使うことで、他の場所(UIなど)から簡単にリストにアクセスし、データを操作することができます。

Notifierを使った状態管理

@riverpodアノテーションを付けたクラスが、プロバイダの元となるNotifierクラスになります。
また、@riverpodアノテーションがあることで、Riverpodは自動的にNotifierProviderを生成し、それをtodosProviderという名前でUI側で使えるようにします。
todosProviderはNotifierProvider<Todos, List<Todo>>に相当します。データ型List<Todo>になっている理由はbuildメソッドでList<Todo>で状態の初期値を設定しているためです。
そのためTodosクラスの内部では、stateは常にList<Todo>として扱われています。addTodoやremoveTodo、toggleメソッドの中でも、stateに対してList<Todo>として操作が行われています。


class Todos extends _$Todos {
  // 初期状態として空のTodoリストを返す
  
  List<Todo> build() {
    return [];
  }

  // Todoリストに新しいやることを追加するメソッド
  void addTodo(Todo todo) {
    // 新しいTodoをリストに追加するために、既存のリストをコピーして新しいTodoを追加
    state = [...state, todo];
  }

  // 特定のIDを持つやることを削除するメソッド
  void removeTodo(String todoId) {
    // 指定されたID以外のTodoを残す
    state = state.where((todo) => todo.id != todoId).toList();
  }

  // 特定のやることの完了状態を切り替えるメソッド
  void toggle(String todoId) {
    state = state.map((todo) {
      // IDが一致するTodoの完了状態を反転させる
      return todo.id == todoId ? todo.copyWith(completed: !todo.completed) : todo;
    }).toList();
  }
}

UIから状態を使う

これで、NotifierProviderを使ってTodosクラスを管理できるようになりました。次は、UIでこの状態を使って、やることリストを表示したり、操作したりします。

class TodoListView extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // todosProviderからTodoリストを取得
    List<Todo> todos = ref.watch(todosProvider);

    // Todoリストを画面に表示
    return ListView(
      children: [
        for (final todo in todos)
          CheckboxListTile(
            value: todo.completed,
            // チェックボックスをタップしたら完了状態を切り替える
            onChanged: (value) => ref.read(todosProvider.notifier).toggle(todo.id),
            title: Text(todo.description),
          ),
      ],
    );
  }
}

これで、ユーザーがチェックボックスをタップすることで、やることの完了状態を切り替えることができます。

AsyncNotifierProviderとは?

AsyncNotifierProviderは、NotifierProviderと似ていますが、非同期のデータ(例: インターネットから取得するデータ) を扱うときに使います。例えば、リモートサーバーからTodoリストを取得して表示したい場合に便利です(インターネット経由で外部からデータを取得する場合)。

例: リモートからやることリストを取得する

まず、Todoクラスにリモートデータからの読み込みを追加します。


class Todo with _$Todo {
  factory Todo({
    required String id,
    required String description,
    required bool completed,
  }) = _Todo;

  // 外部のサーバーからデータを取得するためのメソッド
  factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
}

次に、AsyncNotifierProviderを使って、リモートサーバーからやることリストを取得し、管理します。

AsyncNotifierを使った非同期処理


class AsyncTodos extends _$AsyncTodos {
  // リモートからTodoリストを取得する非同期関数
  Future<List<Todo>> _fetchTodo() async {
    final response = await http.get('api/todos');
    final todos = jsonDecode(response.body) as List<Map<String, dynamic>>;
    return todos.map(Todo.fromJson).toList();
  }

  
  FutureOr<List<Todo>> build() async {
    // 初回にリモートからTodoリストを取得
    return _fetchTodo();
  }

  // 新しいTodoを追加してリストを更新する
  Future<void> addTodo(Todo todo) async {
    state = const AsyncValue.loading(); // 状態をローディング中に設定
    state = await AsyncValue.guard(() async {
      await http.post('api/todos', todo.toJson());
      return _fetchTodo(); // 更新後にリストを再取得
    });
  }

  // Todoの完了状態を切り替える
  Future<void> toggle(String todoId) async {
    state = const AsyncValue.loading(); // 状態をローディング中に設定
    state = await AsyncValue.guard(() async {
      await http.patch('api/todos/$todoId', {'completed': true});
      return _fetchTodo(); // 更新後にリストを再取得
    });
  }
}

非同期データのUIでの利用

class TodoListView extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    // asyncTodosProviderから非同期データを取得
    final asyncTodos = ref.watch(asyncTodosProvider);

    // 取得状況に応じてUIを変更
    return asyncTodos.when(
      data: (todos) => ListView(
        children: [
          for (final todo in todos)
            CheckboxListTile(
              value: todo.completed,
              onChanged: (value) => ref.read(asyncTodosProvider.notifier).toggle(todo.id),
              title: Text(todo.description),
            ),
        ],
      ),
      loading: () => CircularProgressIndicator(), // データを取得中の場合
      error: (error, stack) => Text('Error: $error'), // エラーが発生した場合
    );
  }
}

ref.read(asyncTodosProvider)とref.read(asyncTodosProvider.notifier)の違い

ref.read(asyncTodosProvider)は、AsyncTodosプロバイダの状態(データそのもの)を取得するために使います。この場合、返ってくるのはAsyncValue<List<Todo>>です。状態そのもの(例えばTodoリスト全体) を取得したいときに使います。

final asyncTodos = ref.read(asyncTodosProvider);

asyncTodosの中には、以下のような状態が含まれます。

  • loading: データがまだ取得されていないとき。
  • data: データが正常に取得されたとき(List<Todo>など)。
  • error: エラーが発生したとき。

ref.read(asyncTodosProvider.notifier)は、AsyncTodosのロジック部分(AsyncNotifierのインスタンス) にアクセスするために使います。この場合、返ってくるのはAsyncTodosクラスのインスタンスで、状態を操作するためのメソッド(addTodoやtoggleなど)を呼び出すことができます。

ref.read(asyncTodosProvider.notifier).toggle(todo.id);

ここでnotifierにアクセスすることで、AsyncTodosクラスの中に定義されたtoggleメソッドを使って、Todoの完了状態を切り替えることができます。

ref.read(asyncTodosProvider)だけでは、現在の状態(データや非同期処理の進行状況) しか取得できず、状態を変更するメソッド(toggleなど)にはアクセスできません。状態の変更を行いたい場合は、notifierにアクセスする 必要があります。notifierはAsyncNotifierクラスそのものにアクセスするための方法です。

  • ref.read(asyncTodosProvider): 現在の状態(例えばAsyncValue<List<Todo>>)を取得します。データやロード状態、エラー状態などを確認したいときに使います。
  • ref.read(asyncTodosProvider.notifier): 状態を操作するためのメソッド(toggleやaddTodoなど)にアクセスします。状態を変更するためには、notifierにアクセスする必要があります。

まとめ

NotifierProvider は、同期的な状態管理に使用し、アプリ内のビジネスロジックを一元化するのに便利です。
AsyncNotifierProvider は、非同期でデータを扱うときに使用し、リモートデータの取得や更新に役立ちます。

Discussion