【Flutter解説】Riverpod2.0 世代の使い方と概念まとめ

 

※この記事では基本的なClassやFlutterウィジェットの使い方をなんとなく分かる読者を想定しています。

 

段々と細かい話になっていきますが、読み物として適当に読んでいく感じでも大丈夫です。なるべく誰でも読めるようにします。

 

Riverpod初心者の方は順番に見ていくと良い感じです。

 

Riverpodって何

めちゃくちゃざっくり言えば、Flutterで状態管理しやすくするためのパッケージです。

 

アプリ開発だと画面から遠くの別の画面にデータを渡すことが難しかったり、クラスのインスタンスの管理で失敗しやすかったり(うっかり別のインスタンス作っちゃうとか)

、サーバーのデータが更新されたやつを待機してすぐ画面に反映する方法とかがわからなかったりするんです。

 

そういうのを、とりあえずデータ置く場所を作って、使いたい時に拾いに行ったりとか、新しいデータが来たら画面を更新したりとか、そう言う仕事をしてもらうためのものです。

 

用語として、

Provider(プロバイダー/供給担当さん)がデータを置いておく場所のこと。

Notifier(ノティファイアー/更新担当さん)がプロバイダーの中身を更新するためのクラスのこと。

ref(レフ/受け取り担当さん)がプロバイダーの中身を見に行ったり、通知を受け取ったりする観測者のこと。

 

基本の木

@riverpodのクラスや関数を書いて、import分の下の方に

 

part 'ファイル名.g.dart';

と書き足し、

build_runnerやriverpodパッケージをインストールした後に

 flutter build_runner build

のコマンドをコンソール上で叩くと、5秒くらいでこのファイルのすぐ下に「ファイル名.g.dart」のファイルが生成されて、ConsumerWidetなどのRiverpod用ウィジェットからプロバイダーが使えるようになります。

 

イメージはそんな感じで、スタートガイドは公式を翻訳して見てください。

Riverpod2.0と1.0だったり、Generatorとそうでないものとでちょっとごっちゃになってるのでご注意ください。

riverpod.dev

 

Classベースと関数ベース

@riverpod

int counter(CounterRef ref){

 return 0;

}

この書き方をしてflutter build_runnner buildコマンドを打つと、Providerが生成されます。

あ、part 'ファイル名.g.dart'; を忘れずに。

 

こちらはNotifierを持たず、更新をすることができません。

 

  @riverpod
  class Counter extends _$Counter{

  @override
  int build(){
   return 0;
  }
 }

この書き方をした場合、NotifierProviderが生成されます。

 

どちらでも使い方は同じで、使用時は

final count = ref.watch(counterProvider);

という具合でint型の0が取得できる。

 

この2種類の使い分けは、状態管理としてデータの更新をしたい場合はクラスの方を作ってNotifierがあると何かと便利、クラスのインスタンスを保持したいだけとか他のProviderの値を見てちょっと加工して表示したいだけとかの場合は関数の方が短くて良いです。

 

まあどっちにしろクラスの方が多機能で、関数ベースと同じこともできるので、僕はクラスの方が好きです。

 

refって何

Riverpodを使う場合は、StatelessWidgetなどの代わりに、ConsumerWidgetを使います。

 

refはConsumerWidgetやHookConsumerWidget等を作成する際に、WidgetRef refとして引数に与えられていたり、

Riverpodクラスの中で extends _$Counter 等の継承元クラスから与えられていたりします。

 

型はNotifierProviderRefとかSampleRefとか(生成されたProviderの種類による)。

基本的には作ったProvider専用Ref型か、WidgetRef型です。

 

ref.watch、ref.read、ref.listen、ref.onDisposeとか。Providerの値と変更に関するメソッドがこのrefにくっついています。

 

これがどこのrefなのか、いつのrefなのかっていうのがちょっと大事だったりするんですけど、それはまあ後のお話。

 

実を言うと、このrefというのは基本はProviderが持っているもので、どうやらConsumerWidgetやHookConsumerWidgetは普通のWidgetにProviderをくっつけてrefの機能を持たせたものらしいです。

 

