【Riverpod学習】Providerの種類〜(Async)NotifierProviderについて〜
(Async)NotifierProvider
以下の公式を見ながら学習したのでその備忘録です
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