👻

【Flutter】riverpod_generatorで作るRiverpod Providers2系

2023/05/21に公開
7

これは何か

今更ですが、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 とは

RiverpodProviderBlocGetX と並ぶ状態管理パッケージです。その他のパッケージと同じように依存性を注入することによって、画面とロジックを切り離し、シンプルな実装を実現してくれます。依存性の注入だけでなく、他にも状態値のキャッシュや自動的な解放など様々な機能が装備されています。

Provider というクラスをグローバルな変数の様に定義することで、widget ツリーを考慮することなく、データの注入を行うことが可能です。

Riverpod Generator はこの Provider をシンプルなアノテーションと定義で自動生成を行ってくれるパッケージです。Generator を用いることで冗長になりがちだった Provider の定義をシンプルにすることができます。また型の安全性を高めたり、よく使われるメソッドも自動で生成してくれます。

Generator で生成するメリット・デメリット

メリット

  • シンプルな記述
  • 戻り値に応じて、自動で Provider を選定してくれる
  • Family の引数の自由度が上がる

デメリット

  • Provider の実装がブラックボックス化

メリットはなんと言っても記述のシンプルさです。素で riverpod を記述していた人はその恩恵を特に感じるでしょう。また地味に面倒だった Family の引数の自由度も上がります。

デメリットはやはりブラックボックス化です。直接 Provider を記述しない為、Provider の理解がないと裏でどの Provider が生成されているのか分からなくなります。

Riverpod generator を使った実装手順

  1. パッケージのインストール
  2. 定義ファイルにインポート
  3. アノテーションを記述
  4. build_runner でコード生成

1.パッケージのインストール

Riverpod を使うので当然ですが、flutter_riverpodをインストールします。
https://pub.dev/packages/flutter_riverpod

riverpod_generator を使う際には以下 3 つのパッケージをインストールします

https://pub.dev/packages/riverpod_generator
https://pub.dev/packages/riverpod_annotation
https://pub.dev/packages/build_runner

pubspec.yaml で以下のとおり、追記します

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 を定義するファイルで使用するパッケージをインポートします

some_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

ファイル単位でのインポートでは riverpod_annotation と後に生成されるファイルのインポートを記述しておく必要があります。

some_provider.dart
 import 'package:riverpod_annotation/riverpod_annotation.dart';

+ part '{ファイル名}.g.dart';

3. アノテーションを記述

provider の定義前にアノテーションを付けます

some_provider.dart
 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 {返り値};
}

ルール

  1. riverpod_annotation と生成ファイルをインポート
  2. @riverpod のアノテーションを付ける
  3. part で指定するファイル名:ファイル名を間違えないように気をつけてください。異なると認識されません。
  4. メソッド名: 自由(記法はローワーキャメルケース)
  5. リファレンス名: メソッド名のアッパーキャメルケース+Refを指定します
  6. 返り値の型と返り値:任意で指定します
  7. 生成されるプロバイダ名ファイル名のローワーキャメルケース+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;
   }
}

ルール

  1. riverpod_annotation、生成ファイルをインポート
  2. @riverpod のアノテーションを付ける
  3. クラス名:自由(記法はローワーキャメルケース)
  4. build メソッド:必ず記述します
  5. 状態値状態値とその初期値を定義します。状態値を持たない場合は返り値を void にします
  6. クラスメソッド:自由に記述してください。state で状態値にアクセス出来ます
  7. 生成されるプロバイダ名ファイル名のローワーキャメルケース+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 型になっただけです。それ以外のルールは同じです。

その他ポイント

  1. 状態値の型で指定した値は自動的に AsyncValue にラップされます。

  2. 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つ以上の値を渡したい場合、クラスにまとめる必要がありました。それに比べ非常に柔軟です。

some_controller.dart
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)と書き換えるだけです。頭文字が大文字になっていることに注意してください。

some_controller.dart
 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 を使うのが良いかと思います。

サンプルコード

サンプルコードは全て下記レポジトリよりご覧いただけます

https://github.com/heyhey1028/flutter_samples/tree/main/samples/generate_riverpod

最後に

素で Riverpod を書いた経験がないと riverpod_generator でなんの Provider が生成されているか分からないというデメリットがあると書きました。

しかし逆に素で書いていた経験に引っ張られてしまうことで混乱する事もあるように感じました。実際私は混乱しました。もしかしたら素で書いた経験がない方がスッと馴染める可能性もあります。

なので 初学者は躊躇せず最初から riverpod_generator に入門しても良いのかも! と思ったり、思わなかったりでした!!

お疲れ様でした!!

Flutter大学

Discussion

あっぷる中谷あっぷる中谷

細かいところなんですけど

ルール
riverpod_annotation と生成ファイルをインポート
@riverpod のアノテーションを付ける
part で指定するファイル名:ファイル名を間違えないように気をつけてください。異なると認識されません。
メソッド名: ファイル名をローワーキャメルケースにした名前にする
リファレンス名: ファイル名のアッパーキャメルケース+Refを指定します
返り値の型と返り値:任意で指定します
生成されるプロバイダ名:ファイル名のローワーキャメルケース+Providerで生成されます

ここが

  1. メソッド名: ファイル名をローワーキャメルケースにした名前にする
  2. リファレンス名: ファイル名のアッパーキャメルケース+Refを指定します

違っていて、

正確には

  1. メソッド名: 自由(記法はローワーキャメルケース)
  2. リファレンス名: メソッド名のアッパーキャメルケース+Refを指定します

です!

修正頂けると助かります🙇

heyhey1028heyhey1028

確かに。人によってProviderって名前入れたり、入れなかったり自由にしますもんね。ご指摘ありがとうございます👍

あっぷる中谷あっぷる中谷

ありがとうございます!!

デフォルトだと自動生成の方がメソッド名+Providerの命名になるので
ほんとこの辺名前の付け方難しいなぁと思ってます😅

heyhey1028heyhey1028

わかりみです。どこかでこの自動でメソッド名に付くProviderを外せる機構が欲しいってissue見た気がします😅