というわけで概念としてはrefはProviderが持ってるもの。ConsumerWidgetとかもProviderの一種。ref.watch()を使っている状況で値が変わるとProviderが再ビルドされる。ってだけ考えると分かりやすい。

 

ConsumerWidgetだけじゃなく、Providerであればどちらも同じようにref.watch()での再ビルドが使えます。

 

使い方

Consumer系のWidgetを使う時はまずProviderScopeという親Widgetが必要です。

void main(){

 runApp(const ProviderScope(child:myApp()));

}

という感じ。使いたいWidgetがProviderScope()の中に入っていれば良いのですが、よくあるやり方としては、一番上になるWidgetをProviderScope()にする。という具合です。

 

この子はStatefulWidgetを継承してるので、WidgetとしてrunApp()の引数にも使えます。

 

で、このProviderScopeのWidgetツリー下にあるConsumer系のWidget内か、もしくはRiverpodクラスや関数の中でrefを使えば、ProviderやNotifierはどこからでも参照できるものになっています。

 

class CountText  extends ConsumerWidget{

 @override

 Widget build(BuildContext context, WidgetRef ref){

  final count = ref.watch(counterProvider);

  return Text(count.toString());

 }

}

 

こんな感じ。

で、Widgetツリー下でこの値に関わるものが生成された場合。この例で言えばCountTextウィジェットWidgetツリーで使用されて生成されたときに、初めてcounterProviderがwatchされて、counterProviderのbuild()メソッドが走り、初期値が生成されます。

 

このCountTextのWidgetWidgetツリーから外れたときにCounterProviderはディスポースされてます。消えます。そしてまた次に生成される時にはCounterProviderには初期値の0が設定されます。

 

また、クラスで作る方のProviderでは

ref.watch(counterProvider.notifier)

も使うことができますが、こっちの値はNotifierのインスタンスであり、Providerの値ではありません。

 

一応、

final count = ref.watch(counterProvider.notifier).state;

として、Providerの値がこんなところにあったりはするのですが、これを使おうとするとriverpod_lint ルールに怒られて警告表示が出ます。

 

Riverpod2世代ではこのNotifierからstate(= 値)にアクセスする方法は非推奨になっています。

大人しくProviderから値を参照しましょう。

 

ちなみに

 

@riverpod

class Counter extends _$Counter{

 @override

  int build(){

  return 0;    //←ココ

 }

}

 

ココ、と書いた部分がこのProviderの初期値になります。

 

Notifierって何

じゃあどこがNotifierになってるのという話ですが、

この@riverpodが付いたクラス全体、このクラス自体がNotifierの部分になります。

 

このNotifierは、Providerの値を更新するために使います。

 

このクラスにメソッドを追加します。

@riverpod

class Counter extends _$Counter{

 @override

  int build(){

   return 0; 

 }

 void plusCount(){

  state = state +1; 

 }

}

 

こうしてメソッドを追加すると、

ref.read(counterProvider.notifier).plusCount();

が使えるようになります。

 

少しわかりにくいのが

//こっちはプロバイダーで、値が入ってる。

int count = ref.watch(counterProvider);

 

//こっちはNotifierで、@riverpodを付けたクラスのインスタンスが入ってる。

Counter counterNotifier = ref.watch(counterProvider.notifier);

 

いうことで、Providerからとってくるのが値であるint型、

.notifierから取ってくるのが Couterクラス型になっています。

 

普通にCounterクラスのインスタンスを作る場合は

Counter counter = Counter();

て感じですが、これをref.watch(counterProvider.notifier)を使えば、どこからでも同一インスタンスを参照できるイメージでOKです。

 

よくわかんないですけどCounter().plusCount();とか、refを使わないで適当にNotifierのインスタンス作って使おうとするのはヤバめです。多分動いたりするんですけどProviderとは関係ない存在になったりするので訳がわからんコードになります。

 

長ったらしいですが、画面ごとに参照してるインスタンスを間違えたりしなくなるので良い子です。

 

