Flutterの状態管理ライブラリのRiverpod。
いつも迷うので、再度入門してみたときの備忘録(*´ω`*)
プロバイダの一覧
プロバイダの種類 | 生成されるステートの型 | 具体例 |
---|---|---|
StateNotifierProvider | StateNotifierのサブクラス | イミュータブルで複雑なステートオブジェクト |
Provider | 任意 | サービスクラス / 算出プロパティ(リストのフィルタなど) |
FutureProvider | 任意のFuture | API の呼び出し結果 |
StreamProvider | 任意のStream | API の呼び出し結果のStream |
StateProvider | 任意 | フィルタの条件 / シンプルなステートオブジェクト |
ChangeNotifierProvider | ChangeNotifier のサブクラス | ミュータブルで複雑なステートオブジェクト |
ChangeNotifierProviderは非推奨、StateProviderは限定的なので、
ほかの4つを理解できればよさそう。
Code generation
riverpod_generator
を使うと@riverpod
で自動生成してくれる。
flutter pub add riverpod_annotation flutter pub add dev:riverpod_generator
大きくClass-BasedとFunctionalの2つの書き方がある。
- Class-Based: 外部から状態を変更できる(mutate methods)
- Functional: 外部から状態を変更できない(値の取得/加工のみ)
// Sync Class-Based @riverpod class Example extends _$Example { @override String build() { return 'foo'; } // Add methods to mutate the state }
// Sync Functional @riverpod String example(ExampleRef ref) { return 'foo'; }
Functionalは、Class-Basedの糖衣構文らしい。
StateNotifierProvider
ユーザ操作などにより変化するステートを管理する方法として Riverpodが推奨するもの。
一番よく使うやつ。
@riverpod class Todos extends _$Todos { @override List<Todo[]> build() { return []; } // Todo の追加 void addTodo(Todo todo) { state = [...state, todo]; } }
.family
family
という機能を使って、パラメタを渡すこともできる。
パラメタごとに別のProviderが作られる。
@riverpod class Todos extends _$Todos { @override List<Todo[]> build({required String categoryId}) { return []; } // ... }
final state = ref.watch(todosProvider(categoryId: "..."));
同じパラメタかどうかは、==
で比較されるため、
Listやカスタムクラスなどの場合は注意が必要。
// NG ref.watch(activityProvider(['recreational', 'cooking'])); // OK ref.watch(activityProvider(const ['recreational', 'cooking']));
Provider
最も基本的なプロバイダであり、値を同期的に生成する。
主な用途は、
例えば、StateNotifier
なtodosProvider
から、
特定の値を絞り込んで取得するときなど。
final completedTodosProvider = Provider<List<Todo>>((ref) { // todosProvider から Todo リストの内容をすべて取得 final todos = ref.watch(todosProvider); // 完了タスクのみをリストにして値として返す return todos.where((todo) => todo.isCompleted).toList(); });
FutureProvider
非同期操作が可能なProvider
。ステートしかを持たない。
主な用途は、
- 非同期操作を実行し、その結果をキャッシュ(例えばネットワークリクエストなど)
- 非同期操作の error/loading ステートを適切に処理
- 非同期的に取得した複数の値を組み合わせて一つの値に統合
なお、FutureProvider
には値を計算する処理を直接変更する手段がないため、
そういった場合は、StateNotifierProvider
を利用する。
設定値を取得する例が載っていて、
@riverpod Future<Configuration> fetchConfiguration(FetchConfigurationRef ref) async { final content = json.decode( await rootBundle.loadString('assets/configurations.json'), ) as Map<String, Object?>; return Configuration.fromJson(content); }
watch
の返り値がAsyncValue
なので、
UI側では、それによって読み込み中/取得成功/取得失敗の表示を切り分けることができる。
class MyConfiguration extends HookConsumerWidget { const MyConfiguration({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final config = ref.watch(fetchConfigurationProvider); return Scaffold( body: switch (config) { AsyncError(:final error) => Center(child: Text('Error: $error')), AsyncData(:final value) => Center(child: Text(value.host)), _ => const Center(child: CircularProgressIndicator()), }, ); } }
StreamProvider
FutureProvider
のStream
版。
主な用途は、
- Firebase(Firestoreのsnapshot)やWebSocketの監視
- 一定時間ごとに別のプロバイダを更新
StreamBuilder
もあるがStreamProvider
には以下の利点がある。
- 他のプロバイダから
ref.watch
でストリームを監視できる AsyncValue
によりloading/errorを適切に処理できる- 通常のストリームとブロードキャスト(broadcast)ストリームを区別する必要がない
- ストリームから出力された直近の値をキャッシュする(途中で監視を開始しても最新の値を取得することができる)
StreamProvider
をオーバーライドすることでテスト時のストリームを簡単にモックにできる
StateProvider
外部から変更が可能なステート(状態)を公開するプロバイダ。
StateNotifierProvider
の簡易版。
UI側で利用されるシンプルなステートを管理するのにうってつけらしい。
列挙型(enum)/文字列型/bool型/数値型など。
逆に以下の場合は、StateNotifierProvider
が必要。
- ステートの算出に何かしらのバリデーション(検証)ロジックが必要
- ステート自体が複雑なオブジェクトである(カスタムのクラスや List/Map など)
- ステートを変更するためのロジックが単純な count++ よりは高度である必要がある
なので、
- クラス/List/Mapではない単一の型のステートを
- 直接代入するだけ
に使うのがよいかんじっぽい。
プロバイダの利用/組み合わせ
ref.watch
を使って、プロバイダから別のプロバイダの値を監視できる。
@riverpod class Example extends _$Example { @override int build() { // "Ref" can be used here to read other providers final otherValue = ref.watch(otherProvider); return 0; } }
ref.xxx
ref.watch
ステート(状態)の監視。よく使うやつ
const exampleProvider = StateNotifierProvider<>(...); final state = ref.watch(exampleProvider); const streamProvider = StreamProvider<T>(...); // なにも付けないと、AsyncValue AsyncValue<T> state = ref.watch(streamProvider); // .futureをつけると、Futureで受け取れる Future<T> state = ref.watch(streamProvider.future); T state = await ref.watch(streamProvider.future); // 必要な項目の更新だけを取得したい場合は、selectAsyncをつかう Future<String> foo = ref.watch(streamProvider.selectAsync((data) => data.foo)); String foo = awiat ref.watch(streamProvider.selectAsync((data) => data.foo));
ref.read
プロバイダの取得。監視はしない。
StateNotifierProvider
のmethodを呼び出すときに使う。
final methods = ref.read(exampleProvider.notifier);
ref.listen/ref.listenSelf
変更の検知/監視
// exampleProviderの監視 ref.listen(exampleProvider, (previous, next) { print('Changed from: $previous, next: $next'); }); // 自身(Provider)の監視 ref.listenSelf( (previous, next) { print('Changed from: $previous, next: $next'); });
ref.onDispose
終了時の処理。WebSocketのクローズなどで利用。
@riverpod Stream<int> example(ExampleRef ref) { final controller = StreamController<int>(); // When the state is destroyed, we close the StreamController. ref.onDispose(controller.close); // TO-DO: Push some values in the StreamController return controller.stream; }
ライフサイクル(life-cycle)に紐づいていて、
ref.onCancel
やref.onResume
、
onAddListener
、onRemoveListener
もある。
ref.invalidate/ref.invalidateSelf
プロバイダの破棄や強制的な更新
ref.invalidate(someProvider); ref.invalidateSelf();
以上!!理解してそうで、理解できないので、何度も見直さないと。。(*´ω`*)