【Flutter】riverpod_generatorで作るRiverpod Providers2系
これは何か
今更ですが、riverpod_generator を使った riverpod2 系の Provider の生成について整理します。各プロバイダの riverpod_generator を使った実装手順と、使ってみた所感を述べています。
各プロバイダのサンプルコードはリンクを貼っていきますので、そちらを参照してください。
話さないこと
- 各 Provider の役割について軽く触れますが、深くは言及しません
- 
generatorを使わない場合とのコード比較
- 
AsyncValueについて
- 
Hooksについて
環境
| バージョン | |
|---|---|
| Flutter | 3.10.0 | 
| Dart | 3.0.0 | 
| Xcode | |
| Android Studio | |
| flutter_riverpod | 2.3.6 | 
| riverpod_generator | 2.2.3 | 
| riverpod_annotation | 2.1.1 | 
| build_runner | 2.4.4 | 
riverpod、riverpod generator とは
Riverpod は Provider や Bloc、GetX と並ぶ状態管理パッケージです。その他のパッケージと同じように依存性を注入することによって、画面とロジックを切り離し、シンプルな実装を実現してくれます。依存性の注入だけでなく、他にも状態値のキャッシュや自動的な解放など様々な機能が装備されています。
Provider というクラスをグローバルな変数の様に定義することで、widget ツリーを考慮することなく、データの注入を行うことが可能です。
Riverpod Generator はこの Provider をシンプルなアノテーションと定義で自動生成を行ってくれるパッケージです。Generator を用いることで冗長になりがちだった Provider の定義をシンプルにすることができます。また型の安全性を高めたり、よく使われるメソッドも自動で生成してくれます。
Generator で生成するメリット・デメリット
メリット
- シンプルな記述
- 戻り値に応じて、自動で Provider を選定してくれる
- Family の引数の自由度が上がる
デメリット
- Provider の実装がブラックボックス化
メリットはなんと言っても記述のシンプルさです。素で riverpod を記述していた人はその恩恵を特に感じるでしょう。また地味に面倒だった Family の引数の自由度も上がります。
デメリットはやはりブラックボックス化です。直接 Provider を記述しない為、Provider の理解がないと裏でどの Provider が生成されているのか分からなくなります。
Riverpod generator を使った実装手順
- パッケージのインストール
- 定義ファイルにインポート
- アノテーションを記述
- build_runner でコード生成
1.パッケージのインストール
Riverpod を使うので当然ですが、flutter_riverpodをインストールします。
riverpod_generator を使う際には以下 3 つのパッケージをインストールします
pubspec.yaml で以下のとおり、追記します
dependencies:
  flutter:
    sdk: flutter
+ flutter_riverpod: ^2.3.6
+ riverpod_annotation: ^2.1.1
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
+ build_runner: ^2.4.4
+ riverpod_generator: ^2.2.3
2. 定義ファイルにインポート
次に provider を定義するファイルで使用するパッケージをインポートします
import 'package:riverpod_annotation/riverpod_annotation.dart';
ファイル単位でのインポートでは riverpod_annotation と後に生成されるファイルのインポートを記述しておく必要があります。
 import 'package:riverpod_annotation/riverpod_annotation.dart';
+ part '{ファイル名}.g.dart';
3. アノテーションを記述
provider の定義前にアノテーションを付けます
 import 'package:riverpod_annotation/riverpod_annotation.dart';
 part '{ファイル名}.g.dart';