このインスタンスはまあどこからもwatchされなくなったり、ref.refreshとかいうのを使ったりするまでは、基本的には同じ1つのインスタンスを参照していることになります。

 

このNotifierやProviderのインスタンスが自動破棄されるのが嫌な場合は

@Riverpod(keepAlive:true)

と、@riverpodの代わりに書いておけばディスポースされなくなります。

 

ディスポースされたかどうかを見たい場合は

@riverpod

class Counter extends _$Counter{

 @override

 int build(){

  ref.onDispose( () {

   debugPrint('Counterがディスポースされました');

   _dispose();

  });

  return 0;

 }

void _dispose(){

 //なんかディスポースされる時の処理とか。

 }

}

 

 

とか書いてディスポース時の処理を記述しておけば、判定できます。

ref.onDispose( (){} )の{}中には普通に他の処理を書いても良いです。やりたい処理があれば。

 

んで、providerの方の値(state)は、これはkeepAlive:trueとか関係なく普通に上書きされた場合には破棄されます。

 

というかイミュータブルな値で書き換えないと通知されないので、Notifierのメソッドでstateを書き換える場合は新しいオブジェクトが毎回必要になります。

 

値のパラメーターだけ更新とかもできますが(state.text = '新しいテキスト'; とか。クラスインスタンスをstateにしている場合)、こういうことをするとwatchへの通知は発生しません。

 

また、Provider内でref.watch()を使って他の値を監視しているときの再ビルドでは、keepAliveに関わらずProviderの値はちゃんと更新されます。

 

Stateって何

 

このRiverpodクラスや関数のbuild()で作るProviderの値は、クラスとかでも良いんですけど、

 

Class SampleItem{

 String text = 'サンプルテキスト';

 int count = 0;

}

 

@riverpod

class Sample extends _$Sample{

 @override

 SampleText build(){

  return SampleItem();

 }

 void stateUpdate(){

  state.text = '新しいテキスト';

  state.count += 1;

 }

}

とかやると、値の更新が通知されずに「アレっ?」になります。

画面を更新しようと思ったのに、Text(ref.watch(sampleProvider).text)とかで表示してButtonでref.read(sampleProvider.notifier).stateUpdateいても更新されませんね。

 

更新を通知する場合は

 

void stateUpdate(){

 final newItem = SampleItem()

  ..text = '新しいテキスト'

  ..count = state.count + 1;

 state = newItem;

}

ってな感じで新しいインスタンスとしてstateを上書きしてあげてください。

これ、state が [ ]のリスト型とかでも同じやり方なのでお気をつけて。

 

//List<String> build() => [ ] ;が初期値だとして

void addText(String newText){

  state.add(newText); //これが通知されない方法

  state = [...state,newText]; //これが通知される方法

}

 

また、通知はしたくない場合、たとえば.textパラメータを持つ、さっきのSampleItemクラスのインスタンスとかをProviderの初期値に置いて、

state.text = '新しいテキスト';

とか。こういうものをNotifierのメソッドの中で実装するのは一応アリっぽいです。

ただしインスタンスが変更されないので(同一オブジェクトとして判定されるので)、値が変更されていないことになって、watchしててもWidgetが更新されません。

 

Buttonで更新されたパラメーターを次の画面で取得できればそれでいい場合とか。そういう、今表示している画面に影響を与えて欲しくない場合にあえて通知しない方法でtextパラメーターの中身だけ更新するとかが可能です。

 

まあそもそもreadにすれば良いのでは、とかそういうこともあるのですが、これはRiverpod_lintに警告されないのでケースバイケースでやってもいいやつってことなんでしょう。

 

ジェネレーターって何やってるの

@riverpodを付けたクラスに対して、ジェネレーターはProviderを生成してくれます。あとextendsで継承している_$Counter みたいなクラスとかも生成してくれています。

 

part 'counter.g.dart';

 

@riverpod

class Counter extends _$Counter{

 @override

 int build(){

 return 0;

 }

}

// int がProviderの型を教えている。

// return 0; でProviderの初期値を設定している。

 

