📙

StateNotifier, StateProvider, FutureProvider, StreamProviderの使い分け

2022/11/10に公開

公式ドキュメントの説明

https://riverpod.dev/ja/

公式ドキュメントでそれぞれの説明がされているので、まずはそちらを一部抜粋したものを覗いてみましょう。

StateNotifierProvider

StateNotifierProvider は StateNotifier(Riverpod が依存する state_notifier パッケージのクラス)を監視し、公開するためのプロバイダです。 この StateNotifierProvider および StateNotifier は、ユーザ操作などにより変化するステート(状態)を管理するソリューションとして Riverpod が推奨するものです。

StateProvider

StateProvider は外部から変更が可能なステート(状態)を公開するプロバイダです。 StateNotifierProvider の簡易版であり、ステートの管理にわざわざ StateNotifier クラスを定義するほどではない場合にご利用いただけます。

FutureProvider

FutureProvider は非同期操作が可能な Provider であると言えます。
一般的には次のような用途で使われます。
 ・ 非同期操作を実行し、その結果をキャッシュするため(例えばネットワークリクエストなど)。
 ・ 非同期操作の error/loading ステートを適切に処理するため。
 ・ 非同期的に取得した複数の値を組み合わせて一つの値にするため。

StreamProvider

StreamProvider は FutureProvider の Stream 版です。
一般的には次のような用途で使われます。
 ・ Firebase や WebSocket の監視するため。
 ・ 一定時間ごとに別のプロバイダを更新するため。

どう使い分けるか

StateNotifierProviderとStateProvider, FutureProvider, StreamProviderの関係性は、「できること」という観点で見ればこのような形になっています。

各々が完全に分けられているというより、全てのユースケースに対応できるStateNotifierProviderがまず存在していて、他3つは各々の狭いユースケースに特化して作られているといった形です。

テーブルでより詳細に表現するとこんな感じです。

複雑な処理を伴う状態更新 簡単な状態更新 Future Stream
StateNotifierProvider
StateProvider × × ×
FutureProvider × × ×
StreamProvider × × ×

ここで使われている「状態更新」はユーザーによる入力といった外部からの更新のことを指していて、ref.refresh()による再検証や内部のStreamによる更新は含みません。

このテーブルをもとにProviderの選定をフローチャートにするとこのようになります。

この分岐の理由や詳細については、これからStateNotifierProviderとそれぞれのProviderの違いについて紹介しながら説明します。

Future/StreamProviderとStateNotifierProviderの違い

Future/StreamProviderは、そのクラスで扱いたい非同期で取得する状態をAsyncValueでラップして返してくれるProviderです。

FutureProviderとStreamProviderの違いは、
その非同期処理がFutureであればFutureProviderを使い、StreamであればStreamProviderを使うというシンプルなものです。

FutureProviderとStreamProviderではユーザー操作によって状態を更新することはできませんが、StateProviderとStateNotifierProviderではユーザー操作による状態の更新が可能です。

ただ、仮にユーザー操作による状態更新がなくても、FutureProviderとStreamProviderで作りたいものをStateNotifierProviderで実装することは可能です。

実際にそれぞれのコードを見てみましょう。

  • FutureProvider
final futureProvider = FutureProvider.autoDispose(
  (ref) async {
    await Future<void>.delayed(
      const Duration(milliseconds: 400),
    );
    return 'Future completed';
  },
);
  • FutureProviderをStateNotifierProviderで再現
final stateProvider =
    StateNotifierProvider.autoDispose<SampleStateNotifier, AsyncValue<String>>(
  (ref) => SampleStateNotifier(),
);

class SampleStateNotifier extends StateNotifier<AsyncValue<String>> {
  SampleStateNotifier() : super(const AsyncValue.loading()) {
    Future(
      () async {
        await Future<void>.delayed(
          const Duration(milliseconds: 400),
        );
        state = const AsyncValue.data('Future Completed');
      },
    );
  }
}
  • StreamProvider
final streamProvider = StreamProvider.autoDispose(
  (ref) {
    return Stream<String>.periodic(
      const Duration(seconds: 1),
      (count) {
        return count.toString();
      },
    );
  },
);
  • StreamProviderをStateNotifierProviderで再現