+ 
// Provider定義
4. build_runner でコード生成
定義し終わったら、以下のコマンドをプロジェクトのルートで実行し、ファイルを生成します
flutter pub run build_runner --delete-conflicting-outputs
各種 Provider を Generator で作る
Provider
import 'package:riverpod_annotation/riverpod_annotation.dart';
part '{ファイル名}.g.dart';
{返り値の型} {メソッド名}({リファレンス名} ref) {
  return {返り値};
}
ルール
- 
riverpod_annotationと生成ファイルをインポート
- 
@riverpodのアノテーションを付ける
- part で指定するファイル名:ファイル名を間違えないように気をつけてください。異なると認識されません。
- メソッド名: 自由(記法はローワーキャメルケース)
- 
リファレンス名: メソッド名のアッパーキャメルケース+Refを指定します
- 返り値の型と返り値:任意で指定します
- 
生成されるプロバイダ名:ファイル名のローワーキャメルケース+Providerで生成されます
呼び出し
例えばsome_controller.dartというファイルで Provider を定義した場合、someControllerProviderという Provider が生成されます。
...
Widget build(BuildContext context, WidgetRef ref){
  final controller = ref.watch(someControllerProvider);
  return Widget();
}
...
Future Provider
import 'package:riverpod_annotation/riverpod_annotation.dart';
part '{ファイル名}.g.dart';
Future<{返り値の型}> {メソッド名}({リファレンス名} ref) async {
  return {返り値};
}
Provider の記述方法と比較して見て頂くと分かりますが、返り値が Future になっている事以外は同じです。
Stream Provider
import 'package:riverpod_annotation/riverpod_annotation.dart';
part '{ファイル名}.g.dart';
Stream<{返り値の型}> {メソッド名}({リファレンス名} ref) {
  return {返すStream};
}
こちらも FutureProvider と同様に返り値の型が Stream になっているだけで、それ以外の記述は Provider と同じです。
NotifierProvider
import 'package:riverpod_annotation/riverpod_annotation.dart';
part '{ファイル名}.g.dart';
class {クラス名} extends _${クラス名} {
  
  {状態値の型} build() => {初期値};
  // 自由にクラスメソッドを記述
  void someFunction(String value) {
        state = value;
   }
}
ルール
- 
riverpod_annotation、生成ファイルをインポート
- 
@riverpodのアノテーションを付ける
- クラス名:自由(記法はローワーキャメルケース)
- build メソッド:必ず記述します
- 
状態値:状態値とその初期値を定義します。状態値を持たない場合は返り値を void にします
- 
クラスメソッド:自由に記述してください。stateで状態値にアクセス出来ます
- 
生成されるプロバイダ名:ファイル名のローワーキャメルケース+Providerで生成されます。
呼び出し
例えばsome_controller.dartという名前でNotifierProviderを定義した場合、クラス名はSomeController、プロバイダ名はsomeControllerProviderとなります。
...
Widget build(BuildContext context, WidgetRef ref){
final controller = ref.watch(someControllerProvider);
 return Widget(
     onPressed: (){
         ref.read(someControllerProvider.notifier).someFunction();
     },
   );
 }
 ...
AsyncNotifier Provider
import 'package:riverpod_annotation/riverpod_annotation.dart';
part '{ファイル名}.g.dart';
class {クラス名} extends _${クラス名} {
FutureOr<{状態値の型}> build() async {
  // 何かしらの非同期処理
  return {初期値};
}
// 自由にクラスメソッドを記述
void someFunction(String value) {
      // AsyncLoading()でローディング状態にも出来る
      state = const AsyncLoading();
      // 状態値の更新はAsyncValue.data()で行う
      state = AsyncValue.data(value);
 }
}
ルール
NotifierProvider との違いは、返り値の型が FutureOr 型になっただけです。それ以外のルールは同じです。
その他ポイント
- 
状態値の型で指定した値は自動的にAsyncValueにラップされます。
- 
stateでアクセス出来る状態値がAsyncValueとなるので、クラスメソッド内で変更を加えるときはAsyncValue.data()などで状態値の更新を行います。
呼び出し
Widget build(BuildContext context, WidgetRef ref){
  final state = ref.watch(someAsyncControllerProvider);
  return state.when(
    data: (data) => Widget(
      data,
      onPressed:(){
        ref.read(someAsyncControllerProvider.notifier).someFunction();
      }
    ),
    loading: () => const CircularProgressIndicator(),
    error: (error, stackTrace) => Text('$error'),
  );
}
...
プロバイダから取得できる状態値はAsyncValueとなるので、.when()を使って、data,loading,error の場合で返す値を定義します。
Family, keepAlive
family
Provider に対して、外から引数を渡すことが出来る Family はシンプルに引数として定義するだけで generator が判別して FamilyProvider を生成してくれます。また素で書くよりも自由な引数を渡すことが出来ます。
以下例では Provider の引数に2つの引数を渡して、FamilyProvider を生成するように記述しています。素で書く FamilyProvider では渡せる引数は1つの為、2つ以上の値を渡したい場合、クラスにまとめる必要がありました。それに比べ非常に柔軟です。
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'some_controller.g.dart';
String someController(SomeControllerRef ref, int num1, int num2){
  return num1+num2;
}
上記以外でも、名前付引数にしたり、引数に初期値を付けることも可能です。
keepAlive
riverpod2 系では Provider はデフォルトで AutoDispose されるようになりました。riverpod1 系の逆ですね。その為、自動的に破棄されてほしくない場合は明示的に定義する必要があります。
定義方法は@riverpodアノテーションを@Riverpod(keepAlive:true)と書き換えるだけです。頭文字が大文字になっていることに注意してください。
 import 'package:riverpod_annotation/riverpod_annotation.dart';
 part 'some_controller.g.dart';