このクラスはNotifierの機能を書いていて、ref.watch(counterProvider.notifier)とかで持ってくるオブジェクトがCounterクラスのインスタンスだという話をしました。

 

このNotifierクラスの継承元の_$Counterクラスが、build()メソッドやらstateパラメーターを持っていて、ジェネレーターがcounter.g.dartを作ると継承が機能するようになります。

 

僕らが書いているのはNotifierクラスやbuild()メソッドで定義しているProviderの初期値や型であって、Providerの設計については書いていないんです。

 

このProviderの本体を生成してくれることと、NotifierがProviderのstateにアクセスできるようにしてくれているのがジェネレーターの役割です。

 

Providerの種類

Riverpod2ではProviderの種類は大まかに分けて3種類になりました。

  • Provider
  • FutureProvider
  • StreamProvider

これを書いてみると

 

// これがProvider
@riverpod

String sample(SampleRef ref){

 return 'サンプル'

}

 

// これがFutureProvider

@riverpod

Future<String> futureSample(FutureSampleRef ref) async {

  final  futureData = futureMethod(); //何かStringを返す、awaitが必要な処理。

  return futureData;

}

 

// これがStreamProvider

@riverpod

Stream<int> streamSample(StreamSampleRef ref) async*{

 yield 1;

 yield 2;

 yield 3;

}

 

// Streamあんまり使えてなくていい例が思いつかない...

 

とまあ、Dartの文法上普通の値はProvider、Futureな値はFutureProvider、Streamな値はStreamProviderとして宣言します。

 

これが基本的な3種類のProviderとして覚えてもらって、違いはref.watchやref.listenしたときの機能や値の使い方やタイミングが違うぞということが分かればOKです。

 

実はこの3種類、例に挙げたものは全て関数で書いたんですけど、これをクラスで書くと

 

// これがNotifierProvider
@riverpod

class Sample extends _$Sample{

 @override

 String build(){

  return 'サンプル'

 }

}

 

// これがAsyncNotifierProvider

@riverpod

class FutureSample extends _&FutureSample{

 @override

 Future<String> build() async {

  final  futureData = futureMethod(); //何かStringを返す、awaitが必要な処理。

  return futureData;

 }

}

 

// これがStreamNotifierProvider

@riverpod

class StreamSample extends _$StreamSample{

 @override

 Stream<int> build() async*{

  yield 1;

  yield 2;

  yield 3;

 }

}

 

という書き方になり、それぞれ

Provider→NotifierProvider

FutureProvider→AsyncNotifierProvider

StreamProvider→StreamNotifierProvider

 

として名前が変わります。(FutureだけAsyncになっちゃう。)

 

実際のところ、Providerとして扱っている値はそれぞれ多分同じで、関数ベースではProviderだけが生成されていたものが、クラスベースではNotifierも使えるようになるぞ、というそれだけの話です。

 

Riverpod2ではそこまでProviderの種類とかNotifierの種類がどうとかで覚えなくても大丈夫で、というかジェネレーターのやりたかったことが「Notifierさえ書いてしまえばProviderは気にしなくていい」という具合なのです。

 

実際にこの3種類のProviderをwatchやlistenして使う際に気をつけるべきことも普通のFutureやStreamの扱いと同じようなところです。

 

それほど、どのProviderだからどうとか、このNotifierにはこのProviderが対応してて...とかそういうことを考える必要はあまりないです。

 

ただFutureとかStreamとかのタイミングの問題が絡む処理というもの自体がエンジニアは苦手で、Riverpodで別の場所に保存して処理されているとなると余計に分からなくなっちゃう。。。とか、そっちの問題の方が根深そうです。。。

 

ref.read

providerを参照するときはref.watch(プロバイダー)みたいな書き方ですね。

 

また、状態変更を通知しない取得方法としてref.read(プロバイダー)の書き方が用意されています。

 

基本的に、よく分からないということであれば、全てref.watch()の方法で大丈夫です。

 

非同期処理でref.watchを使うとやばいというような話がありますが、あれはref.readでも同じくやばいので、この使い分けに関わらずとても気をつけましょう。

 

