Riverpod にはいろいろな種類の Provider
が用意されています。
2023.8.8 時点(バージョンは 2.3.6)では、以下の Provider
がドキュメントの All Providers のメニューに列挙されています。
- Provider
- (Async)NotifierProvider
- StateNotifierProvider
- FutureProvider
- StreamProvider
- ChangeNotifierProvider
ちょっとひとつずつドキュメントを開いて違いを比較するには多く、また気づいたら非推奨になっているものもありますので、この記事でいったん現時点での Provider の使い分けをまとめてみたいと思います。
Code generation
Riverpod の Provider の使い分けを考える上で、 code generation の話題は避けて通れません。
Provider の種類が多く初学者にとって使い分けが難しい問題は作者の Remi さんも認識しており、それを「Provider の種類を考えずとも最適なものを生成する」というアプローチで解決しようとしているのが code generation だからです。
その考えを反映してか、現在 Remi さんが整備中の新しいドキュメントでは、 All Providers
の項目が左メニューの 下から2番目 まで後回しにされています。
Provider の種類は、 ProviderContainer
や ProviderElement
などのように「内部的に使われているクラス」という扱いにそのうちなるのかもしれません。
関数とクラス
code generation の具体的な使い方は割愛しますが、大きくわけて 2 つの種類があるとドキュメントでは整理されています。それが Functional と Class-Based です。
Providers can be defined either as an annotated function or as an annotated class
これは、State を生成・更新するロジックが「関数ひとつで完結する」ものなのか、「クラス(から生成されたオブジェクト)を必要する」ものなのか、という違いです。
たとえば他の Provider の値を .watch()
し、そこに何かしらのロジックを施して得られる値でよければ Functional が適切ですし、値の更新にメソッドや private なフィールドなどを必要とする場合は Class-Based となります。
たとえば、カウントを管理する countProvider
を .watch
して何か値を保持する Provider の例を Functional と Class-Based で書いてみます。
@riverpod
int multiCount(MultiCountRef ref) {
final count = ref.watch(countProvider);
return count * 2;
}
@riverpod
class MultiCount with _$MultiCount {
// private なフィールド
var _multiplier = 2;
@override
int build() {
final count = ref.watch(countProvider);
return count * _multiplier;
}
// 外部に提供するメソッド
void multiplyWith(int value) {
_multiplier = value;
state = state * _multiplier;
}
}
code generation において、 Functional な方法で作られる Provider が Provider
とその派生であり、 Class-Based に生成される Provider は NotifierProvider
とその派生となります。
「その派生」というのは、値の生成に Future な処理を必要とする FutureProvider
と AsyncNotifierProvider
、そして値の生成に Stream 処理を必要とする StreamProvider
と StreamNotifierProvider
です。
詳しい分類表は公式ドキュメントを参照してください。
その他の Provider
以上です。
まだたくさんあるじゃないかと思われるかもしれませんが、積極的な利用を推奨されているものは以上の code generation がサポートする 6 種類の Provider のみです。その他はすべて「非推奨」となっています。理由としては、「パッケージ利用者に混乱を与えるから」ということのようです。
StateNotifierProvider
ドキュメントを見ると、以下の注釈が書かれています。
Prefer using NotifierProvider instead.
細かな理由は書かれていませんが、おそらく用途が NotifierProvider
と似ていること、また利用する際
final someProvider = StateNotifierProvider<SomeNotifier, SomeState>((ref) {
// ①
return SomeNotifier();
});
class SomeNotifier extends StateNotifier<SomeState> {
SomeNotifier() : super(SomeState());
// ②
}
のような形でコーディングすることになり、
-
ref.watch
などの処理を ① と ② のどこにどう書けば良いのか -
ref
はどう扱えば良いのか、SomeNotifier
に渡して良いのか
など考えさせられる部分が多く、「すべてをクラス内に書けば良い」 NotifierProvider
の方が混乱が少ないためと推測しています。
ChangeNotifierProvider
こちらも同様に NotifierProvider
が推奨されています。
Prefer using NotifierProvider instead.
Consider using ChangeNotifierProvider only if you are absolutely certain that you want mutable state.
もし State をどうしても mutable(可変)にしたい場合に、 notifiyListeners
を呼び出し忘れないことをコーディング側が担保しながら利用する形になります。
StateProvider
こちらはドキュメントに記載はないものの、 Riverpod の Discord サーバーなどで頻繁に非推奨である旨が発言されています。
おそらく code generation の Functional と Class-Based のどちらの整理にも含まれない中途半端なものであるためと思われます。
基本的に (Provider
ではなく) StateProvider
を使いたい場合というのは、state を外部から変更したい場合になると思いますので、この場合は NotifierProvider
(とその派生)を使う形になるかと思います。
まとめ
以上、走り書きで Riverpod の Provider を整理してみました。
ドキュメント上では整理しきれていませんが、今後 Dart に対する static metaprogramming の導入によって code generation による記法のみがサポートされるようになり、従来の書き方は廃止されることも発言されていますので、なるべく早い段階で code generation が対応している 6 つの Provider への乗り換えと、できる限り code generation を使った記法へ移行していく必要がありそうです。
余談
公式ドキュメントの日本語訳ページは、 NotifierProvider
に関する情報や、StateNotifierProvider
などに対する NotifierProvider
を推奨する文言などの追記が追いついていない状況です。(翻訳はどうしても有志の方の協力が必要なため、仕方ない部分と思います)
日本語訳があるからと日本語訳ページのみを読んでしまうとクリティカルな部分で「古い情報」が残ってしまっているリスクがありますので、必ず原文である英語を一読することをお勧めします。