- 
+ (keepAlive: true)
 String someController(SomeControllerRef ref, int num1, int num2){
  return num1+num2;
}
記述イメージ
riverpod_generator を使った際の記述イメージは、
- Provider
- FutureProvider
- StreamProvider
はメソッド定義のような記述で、
- NotifierProvider
- AsyncNotifierProvider
はクラス定義のような記述です。
オマケ: Provider の選び方
どの Provider を使ったら良いのかはよく迷うポイントかと思います。ここからは自分の勝手な解釈も交えつつ、どういうポイントで選定するかについて軽く触れます。当たり前っちゃ当たり前な事書いてる可能性は大です(笑)
私の中では以下のような基準で選定しています。
| ユースケース | プロバイダ | 
|---|---|
| Stateless なクラスだけ注入したい | Provider | 
| 複数の Provider の値を加工して注入したい | Provider | 
| 非同期な状態値だけ注入したい | FutureProvider, StreamProvider | 
| 状態値の他にメソッドも注入したい + 状態値の初期値が同期的 | NotifierProvider | 
| 状態値の他にメソッドも注入したい + 状態値の初期値が非同期的 | AsyncNotifierProvider | 
NotifierProvider でも実は AsyncValue を状態値として返すことが可能です。しかし AsyncNotifierProvider を使えば、自動的に状態値が AsyncValue として返されるので、非同期な状態値を返すのであれば、素直に AsyncNotifierProvider を使うのが良いかと思います。
サンプルコード
サンプルコードは全て下記レポジトリよりご覧いただけます
最後に
素で Riverpod を書いた経験がないと riverpod_generator でなんの Provider が生成されているか分からないというデメリットがあると書きました。
しかし逆に素で書いていた経験に引っ張られてしまうことで混乱する事もあるように感じました。実際私は混乱しました。もしかしたら素で書いた経験がない方がスッと馴染める可能性もあります。
なので 初学者は躊躇せず最初から riverpod_generator に入門しても良いのかも! と思ったり、思わなかったりでした!!
お疲れ様でした!!




Discussion
細かいところなんですけど
ここが
違っていて、
正確には
です!
修正頂けると助かります🙇
確かに。人によってProviderって名前入れたり、入れなかったり自由にしますもんね。ご指摘ありがとうございます👍
修正しました
ありがとうございます!!
デフォルトだと自動生成の方がメソッド名+Providerの命名になるので
ほんとこの辺名前の付け方難しいなぁと思ってます😅
わかりみです。どこかでこの自動でメソッド名に付くProviderを外せる機構が欲しいってissue見た気がします😅
一応変更自体はできるようにはなってます!
おお!build.yamlから変更できるんですね!これは知らなかった、ありがたい🙏