プロバイダの利用方法
本セクションの前に「プロバイダとは」のセクションに目を通していただくことをおすすめします。
ここではプロバイダの利用方法について説明します。
ref
オブジェクトを取得する
プロバイダを利用するには、まず ref
オブジェクトを取得する必要があります。
このオブジェクトを通じて、プロバイダと様々なやり取りを行うことになります。
ref
はウィジェットもしくはプロバイダから取得することができます。
プロバイダから ref
を取得する
プロバイダはすべて ref
オブジェクトを引数として受け取ります。
final provider = Provider((ref) {
// `ref` を通じて他のプロバイダを利用する
final repository = ref.watch(repositoryProvider);
return SomeValue(repository);
})
この ref
はさらに別のオブジェクトに渡すこともできます。
例えば...
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter(ref);
});
class Counter extends StateNotifier<int> {
Counter(this.ref) : super(0);
final Ref ref;
void increment() {
// Counter は `ref` を使って他のプロバイダーを利用することができる
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}
これで StateNotifier である Counter
は ref
を通じて他のプロバイダを利用することができるようになりました。
ウィジェットから ref
を取得する
通常のウィジェットでは ref
を使用することができないため、Riverpod では複数の方法を用意しています。
StatelessWidget の代わりに ConsumerWidget を継承する
ウィジェットツリーの中で ref
を取得するには StatelessWidget を ConsumerWidget に置き換えるのが基本です。
ConsumerWidget は StatelessWidget とほぼ同等のものであり、build メソッドに第2パラメータが存在する以外の違いはありません。
それが ref
です。
ConsumerWidget の使用例は次の通りです。
class HomeView extends ConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// `ref` を使ってプロバイダーを監視する
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
StatefulWidget+State の代わりに ConsumerStatefulWidget+ConsumerState を継承する
ConsumerWidget と同様、ConsumerStatefulWidget+ConsumerState は StatefulWidget+State に対応します。
唯一の違いは ref
オブジェクトが使用できるということです。
ただ ConsumerWidget と異なり、build メソッドに ref
オブジェクトは渡されていません。
ref
は ConsumerState のプロパティです。
class HomeView extends ConsumerStatefulWidget {
const HomeView({super.key});
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends ConsumerState<HomeView> {
void initState() {
super.initState();
// `ref` は StatefulWidget のすべてのライフサイクルメソッド内で使用可能です。
ref.read(counterProvider);
}
Widget build(BuildContext context) {
// `ref` は build メソッド内で使用することもできます。
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
HookWidget の代わりに HookConsumerWidget を継承する
これは flutter_hooks のユーザ向けです。 フックを使用するために HookWidget を使っている場合、そのまま ConsumerWidget に置き換えることができません。
そこで登場するのが hooks_riverpod の HookConsumerWidget です。 HookConsumerWidget は ConsumerWidget と HookWidget の役割を併せ持ちます。 これにより、ウィジェットはプロバイダーとフックの両方を利用することができます。
使用例は次の通りです。
class HomeView extends HookConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// build メソッド内でフックを使用できます。
final state = useState(0);
// `ref` オブジェクトを使ってプロバイダを監視することもできます。
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
HookWidget の代わりに StatefulHookConsumerWidget を継承する
flutter_hooks のフックに加えて StatefulWidget のライフサイクルメソッドを活用する必要がある場合は StatefulHookConsumerWidget に置き換えてください。
使用例は次の通りです。
class HomeView extends StatefulHookConsumerWidget {
const HomeView({super.key});
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends ConsumerState<HomeView> {
void initState() {
super.initState();
// 「ref」は StatefulWidget のすべてのライフサイクルメソッド内で使用できます。
ref.read(counterProvider);
}
Widget build(BuildContext context) {
// HookConsumerWidget のように build メソッドの中でフックが使えます。
final state = useState(0);
// プロバイダ監視のために「ref」を使用することも可能です。
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
Consumer と HookConsumer ウィジェット
ref
オブジェクトは Consumer もしくは HookConsumer ウィジェットのコールバック関数 builder
から取得することもできます。
これらのウィジェットを使用する場合は ConsumerWidget および HookConsumerWidget のようにクラスを定義する必要がありません。 使用例は次の通りです。
Scaffold(
body: HookConsumer(
builder: (context, ref, child) {
// HookConsumerWidget と同様にフックが使えます。
final state = useState(0);
// `ref` オブジェクトを使ってプロバイダを監視することもできます。
final counter = ref.watch(counterProvider);
return Text('$counter');
},
),
);
ref
を使ってプロバイダを利用する
ref
オブジェクトの取得方法が分かったところで、早速使ってみましょう。
ref
の使い道は主に3通りあります。
ref.watch
: プロバイダの値を取得した上で、その変化を監視する。値が変化すると、その値に依存するウィジェットやプロバイダの更新が行われる。ref.listen
: プロバイダの値を監視し、値が変化するたびに呼び出されるコールバック関数(画面遷移、ダイアログの表示など)を登録する。ref.read
: プロバイダの値を取得する(監視はしない)。クリックイベント等の発生時に、その時点での値を取得する場合に使用できる。
機能の実装時には可能な限り ref.watch
を使用するようにしてください。
ref.watch
によりアプリはリアクティブかつ宣言的になり、コードの保守性を高めることができます。
ref.watch
を使ってプロバイダを監視する
ウィジェットあるいはプロバイダ内で ref.watch
を使ってプロバイダを監視することができます。
例えば、ref.watch
でプロバイダに別の複数のプロバイダを監視させ、それらの値を組み合わせて新たに値を生成するということも可能です。
この手法は Todo リストのフィルタリングにも活用できます。 例えば、Todo アプリに次のプロバイダがあるとします。
filterTypeProvider
: 現在のフィルタの種類を公開するプロバイダ(なし、完了のみ表示、未完了のみ表示...)todosProvider
: Todo リストの内容をすべて公開するプロバイダ
これらのプロバイダを第3のプロバイダで ref.watch
を使って組み合わせ、Todo リストにフィルタを適用します。
final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());
final filteredTodoListProvider = Provider((ref) {
// フィルタの種類と Todo リストを取得、監視する
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);
switch (filter) {
case FilterType.completed:
// Todo リストを完了タスクのみにフィルタリングして値を返す
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// フィルタ未適用の Todo リストをそのまま返す
return todos;
}
});
これで filteredTodoListProvider
はフィルタ適用済みの Todo リストを公開することができます。
この Todo リストは、filterTypeProvider
や todosProvider
の値が変わると自動的に更新されます。
逆に言えば、いずれかが変わらない限りは再計算されることはありません。
同様にウィジェット内でも ref.watch
を使ってプロバイダの値を表示し、値が変わるたびに UI を更新させることができます。
final counterProvider = StateProvider((ref) => 0);
class HomeView extends ConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// `ref` を使ってプロバイダを監視する
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
このサンプルコードでは、カウンターの数字を値として管理するプロバイダを監視しています。 プロバイダの値が変わると、ウィジェットが更新されて新しい値で UI が構築されます。
watch
メソッドは ElevatedButton の onPressed
内など、非同期的な場面で呼び出さないでください。
また initState
を始め、State のライフサイクルメソッド内での使用も避けてください。
これらの場合は代わりに ref.read
を使用してください。
ref.listen
を使ってプロバイダを監視する
ref.listen
は ref.watch
と同様にプロバイダを監視することができます。
最も大きな違いは、ref.watch
が値の変化に応じてウィジェットやプロバイダを更新するのに対して、
ref.listen
は任意の関数を呼び出してくれるという点です。
例えば、エラー発生時のスナックバー表示など、何かしらの変化に反応して処理を実行したいときに便利です。
ref.listen
メソッドは2つの位置引数が必要です。
第1引数にプロバイダ、第2引数にステート(状態)が変化した際に実行するコールバック関数を設定します。
このコールバック関数には呼び出し時に、プロバイダの直前のステートと新しいステートの値が渡されるため、それぞれをパラメータとして使用できます。
以下はプロバイダ内で使用した例です。
final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);
final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
});
また、ウィジェットの build
メソッド内でも使用できます。
final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);
class HomeView extends ConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
return Container();
}
}
listen
メソッドは ElevatedButton の onPressed
内など、非同期的な場面で呼び出さないでください。
また initState
を始め、State のライフサイクルメソッド内での使用も避けてください。
ref.read
を使ってプロバイダのステートを取得する
ref.read
メソッドを使うことでプロバイダのその時点でのステートを取得することができます。純粋な取得以外の副作用はありません。
ref.read
はユーザ操作によって呼び出される関数内で使用するのが一般的です。
例えば、ボタンクリックイベント発生時にカウンターの数字を変更する場合に使用できます。
final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);
class HomeView extends ConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// `Counter` クラスの `increment()` メソッドを呼び出す
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
ref.read
はリアクティブではないため、可能な限り使用を避けてください。
watch
や listen
の使用では問題が生じる場合の回避策として存在しています。
ほとんどの場面では watch
や listen
の使用、特に watch
の使用がベターなはずです。
【重要】 ref.read
は build メソッドの中で使わない
ウィジェットのパフォーマンスを最適化するため、ref.read
を次のように使いたくなることがあるかもしれません。
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
// `ref.read` を使うことでプロバイダのステート変化は無視される
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
しかしこれは悪い使用例であり、追跡困難なバグの要因になり得ます。
このような ref.read
の使い方は、次のような考え方がもとになっていることが多いと思われます。
「プロバイダが公開する値が変わることはない。だから ref.read の使用は安全である」。
しかし、そのプロバイダは今は値が変わることがないかもしれませんが、明日も同じことが言えるとは保証できません。
その点でこの推測には問題があると言えます。
ソフトウェアに変更はつきものです。そして将来、絶対に変わらないと考えられていた値が、変わる必要に迫られることは十分あり得ることです。
そしてもしこの値の取得に ref.read
が使われていたら、その時にはコードを振り返り ref.read
が使われているところをすべて
ref.watch
に変更する必要があります。これはエラーを招くおそれがありますし、変更し忘れる箇所がいくつか出てくる可能性もあります。
一方、最初から ref.watch
を使用していれば、リファクタリング時に生じる問題は比較的少ないはずです。
でもウィジェットの更新回数を抑えるためにどうしても ref.read
が使いたい
このような声に対しては、ref.watch
を使用してまったく同じ効果を得ることができる点を強調しておきたいです。
例えば...
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
とする代わりに、
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
としてください。 この2つのコードの効果はいずれも同等です。 カウンターの数字が増えた際にボタンが更新されることはありません。
加えて、後者のアプローチの場合はカウンターの数字がリセットされた場合にも対応することができます。 例えば、別の場所で次のメソッドを呼び出すとします。
ref.refresh(counterProvider);
するとこの ref.refresh
メソッドにより StateController
オブジェクトは再度生成されます。
もしここで ref.read
を使用していたら、ボタンは古い StateController
インスタンスを使うことになります。
このインスタンスはこの時点で既に破棄されているため、使うべきではないはずです。
一方 ref.watch
を使用した場合は、ボタンが更新されて新しい StateController
を取得することができます。
プロバイダから取得できる値の種類
利用するプロバイダによっては、取得できる値の種類にバリエーションが存在します。
例えば、次のような StreamProvider があるとします。
final userProvider = StreamProvider<User>(...);
この userProvider
を利用する場合は、以下の値を取得することができます。
userProvider
を監視することで、Stream の現在のステートを同期的に取得する。Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<User> user = ref.watch(userProvider);
return user.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => const Text('Oops'),
data: (user) => Text(user.name),
);
}userProvider.stream
を監視することで、Stream を取得する。Widget build(BuildContext context, WidgetRef ref) {
Stream<User> user = ref.watch(userProvider.stream);
}userProvider.future
を監視することで、Stream から出力された最新の値で解決する Future を取得する。Widget build(BuildContext context, WidgetRef ref) {
Future<User> user = ref.watch(userProvider.future);
}
その他のプロバイダで取得できる値の種類については、 API reference を参照してください。
select
を使って更新の条件を限定する
最後に、ウィジェットおよびプロバイダの更新(もしくは ref.listen
による関数の実行)の条件を限定する方法をご紹介します。
プロバイダを監視するということは、そのプロバイダが公開するオブジェクト全体のステートを監視するということです。 しかし、その監視の範囲を狭めて特定のプロパティのみを監視対象としたい場合があります。
例えば、次の User
オブジェクトを公開するプロバイダがあるとします。
abstract class User {
String get name;
int get age;
}
それを監視するウィジェット側は user の name プロパティしか使用しません。
Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}
このように userProvider を普通に監視すると、 無関係な age プロパティの変化もウィジェット更新のトリガーとなってしまいます。
監視対象を name プロパティに限定したい場合は、select
を使ってその旨明示します。
変更後のコードは次のようになります。
Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}
つまり select
を使って監視対象にするプロパティを返す関数を指定することができるのです。
これで User
オブジェクトに変化があるたびに、Riverpod はこの関数を呼び出し、対象プロパティの古い値と新しい値を比較します。
そして値が異なる場合は Riverpod はウィジェットを更新します。
一方、対象プロパティ以外の値が変わっただけで対象プロパティの値自体が不変の場合は、Riverpod はウィジェットの更新を行いません。
もちろん、select
と ref.listen
を組み合わせることもできます。
ref.listen<String>(
userProvider.select((user) => user.name),
(String? previousName, String newName) {
print('The user name changed $newName');
}
);
このようにすることで name プロパティの値が変わったときにだけ、第2引数で登録したコールバック関数を呼び出すことが可能になります。
select
で明示する値は、必ずしも対象オブジェクトのプロパティそのものである必要はありません。
==
演算子のオーバーライドなどでオブジェクトの等価性が定義されていれば何を返しても問題ありません。
例えば、以下のような値を返すことも可能です。
final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));