final stateProvider =
    StateNotifierProvider.autoDispose<SampleStateNotifier, AsyncValue<String>>(
  SampleStateNotifier.new,
);

class SampleStateNotifier extends StateNotifier<AsyncValue<String>> {
  SampleStateNotifier(Ref ref) : super(const AsyncValue.loading()) {
    final subscription = Stream<String>.periodic(
      const Duration(seconds: 1),
      (count) {
        return count.toString();
      },
    ).listen((event) {
      state = AsyncValue.data(event);
    });
    ref.onDispose(
      subscription.cancel,
    );
  }
}

見ての通り、FutureProviderとStreamProviderの方がだいぶ少ない記述量で済みます。

このように、FutureProviderやStateProviderで書ける箇所をStateNotifierProviderで書くと

  1. 必要以上のボイラープレートコードを書かなければいけない。
  2. 実態より制約が緩くなるので、コードを読む時に責務を把握しづらくなる。

という難点があるので、簡単な処理はそれに合わせて作られたProviderを臨機応変に使った方が楽に管理できます。これは後に説明する、適所でStateProviderを使う理由にも当てはまります。

StateProviderとStateNotifierProviderの違い

StateProviderとStateNotifierProviderはどちらも状態の更新を行いますが、複雑な処理を行う場合はStateNotifierProviderを使います。

公式ドキュメントは、StateProviderに適したシンプルな状態として、以下を挙げています。

列挙型(enum)、例えばフィルタの種類など
文字列型、例えばテキストフィールドの入力内容など
bool 型、例えばチェックボックスの値など
数値型、例えばページネーションのページ数やフォームの年齢など

そして、使うべきでないケースとして

ステートの算出に何かしらのバリデーション(検証)ロジックが必要
ステート自体が複雑なオブジェクトである(カスタムのクラスや List/Map など)
ステートを変更するためのロジックが単純な count++ よりは高度である必要がある

を挙げています。正直これだけで十分な説明のように見えます。

状態の更新の直前や直後にFirebaseとの通信といった、状態の更新に直接的に関係のない処理を行いたい場合はStateProviderでは実装できません。

逆に状態の更新のみを行う限りはStateProviderでの実装は可能ですが、状態の更新パターンが多くなってきたりすると、StateNotifierProviderで自分で定義した関数を使った状態を更新した方が楽になります。

公式では使うべきでないケースとしてカスタムクラスが挙げられていますが、上で挙げられている文字列やbool型といったシンプルな状態をもつクラスを単純に更新するだけの場合は、StateProviderを使ってもそこまで問題ないと個人的には考えています。

Future・StreamProviderと同じように、StateProviderもStateNotifierProviderによって再現できるので、書き方にどういった違いがあるのか見てみましょう。

  • StateProvider
final stateProvider = StateProvider.autoDispose<int>((ref) => 0);
  • StateNotifierProvider
final stateProvider =
    StateNotifierProvider.autoDispose<SampleStateNotifier, int>(
  (ref) => SampleStateNotifier(),
);

class SampleStateNotifier extends StateNotifier<int> {
  SampleStateNotifier() : super(0);

  void increment() {
    state++;
  }
}

StateProviderは殆ど書くことがないので、適所で使えればだいぶ時間をセーブできます。

StateNotifierProviderの行方

Riverpodができる前から活躍していたStateNotifierでしたが、Riverpodのアップデートと共に徐々に使用する幅は狭くなっていっています。

少し前のツイートですが、Riverpod作者のRemiさんも「ほとんどのケースではStateNotifierは必要無い」と明言しています。

また、ここでは紹介してはいませんがAsyncNotifierNofifier
を使えばStateNotifierは完全にリプレイスできます。

AsyncNotifierとNotifierについてはまだドキュメントで出揃っていないのと(issue)、自分もあまり触ったことがなかったのが理由で説明を省きましたが、詳細が気になる方はAndreaさんの記事でわかりやすく説明されているので気になる方は目を通してみてください。

終わりに

サンプルコードを詳しく見たい方はこちらをご覧ください。
https://github.com/santa112358/riverpod_providers_usage

この記事が少しでも勉強の助けになれていれば嬉しいです。記事の内容について訂正すべき箇所や質問があれば遠慮なくお声がけください。

Discussion