くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Riverpodの各Providerに再入門する(3度目)

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

最も基本的なプロバイダであり、値を同期的に生成する。

主な用途は、

  • 計算結果をキャッシュ
  • 他のプロバイダに値への公開(RepositoryHttpClientインスタンスなど)
  • selectを使わずにプロバイダやウィジェットの更新の条件を限定

例えば、StateNotifiertodosProviderから、
特定の値を絞り込んで取得するときなど。

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

FutureProviderStream版。

主な用途は、

  • 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.onCancelref.onResume
onAddListeneronRemoveListenerもある。

ref.invalidate/ref.invalidateSelf

プロバイダの破棄や強制的な更新

ref.invalidate(someProvider);

ref.invalidateSelf();

以上!!理解してそうで、理解できないので、何度も見直さないと。。(*´ω`*)