Riverpod の watch・read・listen
共有状態は Widget の外に持ち出す。Riverpod で状態を宣言し、それを読む三つの入口 watch・read・listen の違いと使い分けを、最小の実コードで整理する。
前回、状態には「その画面限定のローカル状態」と「複数の画面で共有する状態」があり、後者は
StatefulWidget の外へ持ち出すと述べました。その「外」を担うのが Riverpod です。
Riverpod でいちばん最初に迷うのが、状態を読む入口が三つあることです——ref.watch /
ref.read / ref.listen。名前が似ていて、どれを使えばいいか分かりにくい。今回はここを、
「どこで呼ぶか」「呼ぶと何が起きるか」の二軸で一度に片付けます。
§ 01WHYProvider が解決する問題
状態を Widget の外に持つと、何がうれしいのか。
- 共有: ログインユーザーやカゴの中身を、どの画面からでも同じ実体として読める
- バケツリレーの解消: 深い階層へ値を渡すために、途中の Widget すべてにコンストラクタ引数を 通す必要がなくなる
- 依存の注入: 「この画面はこのサービスを使う」という依存関係も、同じ仕組みで差し替え可能に
なる(テスト時に本物を偽物へ差し替える、など)1Provider はテストや画面単位で
ProviderScope(overrides: ...)により別実装へ差し替えられる。これが「Provider を依存性注入として使う」の核で、テストの回で改めて扱う。
Riverpod では、状態や依存を Provider という宣言で定義し、ProviderScope(連載第1回で
runApp() のルートを包んだ、あれ)配下のどこからでも読み出します。
§ 02PROVIDER状態の入れ物を定義する
Provider は「ここにこの値がある。作り方はこう」という宣言です。グローバルに置きますが、
実際に読まれるまで作られない遅延評価で、必要なくなれば破棄もできます2Provider は autoDispose で「読まれなくなったら破棄」にでき、逆に keepAlive で保持し続けることもできる。この破棄と再生成の話は、後の invalidate の回で掘り下げる。。
最小の例を二つ。読み取り専用の値と、変更できるカウンターです。
// 派生・読み取り専用の値final greetingProvider = Provider<String>((ref) => 'こんにちは');// 変更できる状態(Notifier)final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);class CounterNotifier extends Notifier<int> {@overrideint build() => 0; // 初期値void increment() => state++; // state を書き換えると購読側へ通知される}
Notifier の state を書き換えると、その Provider を監視している側へ変更が伝わります。
setState() に相当する通知を、Widget の外側で行っている、と考えると対応が取れます。
§ 03CONSUMERref から読む
Provider を読むには WidgetRef ref が要ります。ConsumerWidget を継承すると、build() が
ref を受け取れます。
class CounterScreen extends ConsumerWidget {const CounterScreen({super.key});@overrideWidget build(BuildContext context, WidgetRef ref) {final count = ref.watch(counterProvider);return Scaffold(body: Center(child: Text('$count')),floatingActionButton: FloatingActionButton(onPressed: () => ref.read(counterProvider.notifier).increment(),child: const Icon(Icons.add),),);}}
この ref が、三つの入口すべての起点です。ここから watch / read / listen を見ていきます。
§ 04WATCHbuild() の中で監視する
ref.watch(provider) は、値を読むと同時に購読する入口です。build() の中で使い、監視した
値が変わると、その Widget が再構築されます3watch は購読(依存)を登録するため、build() の外——初期化処理やコールバック——で呼ぶと誤り。Riverpod はそうした誤用を検知して警告する。。
final count = ref.watch(counterProvider); // count が変わるたび build() が走る
原則は単純で、UI に表示する値は build() の中で watch する。上の例では、カウンターの値が
増えるたびに画面の数字が更新されます。
§ 05READコールバックで一度だけ読む
ref.read(provider) は、購読せずに今の値を一度だけ読む入口です。ボタンの onPressed の
ように、「再描画したいのではなく、いま動作させたい」場面で使います。
onPressed: () => ref.read(counterProvider.notifier).increment(),
counterProvider.notifier で Notifier 本体を取り出し、メソッドを呼んでいます。ここで watch
ではなく read を使うのは、コールバックは監視の場所ではないからです。
ここで初学者がよく取り違えます。
build()の中でreadを使う → その provider の変化が再構築のきっかけにならない(他の理由で再構築されない限り、表示が更新されない)- コールバックの中で
watchを使う →watchはbuild()の中で依存を登録するためのもので、コールバックは呼ぶ場所として不適切
表示は watch、操作は read。まずこの対で覚えると、取り違えが激減します。
§ 06LISTEN変化に副作用で反応する
三つ目の ref.listen(provider, ...) は、値が変わったときに副作用を実行する入口です。watch
と違い、コールバックが走るだけでその場で再構築はしません。画面遷移、スナックバー表示、
ダイアログ——「状態が変わった瞬間に一度だけ何かしたい」場面のためのものです。
@overrideWidget build(BuildContext context, WidgetRef ref) {ref.listen(errorProvider, (previous, next) {if (next != null) {ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(next.message)),);}});// ... 通常の UI}
watch で副作用(showSnackBar など)を起こそうとすると、再構築のたびに発火して二重表示に
なります。副作用は listen——ここが watch との決定的な違いです。
§ 07RULE使い分けの一行ルール
三つを一枚の表にすると、判断は一瞬で済みます。
| 入口 | 呼ぶ場所 | 起きること | 使いどころ |
|---|---|---|---|
watch | build() | 値を監視し、変化で再構築 | 表示する値 |
read | コールバック | 今の値を一度読む/Notifier 取得 | ボタン等の操作 |
listen | build() | 変化時に副作用(再構築しない) | 画面遷移・通知表示 |
覚え方は 「表示は watch、操作は read、反応は listen」。呼ぶ場所(build かコールバックか)と 合わせて確認すれば、取り違えはほぼ防げます。
§ 08SUMMARY状態を読む三つの入口
- 共有状態は Provider として Widget の外に持ち、
ProviderScope配下から読む watch…build()で監視、変化で再構築。表示に使うread… コールバックで一度読む/Notifier を取る。操作に使うlisten…build()で変化に副作用。再構築はしない。反応(遷移・通知)に使う
これで状態を「持ち出して・読む」基本ができました。ただし現実のアプリでは、状態は最初から
そこにあるとは限りません。サーバーや DB から非同期にやってきます。次回は、その待ち時間を
UI に落とし込む FutureProvider と AsyncValue——loading / data / error の三状態を扱います。
- [1] Provider はテストや画面単位で
ProviderScope(overrides: ...)により別実装へ差し替えられる。これが「Provider を依存性注入として使う」の核で、テストの回で改めて扱う。 ↩ - [2] Provider は
autoDisposeで「読まれなくなったら破棄」にでき、逆にkeepAliveで保持し続けることもできる。この破棄と再生成の話は、後のinvalidateの回で掘り下げる。 ↩ - [3]
watchは購読(依存)を登録するため、build()の外——初期化処理やコールバック——で呼ぶと誤り。Riverpod はそうした誤用を検知して警告する。 ↩