【Riverpod】プロバイダーの読み取りのref.xxxを整理して解説
はじめに
皆さん、こんにちは。
Riverpodを学ぶ上で、ref
を介したプロバイダーの読み取りは最も重要な概念です。しかし、ref.watch
、ref.read
、ref.listen
、そしてselect
といった複数のメソッドがあり、それぞれの使い分けに戸惑うこともあるでしょう。この記事では、公式ドキュメントの内容を基に、これらのメソッドの役割と正しい使い方を具体的なコード例を交えて解説します。
サンプルコード
ref
を使えるように
ConsumerWidgetを継承して概要
- Providerを読み取るウィジェットはConsumerWidgetを継承して
- buildメソッドのWidgetRef型引数(ref)を利用する
- Providerの読み取り以外はStatelessWidgetと同じ
- Flutter Riverpod Snippetsでは「stlessConsumer」で雛形
プロバイダーの値を読み取るウィジェットを作成するには、ConsumerWidget
を継承するのが一般的な方法です。
ConsumerWidget
を継承するとbuild
メソッドの引数としてWidgetRef
型のref
を受け取ります。このref
を通じて、プロバイダーの値をref.watch()
、ref.read()
、ref.listen()
などのメソッドを使います。
ConsumerWidget
は、プロバイダーの読み取り機能を除けば、StatelessWidget
とほとんど同じように記述できます。
Flutter Riverpod Snippets(VS Code拡張機能)を使っている場合は、「stlessConsumer
」と入力するだけで、ConsumerWidget
の基本的な雛形が自動生成されます。
例(雛形のまま)
class MyConsumer extends ConsumerWidget {
const MyConsumer({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// buildメソッド内でrefを使ってProvierを読み取る
return Container();
}
}
ref.read
でその場限りの利用
概要
- 最もシンプルな読み取り方法
- 現在の状態を一度だけ取得し更新を監視しない
- 「その場限り」での利用に収めること
- buildメソッドの直下では利用しないこと
- UIに反映させる必要のないデータを読み取る
- 主にNotifier(更新処理の呼び出し)の参照で利用する
ref.read
はプロバイダーの最もシンプルな読み取り方法です。read
での読み取りはプロバイダーの現在の状態を一度だけ取得し、その後の更新を監視しません。
この特性から、ref.read
は「その場限り」での利用に留めるべきです。例えば、ボタンのonPressed
コールバック内で、その瞬間のデータ値(状態に保存したユーザ名とパスワード等)を取得したり、Notifier
のインスタンスを参照して更新処理を呼び出す際に主に利用します。
ElevatedButtonで状態更新処理を行う例
ElevatedButton(
onPressed: () {
// 状態更新をメソッドチェーンで
ref.read(counterProvider.notifier).increment();
},
child: Text('+'),
),
例
class MyConsumer extends ConsumerWidget {
const MyConsumer({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// このようにbuildの直下でのreadはアンチパターン
// final currentCount = ref.read(counterProvider);
// final counter = ref.read(counterProvider.notifier);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('$counter'),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
// 処理内での一次的な値の利用なので推奨される書き方
final currentCount = ref.read(counterProvider);
// 更新1:状態更新の呼び出しなので推奨される書き方
final counter = ref.read(counterProvider.notifier);
counter.increment();
// 更新2:状態更新はメソッドチェーンの以下でもOK
ref.read(counterProvider.notifier).increment();
},
child: Text('+'),
),
],
),
],
);
}
}
ref.watch
で変更を監視してUIに反映
概要
- プロバイダーを読み取りリアクティブな値(状態)を取得
- 状態の変更を監視し、変更に伴いUIを自動更新
- 値(状態)の読み取りはできるだけ
watch
を使う
ref.watch
は、最も基本的でかつ推奨される読み取り方法です。ref.watch
は単に現在の値を取得するだけでなく、状態の変更を監視し、変更に伴ってUIを自動的に更新する「リアクティブな値」を取得します。
例えば、カウンターアプリの画面に現在の数字を表示する場合、ref.watch
を使ってカウンターのプロバイダーを監視します。カウンターの値が増えるたびに、ref.watch
がその変更を検知し、カウンターの数字を表示しているウィジェットだけを自動的に再構築してくれるため、常に最新の数字が画面に反映されます。
このため、UIに表示するデータや、UIの振る舞いを決定するような状態を扱う際は、できるだけref.watch
を使うことが推奨されます。
例
class MyConsumer extends ConsumerWidget {
const MyConsumer({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// 状態の読み取り
final counter = ref.watch(counterProvider);
final hello = ref.watch(helloProvider);
// このように静的な値でもreadで読み取るのはアンチパターン
// final hello = ref.read(helloProvider);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 値の利用は通常の変数と同じ
Text('$counter'),
Text(hello),
],
);
}
}
ref.listen
で状態更新に伴う副作用
概要
- 状態更新を監視し、任意の関数を実行
- 更新前の値と更新後の値を利用可能
- 状態更新をきっかけにした副作用処理で利用
- 画面遷移、スナックバー表示、など
ref.listen
は、状態更新に伴う副作用を処理するためのメソッドです。プロバイダーの状態更新を監視し、状態が変化するたびにUIの再構築を伴わずに任意の関数を実行できます。
コールバック内では、previous
(更新前の値)とnext
(更新後の値)の両方を利用することができます。ref.listen
の主な用途は、状態更新をきっかけにした副作用処理です。
例えば、ユーザーの認証状態が「ログイン済み」に変わった際に画面遷移を行ったり、フォームの送信結果(成功/失敗)に応じて「保存しました」といったスナックバー表示やエラーダイアログを表示したりなどです。UIとは直接関係ないログ出力やアナリティクスイベントの送信などにも利用できます。
watch
がUIの再構築を伴うのに対し、listen
はUIとは独立した副作用を実行するために使われます。これにより、UIの描画ロジックと、それに付随する非UI的な処理を明確に分離することができます。
例
class MyConsumer extends ConsumerWidget {
const MyConsumer({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// 状態の読み取り
final counter = ref.watch(counterProvider);
// Counterが更新されたら実行したい関数を登録
ref.listen(counterProvider, (int? pre, int next) {
print('$pre -> $next changed!');
});
return Column( 省略 );
}
}
プロバイダー.select
で状態の一部のみを監視
概要
-
watch
やlisten
の引数をプロバイダー.select(監視対象の指定)
にする - オブジェクトの一部のプロパティのみを監視対象にしたい時に利用
- オブジェクト全体を監視すると無関係な変更で不必要なUIの再構築が発動してしまう
- パフォーマンス最適化の観点で利用する機能
状態がオブジェクトの場合はref.watch()
やref.listen()
の引数をプロバイダー.select()
とすることで、状態の一部のみを監視対象とすることができます。
オブジェクトを状態としている場合、ref.watch()
でそのまま監視すると、無関係なプロパティが変更されただけでも不必要なUIの再構築が発動してしまいます。例えば、ユーザーオブジェクトのage
が変わっただけで、name
しか表示していないウィジェットが再構築されるといった事態です。
これを避けるために、ref.watch()
やref.listen()
の引数に、provider.select((state) => state.property)
のように監視したいプロパティを返す関数を指定します。これにより、指定したプロパティの値が変更されたときだけウィジェットが更新されるようになります。
select
は、無駄な再構築を防ぎ、アプリケーションのパフォーマンスを最適化する観点で重要な機能です。
例(Userオブジェクトのnameだけを監視するウィジェット)
class MyConsumer extends ConsumerWidget {
const MyConsumer({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// Userオブジェクトのnameだけを監視対象にする
final userName = ref.watch(
userNotifierProvider.select((user) => user.name),
);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
// Userオブジェクトのnameを利用
Text(userName),
ElevatedButton(
onPressed: () {
// 状態の一部を更新する処理の呼び出し
ref.read(userNotifierProvider.notifier).updateState(newName: '新しい名前');
},
child: Text('名前変更'),
),
],
),
],
);
}
}
例(プロバイダーとUser型の定義)
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'user.g.dart';
class UserNotifier extends _$UserNotifier {
User build() {
return User(name: 'John', age: 10);
}
// 状態の更新
void updateState({String? newName, int? newAge}) {
state = state.copyWith(name: newName, age: newAge);
}
}
// User型
class User {
String name;
int age;
User({required this.name, required this.age});
// 渡された引数の項目のみ更新した新しいオブジェクトを生成
User copyWith({String? name, int? age}) {
return User(name: name ?? this.name, age: age ?? this.age);
}
}
おわりに
この記事では、Riverpodのref
を介したプロバイダーの読み取り方法について、それぞれの役割と使い分けを詳細に解説しました。
-
ref.read
: UIに直接反映させる必要のない、その場限りのデータ取得やメソッド呼び出しに使います。build
メソッド直下での使用はアンチパターンです。 -
ref.watch
: 状態の変化を監視し、UIを自動で更新するための、最も基本的なリアクティブな読み取り方法です。UIに表示するデータには基本的にwatch
を使用します。 -
ref.listen
: 状態変化をトリガーに、UIの再構築を伴わない副作用(画面遷移やスナックバー表示など)を実行するために使います。 -
select
: オブジェクトの状態の一部のみを監視対象に絞り込み、不要な再構築を防ぐことで、パフォーマンスを最適化します。
これらのメソッドは、それぞれ異なる目的とユースケースを持っています。使い分けを理解し実践することで、Riverpodの持つ強力なリアクティブ性を最大限に活かし、クリーンでパフォーマンスの高いアプリケーションを構築できるようになります。
Discussion