少なくとも、Riverpod2.0の動作では基本的にref.watchを使っておけばRiverpodらしい動作をします。ref.readを使いたいタイミングは、

 

あえて画面更新をしたくない場合。

 

 

(){}のラムダ式の中や、別の場所にある関数にrefを渡して使う場合。

 

です。

 

では、このreadとwatchの使い分けを書き出してみます。

それぞれの動作をチェックしてみたので、バグに困っている人とかはよーく確認してみてください。

 

これがまた非常に複雑なのですが、基本の使い方は一つ目のWidgetの書き方になります。

 

@riverpod

class ButtonLabel  extends _$ButtonLabel{

@override

 String build(){

  return '押してください';

 }

 void onTap(){

  state = '押されました';

 }

}

 

// ref.watch(buttonLabelProvider) ←これがWidgetの値にある場合に画面更新。

// ref.watch(buttonLabelProvider.notifier) ←これがあっても値の画面更新はしない。

 

//以下各Widgetでの書き方解説。

 

//Notifierインスタンスは保持される。値に変更があれば画面更新。

//これがよくある普通っぽい使い方。

class CustomButton extends ConsumerWidget{

 @override

 build(BuildContext context,WidgetRef ref){

  return TextButton(

   child:Text(ref.watch(buttonLabelProvider)), // 押したら表示が変わる

   onTap:ref.read(buttonLabelProvider.notifier).onTap, // 動く。

  );

 }

}

 

//Notifierインスタンスは保持される。値に変更があれば画面更新。

//これが両方参照し続けてるパターン。上のと動作は変わらない。

//このシチュエーションではこの書き方がオススメ。

class CustomButton extends ConsumerWidget{

 @override

 build(BuildContext context,WidgetRef ref){

  return TextButton(

   child:Text(ref.watch(buttonLabelProvider)), // ボタンを押すとすぐ表示が変わる

   onTap:ref.watch(buttonLabelProvider.notifier).onTap, // 動く。

  );

 }

}

 

//Notifierインスタンスは保持される。値は変更するけど画面更新しない。

//これは参照即切れ。他のタイミングの画面更新時にまた値が反映される。

//要注意。
class CustomButton extends ConsumerWidget{

 @override

 build(BuildContext context,WidgetRef ref){
  return TextButton(

   child:Text(ref.read(buttonLabelProvider)), // 押したタイミングで表示が変わらない

   onTap:ref.read(buttonLabelProvider.notifier).onTap, // 動く。値も変わる

  );

 }

}

 

//Notifierインスタンスは保持される。値は変更するけど画面更新しない。

//これは分かりにくい使い方。実は画面更新してくれないやつ。

class CustomButton extends ConsumerWidget{

 @override

 build(BuildContext context,WidgetRef ref){
  return TextButton(

   child:Text(ref.read(buttonLabelProvider)), // 押したタイミングで表示が変わらない

   onTap:ref.watch(buttonLabelProvider.notifier).onTap, // 動く。

  );

 }

}

 

//Notifierインスタンスは保持される。値は変更され画面更新もする。

//ProviderがwatchされているからNotifierも破棄されない。

//ラムダ式(){}の中ではreadが基本。

class CustomButton extends ConsumerWidget{

 @override

 build(BuildContext context,WidgetRef ref){
  return TextButton(

   child:Text(ref.watch(buttonLabelProvider)), // 押したタイミングで表示が変わる

   onTap:(){

    ref.read(buttonLabelProvider.notifier).onTap(); // 動く。

    },

  );

 }

}

 

//Notifierインスタンスが破棄される。値も破棄される

//通知なんか要らん、って感じの使い方。

class CustomButton extends ConsumerWidget{

 @override

 build(BuildContext context,WidgetRef ref){
  return TextButton(

   child:Text(ref.read(buttonLabelProvider)), // 押したタイミングで表示が変わらない

   onTap:(){

    ref.read(buttonLabelProvider.notifier).onTap(); // 都度新インスタンスとして動く。

    },

  );

 }

}

 

 

 

//Notifierインスタンスが一回破棄される。値は変更するけど画面更新はしない。

