🧖

【Flutter】Riverpodクラスの使い分け(Riverpod2.0以降)

2023/06/11に公開

さて、久々にととのっていきましょう🧖

実務で長らくStateNotifierを使ってきましたが、Riverpod2.0でNotifierとAsyncNotifierが登場しました。
これにより、おそらくStateNotifierはレガシーになったようです。(え〜)
さらに、Riverpod2.3以降はStreamNotifierも追加されています。(え〜)

新規プロジェクトでの開発などに向け、Riverpodについてもう一度整理しようと思ってのこの記事です。

Riverpodクラスの使い分け

はじめにState管理クラスについて大雑把に整理しておくと、それぞれの主なユースケースとRiverpod2.0以降の評価は以下のようになります。

No クラス ユースケース 評価
1 Provider ステートが変わらないRipositoryクラスなどのインスタンス取得に使う(参考 使える
2 StateProvider プリミティブな変数(String、enumなど)などの取得、更新に使う(参考 使えるけどレガシー
3 StateNotifier ステート(immutable)が変わるViewModelクラスなどのインスタンス取得、更新に使う(参考 使えるけどレガシー
4 FutureProvider API通信などの非同期処理の結果取得に使う(参考 使える
5 StreamProvider リアルタイムAPI通信などの非同期処理の結果取得に使う(参考 使える
6 ChangeNotifier ステート(mutable)が変わるViewModelクラスなどのインスタンス取得、更新に使う 基本使わない(非推奨
7 Notifier
(Riverpod2.0で登場!)
No.3のユースケースとほぼ同じ(参考 使える
8 AsyncNotifier
(Riverpod2.0で登場!)
No.4のユースケースに加えて、ステート更新などのメソッドも加えたい場合に使う(参考 使える
9 StreamNotifier
(Riverpod2.3で登場!)
No.5のユースケースに加えて、ステート更新などのメソッドも加えたい場合に使う(参考 使える

このうち今回は、使いどころに迷いそうな、「使えるけどレガシー」になったStateProviderとStateNotifierについて踏み込んでいきます。

「使えるけどレガシー」なクラスの置き換え

結論から言うと、

  • StateProvider => Notifier
  • StateNotifier => Notifierまたは(非同期処理を含むなら)AsyncNotifier

に置き換えるのがクールですね、ということなのですが、そうなる流れを具体的なケースでご紹介します。

StateProvider => Notifierの置き換えについて

数をカウントするクラスをStateProviderで実装します。

final countProvider = StateProvider<int>((ref) {
  return 0;
});

これをNotifierを使って、

final countNotifierProvider = NotifierProvider<CountNotifier, int>(CountNotifier.new);

class CountNotifier extends Notifier<int> {
  
  int build() {
    return 0;
  }
}

のように置き換えられます。

あれ、StateProviderより何だか冗長になっている!?

はい、そうなんです。StateProviderより冗長になってます。(え〜)
ここではまだStateProviderをNotifierに置き換える良さがわかりませんが、このあとのStateNotifier => Notifierの置き換えも踏まえることでその良さがわかるかと思います。

StateNotifier => Notifierの置き換えについて

より柔軟にステートを更新するためにStateProviderでは物足りないことがあります。
そこで、これまではStateNotifierを使ってきました。

数をカウントするクラスをStateNotifierで実装すると以下のようになります。
ステートを更新するincrementメソッドを追加しています。

final countNotifierProvider = StateNotifierProvider<CountNotifier, int>((ref) {
  return CountNotifier();
});

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

  void increment() {
    state++;
  }
}

これをNotifierを使って、

final countNotifierProvider = NotifierProvider<CountNotifier, int>(CountNotifier.new);

class CountNotifier extends Notifier<int> {
  
  int build() {
    return 0;
  }

  void increment() {
    state++;
  }
}

のように置き換えられます。

StateProviderもStateNotifierもシンプルにNotiferに一本化できることがわかりました。

とはいえ、これまで通りStateNotifierに一本化することもできます。
どうしてStateNotifierではなくNotifierが良いのでしょう?

今後はおそらくNotifier(やAsyncNotifier)が優先的にメンテナンスされていくため、というのが大きな理由ですが、その他にも以下のようなメリットが挙げられます。

  1. refがNotifier(やAsyncNotifier)の親クラスで実装されているため、refの受け渡しが必要ない
  2. Notifier(やAsyncNotifier)のbuildメソッドが優れており、コンストラクタが必要ない
    • buildメソッド内でRef.watchまたはRef.listenで外部のステートを監視している場合、そのステートに変更があればbuildメソッドが再実行される
    • buildメソッドが再実行される間、Notifer(例でいうCountNotifier)のインスタンスが作り直されるわけではなく、元のインスタンスの状態が保たれる
  3. Notifier(やAsyncNotifier)のProviderの定義が簡単になる上、Riverpod Generatorを使うことでProviderの自動生成もできる(詳細は割愛)

StateNotifier => AsyncNotifierの置き換えについて

最後にAsyncNotifierについて見ていきましょう。
StateNotifierで非同期処理を扱う場合、以下のようにこれまではAsyncValueを使っていましたが、冗長さがありました。

final countAsyncNotifierProvider = StateNotifierProvider<CountAsyncNotifier, AsyncValue<int>>((ref) {
  return CountAsyncNotifier(ref.read);
});

class CountAsyncNotifier extends StateNotifier<AsyncValue<int>> {
  CountAsyncNotifier(this._reader) : super(const AsyncLoading()) {
    // API通信などの非同期処理で初期化
  }

  final Reader _reader;

  Future<void> increment() async {
    final countRepository = _reader(countRepositoryProvider);
    state = const AsyncLoading();
    state = await AsyncValue.guard(countRepository.increment);
  }
}

これをNotifierを使って、

final countAsyncNotifierProvider = AsyncNotifierProvider<CountAsyncNotifier, int>(CountAsyncNotifier.new);

class CountAsyncNotifier extends AsyncNotifier<int> {
  
  FutureOr<int> build() {
    // API通信などの非同期処理で初期化
  }

  Future<void> increment() async {
    final countRepository = ref.read(countRepositoryProvider);
    state = const AsyncLoading();
    state = await AsyncValue.guard(countRepository.increment);
  }
}

のように置き換えられます。

refの受け渡しやコンストラクタがなくなり、スッキリしましたね。

まとめ

Riverpod2.0以降は以下のように置き換えるとととのうでしょう🧖

  • StateProvider => Notifier
  • StateNotifier => Notifierまたは(非同期処理を含むなら)AsyncNotifier

Discussion