//一回ボタンを動かすと、そこにwatchが見つかってNotifierが保持される。

//これは多分みんな知らない、危ない実装。分かってても使っちゃいけない。

//だからこういうところでwatchを使うなと言われる。

//諸悪の権化

class CustomButton extends ConsumerWidget{

 @override

 build(BuildContext context,WidgetRef ref){
  return TextButton(

   child:Text(ref.read(buttonLabelProvider)), // 押したタイミングで表示が変わらない

   onTap:(){

    ref.watch(buttonLabelProvider.notifier).onTap(); // 初回は新インスタンスで動く。

    },

  );

 }

}

 

//Notifierインスタンスは保持される。値は変更するけど画面更新はしない。

//一つ上のやつの問題を解決した方法。これは画面更新しないと分かれば問題ない

class CustomButton extends ConsumerWidget{

 @override

 build(BuildContext context,WidgetRef ref){

  final notifier = ref.watch(buttonLabelProvider.notifier);
  return TextButton(

   child:Text(ref.read(buttonLabelProvider)), // 押したタイミングで表示が変わる

   onTap:(){

    notifier.onTap(); // 動く。

    },

  );

 }

}

 

//Notifierインスタンスは保持される。値は変更され画面も更新する。

//一番最初にインスタンスwatchすることを宣言。省略にも良い。

//推し。綺麗。

class CustomButton extends ConsumerWidget{

 @override

 build(BuildContext context,WidgetRef ref){

  final notifier = ref.watch(buttonLabelProvider.notifier);

  final buttonLabel = ref.watch(buttonLabelProvider);
  return TextButton(

   child:Text(buttonLabel), // 押したタイミングで表示が変わる

   onTap:(){

    notifier.onTap(); // 動く。

    },

  );

 }

}

 

 

//Notifierインスタンスは破棄される。値は変更するけど画面更新はしない。

//意外なことにこれが動く。

//値の変更はおそらく発生しているが、次に開いたときに再生成される。

//処理タイミングや内容には一応気をつけるべきだけど、通知問題は発生しない。

//時には必要かも。

class CustomButton extends ConsumerWidget{

 @override

 build(BuildContext context,WidgetRef ref){

  final notifier = ref.watch(buttonLabelProvider.notifier);
  return TextButton(

   child:Text(ref.watch(buttonLabelProvider)), // 押したら更新より先に前の画面に戻る

   onTap:()async{

    GoRoute.of(context).pop(); // 前の画面に戻る

    await Future<void>.delayed(const Duration(seconds:1)); // 1秒待ってみる。

    notifier.onTap(); // 裏で動く。

    //ここに ref.readやref.watch〜を書くとエラーが出る。

    },

  );

 }

}

 

/// Riverpod2.0の動作です。1.0はもしかしたら結構違うかもしれません。

 

 

重要な部分だけ言ってみると、

 

ProviderをWatchしている場合は値の変更時に画面が再構築されます。

NotifierをWatchしていても画面は更新されません。

 

この2点。

そして、ラムダ式の中のような、動かしてみないとwatchがあることが分からないというような場所にref.watchをおかないこと。

 

あと

ref.read()だからと言っていつでも使っていいわけじゃないということ。

 

Notifierを動かすことは問題じゃなくて、後からラムダ式の中で値を拾いに行くとか、そうやってrefを変なタイミングで使うのがまずい。

 

ビルド時に

final notifier =ref.watch(buttonLabelProvider.notifier);

とかで一回インスタンスを拾って、そのメソッドをボタンを押したときに使う、とかであれば大丈夫。

 

Notifierのクラスが色々とパラメーターを持っていたりして状態が気になる場合は、このインスタンス破棄についてよーく確認してみて下さい。

 

Notifierを大きなクラスにして色々便利に使っている場合は結構ここでバグが発生しがちになります。

 

という具合で、以上、この辺りがRiverpodのrefを使う上の注意点でした。

 

NotifierをWatchするということについて、Riverpodの作成者さんが回答しているものがあったのでリンクを置いておきます。

stackoverflow.com