Riverpod.dev/docs(開始URL: https://riverpod.dev/docs/introduction/why_riverpod )には日本語訳ページ(/ja)も部分的には既にあるのですが、その部分はそのまま参照して、その他の英語訳のままのページについては自力で翻訳および解説を補足してみました。まだ工事中ですが最終的に満足ができたら上記サイトの翻訳ページに献上できないかな等という目標もあります。
本ブログの(上記Riverpod.dev/docsの)翻訳および解説の目次頁(構成上のトップページ)は次のURLになります。
本頁は、上記のうちの、「2’nd Concepts」の章の翻訳および解説頁になります。
注釈)上記Riverpod.dev/docs には(理由は未確認ですが)「Concepts」という章が(同じトップレベルに)2つ存在しています。その為、本ブログでは、1つ目のConcept章を「1’st Concepts」、2つ目のConcepts章を「2’nd Concepts」と記載してあります。
2’nd Concepts
1.Provider基本概念:本Verから学習する方向け
Providers
プロバイダーは、Riverpodアプリケーションの最も重要な部分である。 プロバイダは、状態の一部をカプセル化し、その状態をリッスンできるようにするオブジェクトです。
Why use providers?
かいつまんでお話しすると…
- 複数の場所でその状態に簡単にアクセスできる。 プロバイダは、Singletons、Service Locators、Dependency Injection、InheritedWidgetsなどのパターンを完全に置き換えるものです。
- この状態を他の状態と組み合わせることを簡単にします。 複数のオブジェクトを1つにまとめるのに苦労したことはないだろうか。 このシナリオはプロバイダーの中に直接組み込まれています。
- パフォーマンスの最適化を可能にします。 ウィジェットのリビルドをフィルタリングする場合でも、高価な状態計算をキャッシュする場合でも、プロバイダは状態変更によって影響を受けるものだけが再計算されるようにします。
- アプリケーションのテスト容易性が向上します。 プロバイダを使用すると、複雑な setUp/tearDown ステップが不要になります。 さらに、任意のプロバイダをオーバーライドして、テスト中に異なる動作をさせることができます。
- ロギングやプル・ツー・リフレッシュなどの高度な機能との統合が容易。
Creating a provider
プロバイダーにはさまざまな種類があるが、どれも同じように機能する。
最も一般的な使い方は、次のようにグローバル定数として宣言することだ:
@riverpod
MyValue my(MyRef ref) {
return MyValue();
}
プロバイダーのグローバルな側面に怯えてはいけない。 プロバイダーは完全に不変である。 プロバイダーを宣言することは、関数を宣言することと変わらない。
このスニペットは、次の3つの要素で構成されている。
- final myProvider (変数の宣言) この変数は、将来プロバイダーの状態を読み取るために使うものだ。 プロバイダーは常に最終的なものでなければならない。
- Provider ( プロバイダー)すべてのプロバイダーの中で最も基本的なものだ。 決して変更されないオブジェクトを公開する。 ProviderをStreamProviderやNotifierProviderのような他のプロバイダーに置き換えて、値の扱い方を変えることができる。
- 共有状態を作成する関数 常にrefと呼ばれるオブジェクトをパラメーターとして受け取る。 このオブジェクトによって、他のプロバイダーを読み込んだり、プロバイダーの状態が破棄されたときに何らかの処理を実行したり、その他いろいろなことができる。
プロバイダに渡された関数が返すオブジェクトの型は、使用するプロバイダによって異なる。 例えば、Providerの関数は任意のオブジェクトを作成できます。 一方、StreamProviderのコールバックはStreamを返すことが期待される。
プロバイダは、制限なくいくつでも宣言できます。 package:providerを使用する場合とは対照的に、Riverpodでは、同じ「タイプ」の状態を公開する複数のプロバイダを作成できます:
同じ「タイプ」の状態を公開する複数のプロバイダを作成できる:@riverpod String city(CityRef ref) => 'London'; @riverpod String country(CountryRef ref) => 'England';
どちらのプロバイダーがStringを作成しても問題はない。
プロバイダを動作させるには、FlutterアプリケーションのルートにProviderScopeを追加する必要がある:
ルートにProviderScopeを追加する必要がある:void main() { runApp(ProviderScope(child: MyApp())); }
Different Types of Providers
複数の異なるユースケースに対応する複数のタイプのプロバイダがあります。 これらすべてのプロバイダが利用可能であるため、1つのプロバイダタイプを他のプロバイダタイプよりも使用するタイミングを理解することが困難な場合があります。 以下の表を使用して、ウィジェット・ツリーに提供したいものに適合するプロバイダを選択してください。
種類 | 戻り値 | 使用例 |
---|---|---|
Provider | 任意の型 | サービス・クラス 計算されたプロパティ (フィルタリングされたリスト) |
StateProvider | 任意の型 | フィルター条件 単純な状態オブジェクト |
FutureProvider | 任意の型の Future | APIコールの結果 |
StreamProvider | 任意の型のストリーム | APIからの結果のストリーム |
NotifierProvider | (Async)Notifierのサブクラス | インターフェイスを通さない限り、不変である複雑な状態オブジェクト。 |
StateNotifierProvider | StateNotifier のサブクラス | インターフェイスを通さない限り不変の複雑なステート・オブジェクト。 notifierProviderを使用することをお勧めします。 |
ChangeNotifierProvider | ChangeNotifier のサブクラス | ミュータビリティを必要とする複雑な状態オブジェクト |
すべてのプロバイダにはその目的がありますが、ChangeNotifierProvidersはスケーラブルなアプリケーションには推奨されません。 「なぜ不変なのか」を参照してください。 package:providerからの簡単な移行パスを提供するためにflutter_riverpodパッケージに存在し、いくつかのNavigator 2パッケージとの統合など、flutter特有の使用例を可能にします:
Provider Modifiers
すべてのプロバイダには、さまざまなプロバイダに機能追加する方法が組み込まれています。
これらは、refオブジェクトに新機能を追加したり、プロバイダが消費される方法を少し変更したりします。 修飾子は、名前付きコンストラクタに似た構文で、すべてのプロバイダに使用できます:Dartfinal myAutoDisposeProvider = StateProvider.autoDispose<int>((ref) => 0); final myFamilyProvider = Provider.family<String, int>((ref, id) => '$id');
現時点では、2つの修正が可能だ:
- .autoDisposeを使用すると、プロバイダがリッスンされなくなったときに、その状態を自動的に破棄します。
- .familyは、外部パラメーターからプロバイダーを作成できる。
Providerは、一度に複数の修飾語を使用することができる:
final userProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
return fetchUser(userId);
});
このガイドは以上です!
Reading a Provider
プロバイダーを消費する方法を説明する。
Obtaining a “ref” object
何よりもまず、プロバイダを読み込む前に、”ref “オブジェクトを取得する必要がある。 このオブジェクトによって、ウィジェットや他のプロバイダからのプロバイダと対話することができる。
Obtaining a “ref” from a provider
すべてのプロバイダーは、パラメータとして “ref “を受け取る:
@riverpod
String value(ValueRef ref) {
// use ref to obtain other providers
final repository = ref.watch(repositoryProvider);
return repository.get();
}
このパラメータは、プロバイダが公開する値に渡しても安全である。
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() {
// Counter can use the "ref" to read other providers
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}
そうすることで、カウンター・クラスがプロバイダーを読むことができるようになる。
Obtaining a “ref” from a widget
ウィジェットには当然refパラメータはありません。 しかし、Riverpodはウィジェットからrefパラメータを取得するための複数のソリューションを提供しています。
Extending ConsumerWidget instead of StatelessWidge
ウィジェットツリーでrefを取得する最も一般的な方法は、StatelessWidgetをConsumerWidgetに置き換えることです。
ConsumerWidgetの使用方法はStatelessWidgetと同じですが、唯一の違いはビルドメソッドに “ref “オブジェクトという追加パラメータがあることです。
典型的なConsumerWidgetは次のようなものである:
class HomeView extends ConsumerWidget {
const HomeView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
//プロバイダーをリッスンするためにrefを使う
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
Extending ConsumerStatefulWidget+ConsumerState instead of StatefulWidget+State
ConsumerWidgetと同様に、ConsumerStatefulWidgetとConsumerStateは、StatefulWidgetのStateと同等であるが、Stateが “ref “オブジェクトを持つ点が異なる。“ref “はビルド・メソッドのパラメータとして渡されるのではなく、ConsumerStateオブジェクトのプロパティとして渡される:
class HomeView extends ConsumerStatefulWidget {
const HomeView({super.key});
@override
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends ConsumerState<HomeView> {
@override
void initState() {
super.initState();
//"ref "は、StatefulWidgetの全てのライフサイクルで使用できる。
ref.read(counterProvider);
}
@override
Widget build(BuildContext context) {
//ビルド・メソッド内でプロバイダーをリッスンするために "ref "を使うこともできる。
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
Using ref to interact with providers
プロバイダーとのやりとりにrefを使ってみましょう。
“ref” には主に3つの用法がある:
- プロバイダの値を取得し、この値が変更されたときに、その値をサブスクライブしたウィジェットまたはプロバイダをリビルドするように、変更をリッスンします。 これは、ref.watch を使って行います。
- プロバイダにリスナーを追加して、プロバイダが変更されたときに新しいページに移動したりモーダルを表示したりするアクションを実行します。 これは ref.listen を使って行います。
- 変更を無視してプロバイダーの値を取得する。 これは、”on click “などのイベントでプロバイダの値が必要な場合に便利です。これは ref.read を使って行います。
可能な限り、機能の実装にはref.readやref.listenよりもref.watchを使う方が好ましい。 ref.watchに頼ることで、アプリケーションはリアクティブかつ宣言的になり、保守性が高まる。
Using ref.watch to observe a provider
ref.watchは、ウィジェットのビルドメソッド内またはプロバイダのボディ内で使用され、ウィジェット/プロバイダがプロバイダをリッスンします:
例えば、プロバイダーはref.watchを使って複数のプロバイダーを新しい値にまとめることができる。
例えば、Todoリストのフィルタリングである。 プロバイダーは2つある:
- filterTypeProvider、現在のフィルタータイプ(なし、完了したタスクのみ表示、…)を公開するプロバイダー。
- todosProvider, タスクのリスト全体を公開するプロバイダー
そして、ref.watchを使うことで、両方のプロバイダーを組み合わせて、フィルタリングされたタスクのリストを作る第3のプロバイダーを作ることができる:
@riverpod
//(1)filterTypeProvider: 現在のフィルタータイプ(なし、完了したタスクのみ表示、…)を公開するプロバイダー
FilterType filterType(FilterTypeRef ref) {
return FilterType.none;
}
@riverpod
//(2)todosProvider: タスクのリスト全体を公開するプロバイダー
class Todos extends _$Todos {
@override
List<Todo> build() {
return [];
}
}
@riverpod
List<Todo> filteredTodoList(FilteredTodoListRef ref) {
//フィルタとTODOリストの両方を取得する
final FilterType filter = ref.watch(filterTypeProvider); //←(1)監視
final List<Todo> todos = ref.watch(todosProvider); //←(2)監視
switch (filter) { //←(1)参照
case FilterType.completed:
//完了したToDoリストを返す //→(2)更新
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
//フィルタリングされていないTODOのリストを返す
return todos; //→(2)更新
}
}
このコードにより、filteredTodoListProvider は、フィルタリングされたタスクのリストを公開するようになりました。
フィルタリングされたリスト(FilterType)は、フィルタまたはタスクのリストのいずれかが変更された場合にも自動的に更新されます。
同様に、ウィジェットは ref.watch を使ってプロバイダからのコンテンツを表示し、そのコンテンツが変更されるたびにユーザーインターフェイスを更新することができます:
@riverpod
int counter(CounterRef ref) => 0;
class HomeView extends ConsumerWidget {
const HomeView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
//プロバイダーをリッスンするためにrefを使う
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
このスニペットは、カウントを保存するプロバイダをリッスンするウィジェットを示している。 カウントが変更されると、ウィジェットは再構築され、UIは新しい値を表示するように更新されます。
ウォッチ・メソッド(ref.watch)は、ElevatedButtonのonPressedのように非同期に呼び出すべきではありません。
また、initStateやその他のStateのライフサイクルの中で使用してはならない。そのような場合は、代わりにref.readを使うことを検討してください。
Using ref.listen to react to a provider change
ref.watchと同様に、ref.listenを使ってプロバイダーを監視することも可能である。
両者の主な違いは、リッスンされるプロバイダが変更された場合に、ウィジェット/プロバイダを再構築するのではなく、ref.listenを使用すると、代わりにカスタム関数が呼び出されることです。
これは、エラーが発生したときにスナックバーを表示するなど、特定の変更が発生したときにアクションを実行するのに便利です。
ref.listenメソッドには2つの位置引数が必要で、1つ目はProvider、2つ目は状態が変化したときに実行したいコールバック関数である。 コールバック関数が呼ばれると、2つの値が渡されます。前のStateの値と新しいStateの値です。
① ref.listenメソッドは、プロバイダー本体の内部で使うことができる:
@riverpod
void another(AnotherRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
}
② またはウィジェットのビルドメソッド内:
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
}
class HomeView extends ConsumerWidget {
const HomeView({super.key});
@override
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のライフサイクルの内部でも使用しないでください。
Using ref.read to obtain the state of a provider
ref.readメソッドは、プロバイダーの状態をリッスンせずに取得する方法である。
ref.readは、ユーザーとのインタラクションによってトリガーされる関数の内部でよく使われる。 例えば、ref.readを使って、ユーザーがボタンをクリックしたときにカウンターをインクリメントすることができます:
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state = state + 1;
}
class HomeView extends ConsumerWidget {
const HomeView({super.key});
@override
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を使ったほうがいい場合がほとんどだ。
DON’T use ref.read
inside the build method
ウィジェットのパフォーマンスを最適化するためにref.readを使いたくなるかもしれない:
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state = state + 1;
}
Widget build(BuildContext context, WidgetRef ref) {
//プロバイダの更新を無視する目的で "read" を使ってしまっている。
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: counter.increment,
child: const Text('button'),
);
}
しかし、これは非常に悪いやり方であり、追跡が困難なバグを引き起こす可能性がある。
ref.readをこのように使うことは、「プロバイダーが公開する値は決して変化しないので、『ref.read』を使えば安全だ」という考えと一般的に結びついている。 この仮定の問題点は、今日そのプロバイダーが確かに値を更新しないかもしれないが、明日も同じであるという保証はないということである。
ソフトウエアは変化しがちであり、以前は変化しなかった値が将来的に変化する必要が生じる可能性がある。 ref.readを使っていると、その値が変化する必要が生じたときに、コードベース全体を調べてref.readをref.watchに変更しなければならない。
最初からref.watchを使えば、リファクタリング時の問題は少なくなる。
しかし、ウィジェットの再構築の回数を減らすためにref.readを使いたかった。
この目標は称賛に値するが、代わりにref.watchを使ってもまったく同じ効果(ビルド回数の削減)が得られることに注意する必要がある。
プロバイダーは、リビルドの回数を減らしながら価値を得るさまざまな方法を提供しており、それを代わりに使うこともできる。
まず1つ目の方法についてお話します。例えば、以下の2つのコードを見て下さい。①は悪い例、②が好ましい例です。
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state = state + 1;
}
Widget build(BuildContext context, WidgetRef ref) {
Counter counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.increment(),
child: const Text('button'),
);
}
@riverpod
class Counter extends _$Counter {
@override
int build() => 0;
void increment() => state = state + 1;
}
Widget build(BuildContext context, WidgetRef ref) {
Counter counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.increment(),
child: const Text('button'),
);
}
どちらのスニペットも同じ効果を得ることができる。つまり、カウンターがインクリメントしてもボタンは再構築されない。
補足)
上記2つの例では、ref.readとref.watchを使用して同じ効果を得ることができると述べられていますが、これは特定の文脈での話です。具体的には、Counterの値が変更されても、その値を直接表示していないウィジェット(この場合はボタン)について言及しています。ここでの重要なポイントは、
上記のボタンはCounterの状態に基づいて動作するが、Counterの値そのものを表示または使用していないため、ボタン自体の再構築が不要である
ということです。このケースでは、ref.watchを使用してもref.readを使用しても、ボタンの再構築は起こらないと言えますが、将来的にCounterの値がUIに影響を与えるようになった場合(例えばカウンターの値を表示するようになった場合)、ref.watchを使用しておくことで、コードの変更なしに対応できるようになります。これは、ref.watchがリアクティブな依存関係を作成するため、関連するウィジェットが適切に再構築されるからです。一応、ここで、再度、両方の仕様について確認しましょう。
ref.read
ref.readは、プロバイダーから値を読み取る際にウィジェットの再構築を引き起こしません。
主にイベントハンドラー(例: ボタンのオンプレス)やライフサイクルメソッドで使われ、ウィジェットのビルドメソッド外で使用することが推奨されます。
ref.watch
ref.watchは、プロバイダーから値を読み取ると同時に、そのプロバイダーの値が変更された場合にウィジェットを再構築します。
ウィジェットがプロバイダーの値に依存している場合(例: テキスト表示)に適しています。
次に2つ目の方法をお話しします。カウンターがリセットされるケースについてです。
次の ref.refresh はアプリケーションの別の部分で呼び出すことができる:
ref.refresh(counterProvider);
//・・・もしここでref.readを使うと、ボタンは以前のCounterインスタンスを使ったままになってしまう。 一方、ref.watchを使うと、新しいCounterを使うようにボタンが正しく再構築される。
これはCounterオブジェクトを再作成します。
もしここでref.readを使うと、ボタンは以前のCounterインスタンスを使ったままになってしまう。 一方、ref.watchを使うと、新しいCounterを使うようにボタンが正しく再構築される。
Deciding what to read
リッスンしたいプロバイダーによっては、リッスン可能な値が複数あるかもしれない。
例として、以下のStreamProviderを考えてみよう:
final userProvider = StreamProvider<User>(...);
この userProvider を読み取ると、次のことができます。
- userProvider自身をリッスンすることで、現在の状態を同期的に読み込む:
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<User> user = ref.watch(userProvider);
return switch (user) {
AsyncData(:final value) => Text(value.name),
AsyncError(:final error) => const Text('Oops $error'),
_ => const CircularProgressIndicator(),
};
}
- userProvider.streamをリッスンして、関連するStreamを取得します:
Widget build(BuildContext context, WidgetRef ref) {
Stream<User> user = ref.watch(userProvider.stream);
}
- userProvider.futureをリッスンして、発行された最新の値で解決するFutureを取得する:
Widget build(BuildContext context, WidgetRef ref) {
Future<User> user = ref.watch(userProvider.future);
}
他のプロバイダーは、異なる代替値を提供するかもしれない。 詳細については、APIリファレンスを参照して、各プロバイダーのドキュメントを参照すること。
Using “select” to filter rebuilds
プロバイダーの読み込みに関連する最後の機能として、ref.watchからウィジェット/プロバイダーをリビルドする回数や、ref.listenが関数を実行する回数を減らす機能がある。
デフォルトでは、プロバイダをリッスンすると、オブジェクトの状態全体をリッスンするので、この点に注意することが重要です。 しかし、ウィジェット/プロバイダが、オブジェクト全体ではなく、いくつかのプロパティの変更だけを気にすることもあります。
例えば、プロバイダがユーザを公開するとして:
abstract class User {
String get name;
int get age;
}
しかし,ウィジェットはユーザ名しか使用しない、とします:
Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}
もし素朴にref.watchを使えば、ユーザーの年齢が変わったときにウィジェットを再構築することになる。
解決策は、select を使用して、User の name プロパティのみをリッスンしたいことを明示的に Riverpod に伝えることです。
更新されたコードは次のようになります。
Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}
select を使用すると、関心のあるプロパティを返す関数を指定できます。
ユーザーが変更されるたびに、Riverpod はこの関数を呼び出し、以前の結果と新しい結果を比較します。それらが異なる場合(名前が変更された場合など)、Riverpod はウィジェットを再構築します。
ただし、それらが等しい場合 (年齢が変わった場合など)、Riverpod はウィジェットを再構築しません。
select を ref.listen とともに使用することもできます。
select を ref.listen とともに使用することもできます。そうすることで、名前が変更された場合にのみリスナーが呼び出されます。:ref.listen<String>( userProvider.select((user) => user.name), (String? previousName, String newName) { print('The user name changed $newName'); } );
そうすることで、名前が変更された場合にのみリスナーが呼び出されます。
オブジェクトのプロパティを返す必要はありません。 == をオーバーライドする任意の値が機能します。たとえば、次のようにすることができます。
(特定のプロバイダーからデータを効率的に取り出す方法)userProviderからuserオブジェクトのnameプロパティを取り出し、その値を使って新しい文字列’Mr ${user.name}’を生成し、それをlabelとしてウォッチしています。final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));
補足) 特定のプロバイダーからデータを効率的に取り出す方法
上記では、特定のプロバイダーからデータを効率的に取り出す方法を示しています。
具体的には、userProviderからuserオブジェクトのnameプロパティを取り出し、その値を使って新しい文字列’Mr ${user.name}’を生成し、それをlabelとしてウォッチしています。.select() メソッドの利点
.select() メソッドは、プロバイダーからの全データではなく、必要な部分のデータのみを選択してリッスンすることができる非常に便利な機能です。これにより、以下のような利点があります:パフォーマンスの向上: 全体ではなく部分的なデータに対する変更のみを監視するため、ウィジェットの不要な再構築を防ぎます。これは、特に大きなデータ構造を扱う場合にパフォーマンスの向上につながります。
コードの明瞭さ: 必要なデータのみを指定することで、そのデータがどのように使われているかがより明確になり、コードの可読性が向上します。
ケーススタディとしての利用
提示されたコードは、たとえばユーザーインターフェースでユーザーの名前に基づく敬称を動的に表示したい場合に非常に有用です。user.nameの値が変わった時だけlabelが更新されるため、無関係なデータの変更による影響を受けません。
Combining Provider States
Combining Provider States
プロバイダー状態の組み合わせ
簡単なプロバイダーを作成する方法を以前に見てきました。しかし実際には、多くの状況でプロバイダーは別のプロバイダーの状態を読み取りたいと考えます。
そのためには、プロバイダーのコールバックに渡されたrefオブジェクトを使い、そのウォッチ・メソッドを使えばいい。
例として、次のプロバイダーについて考えてみましょう。
@riverpod
String city(CityRef ref) => 'London';
これで、cityProvider を使用する別のプロバイダーを作成できます。
@riverpod
Future<Weather> weather(WeatherRef ref) {
final city = ref.watch(cityProvider);
return fetchWeather(city: city);
}
それでおしまい。別のプロバイダーに依存するプロバイダーを作成しました。
FAQ
リッスンされている値が時間の経過とともに変化したらどうなるでしょうか?
リスニングしているプロバイダーによっては、取得された値が時間の経過とともに変化する場合があります。たとえば、NotifierProvider をリスニングしている場合や、リスニングされているプロバイダーが ProviderContainer.refresh を使用して強制的に更新されている場合があります。
watch を使用すると、Riverpod はリッスンされている値が変更されたことを検出でき、必要に応じてプロバイダーの作成コールバックを自動的に再実行します。
これは計算された状態に役立ちます。
たとえば、todo リストを公開する (Async)NotifierProvider について考えてみましょう。
@riverpod
class TodoList extends _$TodoList {
@override
List<Todo> build() {
return [];
}
}
一般的な使用例は、UI で Todo のリストをフィルターして、完了/未完了の Todo のみを表示することです。
このようなシナリオを実装する簡単な方法は次のとおりです。
- 現在選択されているフィルター メソッドを公開する StateProvider を作成します
enum Filter {
none,
completed,
uncompleted,
}
final filterProvider = StateProvider((ref) => Filter.none);
- フィルターメソッドと todo リストを組み合わせて、フィルターされた todo リストを公開する別のプロバイダーを作成します。
@riverpod
List<Todo> filteredTodoList(FilteredTodoListRef ref) {
final filter = ref.watch(filterProvider);
final todos = ref.watch(todoListProvider);
switch (filter) {
case Filter.none:
return todos;
case Filter.completed:
return todos.where((todo) => todo.completed).toList();
case Filter.uncompleted:
return todos.where((todo) => !todo.completed).toList();
}
}
//次に、UI は filteredTodoListProvider をリッスンして、フィルターされた todo リストをリッスンできます。 このようなアプローチを使用すると、フィルターまたは ToDo リストが変更されたときに UI が自動的に更新されます。
次に、UI は filteredTodoListProvider をリッスンして、フィルターされた todo リストをリッスンできます。 このようなアプローチを使用すると、フィルターまたは ToDo リストが変更されたときに UI が自動的に更新されます。
このアプローチの実際の動作を確認するには、Todo リストのサンプルのソース コードを参照してください。
INFO)
上記事例と同様に、他の全てのプロバイダーについても利用可能です。
例えば、watch と FutureProvider の場合でも、以下の様にライブ構成の変更をサポートする検索機能を実装できます。searchプロバイダとconfigsプロバイダ(@riverpod利用)をwatch監視して、Futureプロバイダを作成(@riverpod利用):final searchProvider = StateProvider((ref) => ''); @riverpod Stream<Configuration> configs(ConfigsRef ref) { return Stream.value(Configuration()); } @riverpod Future<List<Character>> characters(CharactersRef ref) async { final search = ref.watch(searchProvider); final configs = await ref.watch(configsProvider.future); final response = await dio.get<List<Map<String, dynamic>>>( '${configs.host}/characters?search=$search'); return response.data!.map(Character.fromJson).toList(); }
このコードはサービスから文字のリストをフェッチし、構成が変更されるたび、または検索クエリが変更されるたびにリストを自動的に再フェッチしています。
プロバイダをリッスンせずに読み取ることはできますか?
この質問における文献の内容は、Riverpodのreadとwatchメソッドに関する重要な考察を提供しており、特にプロバイダからのデータ読み取りの文脈において、いつどちらのメソッドを使うべきかについて説明しています。
以下は、その説明とサンプルコードの翻訳と補足、そして具体的な解説です。
プロバイダの値を取得した後、例えその取得元のデータが更新された場合においても、最初に取得した値を、そのままにしておきたい場合があります。例えば認証用ユーザートークン情報です。(恐らく、ここで想定している状況は、1度ログインしたら、次にログアウトするかアプリを終了するまでは、その利用者は継続利用できる等のアプリを前提としていると思うのですが、それを前提とすると)ref.watch による監視は(すなわち、認証用ユーザートークン情報の更新を知る必要がないので)無用です。
故に(上記の様な場合には)ref.watch ではなく ref.read を使用します。
ref.read なら(プロバイダの値を最初に1度だけ取得するのみなので)、監視対象が変更されても、最初に取得した値のままで利用できます。
その為には、(以下の例)対象オブジェクトに対して、プロバイダ経由で ref を渡します。
これにより、そのオブジェクトはいつでもプロバイダを監視して値を取得できます。
final userTokenProvider = StateProvider<String>((ref) => null);
final repositoryProvider = Provider(Repository.new);
class Repository {
Repository(this.ref);
final Ref ref;
Future<Catalog> fetchCatalog() async {
String token = ref.read(userTokenProvider);
final response = await dio.get('/path', queryParameters: {
'token': token,
});
return Catalog.fromJson(response.data);
}
}
補足) ref.read について
ref.read を使ってプロバイダから値を取得する場合、その時点でのプロバイダの値を読み取ることができますが、その後プロバイダの値が更新されても、ref.read で取得した値は自動的には更新されません。つまり、ref.read はプロバイダの値をリアクティブに監視しません(スナップショット取得(つまり、非リアクティブ))。
(プロバイダの値が更新された後の値を取得するには、再び ref.read を呼び出して新しい値を読み取る必要があります。)ref.read の動作説明)
1. スナップショット取得:
ref.read は、呼び出された時点でのプロバイダの値を取得します。
これはスナップショットとして、その時点でのデータを反映します。
2. 非リアクティブ:
一度 ref.read で値を読み取った後、その値はプロバイダの値が更新されても変更されません。
再度最新の値を取得するためには、ref.read を再び実行する必要があります。
3.更新通知なし:
ref.read は、プロバイダの値の変更を監視または通知しません。
値の変更を監視するには、ref.watch を使用する必要があります。(例えば、ユーザー情報を管理するプロバイダがあるとします。以下のように ref.read と ref.watch を使用するシナリオを考えてみます。)この例では、ref.read を使って最初にユーザー情報を取得し、その後プロバイダの値を更新しています。更新後の値を確認するためには、再度 ref.read を実行しています。これは、ref.read が値を自動で更新しないためです。:final userProvider = StateProvider<String>((ref) => "初期ユーザー"); void updateUser(WidgetRef ref) { //userProviderの値を読み取り String currentUser = ref.read(userProvider); print("現在のユーザー: $currentUser"); //"初期ユーザー" //userProviderの値を更新 ref.read(userProvider.notifier).state = "新しいユーザー"; //再度読み取り currentUser = ref.read(userProvider); print("更新後のユーザー: $currentUser"); //"新しいユーザー" } //ref.read は、プロバイダからデータを取得するための非リアクティブな方法です。プロバイダの値が変更されたときに自動で最新の値に更新されるわけではありません。プロバイダの値の変更をリアクティブに追跡したい場合は、ref.watch を使用する必要があります。
DON’T call read inside the body of a provider
プロバイダの内側で、ref.read を使わないで下さい
プロバイダの内側で、ref.read を使わないで下さい:@riverpod MyValue my(MyRef ref) { // Bad practice to call `read` here final value = ref.read(anotherProvider); return value; }
オブジェクトの不要な再構築を回避するために読み取りを使用した場合は、別FAQの「プロバイダーの更新が頻繁すぎるのですが、どうすればよいですか?」を参照してください。
コンストラクターのパラメーターとして ref を受け取るオブジェクトをテストするにはどうすればよいですか?
「リスニングせずにプロバイダーを読み取ることができますか?」で説明されているパターンを使用している場合は、オブジェクトのテストをどのように作成するか疑問に思うかもしれません。
このシナリオでは、生のオブジェクトではなくプロバイダを直接テストすることを検討してください。
これを行うには、ProviderContainer クラスを使用します。
//テスト対象のプロバイダ↓
final repositoryProvider = Provider((ref) => Repository(ref));
//上記プロバイダのテスト用メソッド(test):
test('fetches catalog', () async {
//ProviderContainerの生成:
//ProviderContainerは、テスト環境でプロバイダーを管理するためのコンテナです。
//(アプリケーション本体とは独立した環境でプロバイダーの挙動をテストできます)
final container = ProviderContainer();
//addTearDown登録:
//addTearDownはテストフレームワークにおいて、
//テストの終了後に特定のクリーンアップ処理を実行するために使用されます。
//テストが終了する際(正常終了でも異常終了でも)、登録された関数
//(この場合はcontainer.dispose)が呼び出されます。これは、
//使用したリソースを適切に解放するために重要です。例えば、
//ProviderContainerのインスタンスを破棄することで、
//テスト中に作成されたすべてのプロバイダーや状態をクリーンアップします。
addTearDown(container.dispose);
//テスト対象オブジェクト(Repository型)の取得:
Repository repository = container.read(repositoryProvider);
await expectLater(
repository.fetchCatalog(),
//completionマッチャー登録:
//completionは、非同期処理の結果をテストする際に使用されるマッチャーです。
//expectLaterと共に使用され、非同期処理完了時の期待される結果を指定します。
//このケースでは、repository.fetchCatalog()の呼び出しの結果がCatalog()
//インスタンスと一致することを期待しています。
completion(Catalog()),
);
});
補足) Flutter/Dartテスト資料:
Testing Flutter apps : https://docs.flutter.dev/testing/overview
An introduction to unit testing : https://docs.flutter.dev/cookbook/testing/unit/introduction
Mock dependencies using Mockito : https://docs.flutter.dev/cookbook/testing/unit/mocking
プロバイダーの更新が頻繁すぎるのですが、どうすればよいですか?
オブジェクトが頻繁に再作成される場合、プロバイダーは関係のないオブジェクトをリッスンしている可能性があります。
たとえば、Configuration オブジェクトをリッスンしているが、 host プロパティしか使用していない場合などです。
Configuration オブジェクト全体をリッスンすると、 host 以外のプロパティが変更された場合にも、プロバイダが(必要以上に)再評価されてしまいます。
この問題の解決策は、構成 (つまりホスト) で必要なものだけを公開する別のプロバイダーを作成することです。
オブジェクト全体をリッスンすることは避けてください。
@riverpod
Stream<Configuration> config(ConfigRef ref) => Stream.value(Configuration());
@riverpod
Future<List<Product>> products(ProductsRef ref) async {
//構成に変更があった場合、productsProvider が製品を再フェッチします。
final configs = await ref.watch(configProvider.future);
final result =
await dio.get<List<Map<String, dynamic>>>('${configs.host}/products');
return result.data!.map(Product.fromJson).toList();
}
ブジェクトのプロパティが 1 つだけ必要な場合は、select を使用することをお勧めします。
@riverpod
Stream<Configuration> config(ConfigRef ref) => Stream.value(Configuration());
@riverpod
Future<List<Product>> products(ProductsRef ref) async {
//ホストのみを聞きます。構成内の何かが変更された場合でも、
//プロバイダーが無意味に再評価されることはありません。
final host = await ref.watch(configProvider.selectAsync((config) => config.host));
final result = await dio.get<List<Map<String, dynamic>>>('$host/products');
return result.data!.map(Product.fromJson).toList();
}
これにより、ホストが変更された場合にのみ productsProvider が再構築されます。
.family
このパートでは、.family プロバイダー修飾子について詳しく説明します。
.family 修飾子の目的は、外部パラメータに基づいて一意のプロバイダを取得することです。
.family 修飾子を使用するケースは次の通りです。
- FutureProviderと.familyを組み合わせてIDからMessageを取得する
- 翻訳を処理できるように、現在のロケールをプロバイダーに渡します。
Usage
.family修飾子を付けてプロバイダを作成すると、パラメータが追加されます。
このパラメータは、プロバイダのステート(状態)を計算する要素として使用できます。
例えば、.family と FutureProvider と組み合わせることで、メッセージID に紐づく Message を取得することができます。:
final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
return dio.get('http://my_api.dev/messages/$id');
});
Familyプロバイダをウィジェットなどで使用する際の構文は通常と若干異なります。
次の構文ではコンパイルエラーが出ます:
Widget build(BuildContext context, WidgetRef ref) {
//(エラー発生)今までの構文はもう機能しません:
final response = ref.watch(messagesFamily);
}
代わりに、次の様に、messagesFamilyに引数を渡して下さい:
Widget build(BuildContext context, WidgetRef ref) {
final response = ref.watch(messagesFamily('id'));
}
.family修飾子を使用するプロバイダに、異なる引数を同時に渡すことも可能です。
例えば、titleFamilyプロバイダに異なるロケール情報を渡し、フランス語訳と英語訳を同時に取得することができます:異なるパラメータを持つファミリーを同時に使用することも可能です。 例えば、titleFamilyを使えば、フランス語訳と英語訳を同時に読むことができます:@override Widget build(BuildContext context, WidgetRef ref) { final frenchTitle = ref.watch(titleFamily(const Locale('fr'))); final englishTitle = ref.watch(titleFamily(const Locale('en'))); return Text('fr: $frenchTitle en: $englishTitle'); }
Parameter restrictions
familyプロバイダを正常に動作させるには、引数として渡すオブジェクトに等価性が定義されている必要があります。
次のいずれかが理想的です。
- プリミティブ (bool/int/double/String)、
- 定数 (プロバイダー)、
- == および hashCode をオーバーライドした不変オブジェクト
【重要】 オブジェクトが一定ではない場合は autoDispose 修飾子との併用が望ましい
family を使って検索フィールドの入力値をプロバイダに渡す場合、その入力値は頻繁に変わる上に同じ値が再利用されることはありません。 おまけにプロバイダは参照されなくなっても破棄されないのがデフォルトの動作であるため、この場合はメモリリークにつながります。
こうしたメモリリークは .family と .autoDispose 修飾子を併用することで避けることができます。
final characters = FutureProvider.autoDispose.family<List<Character>, String>((ref, filter) async {
return fetchCharacters(filter: filter);
});
Passing multiple parameters to a family
family プロバイダに複数のパラメータを渡す
.family 修飾子はプロバイダに複数のオブジェクトを渡すことができません。
しかし、前述した制限を満たしている限りは どのような オブジェクトでも渡すことができます。
これを利用して、例えば以下のオブジェクトを渡すことでプロバイダに複数のパラメータを間接的に渡すことができます。
- Freezed もしくは built_value パッケージで生成されたオブジェクト
- equatable を使用したオブジェクト
- tuble パッケージのタプルオブジェクト
@freezed
abstract class MyParameter with _$MyParameter {
factory MyParameter({
required int userId,
required Locale locale,
}) = _MyParameter;
}
//Something型のプロバイダ(MyParameter型の引数を受取る)を生成
final exampleProvider = Provider.autoDispose.family<Something, MyParameter>((ref, myParameter) {
print(myParameter.userId);
print(myParameter.locale);
//userId と locale を使って処理できる!
});
@override
Widget build(BuildContext context, WidgetRef ref) {
int userId; //どこかで取得する予定のユーザ ID
final locale = Localizations.localeOf(context);
final something = ref.watch(
exampleProvider(MyParameter(userId: userId, locale: locale)),
);
...
}
Provider.autoDispose.family: Something型の値を生成するプロバイダーを定義しており、MyParameter型のパラメータを受け取ります。.autoDisposeはプロバイダーがもはや参照されない場合に自動的にリソースを解放することを意味します。.familyはパラメータに応じて異なる値を提供するプロバイダーを生成します。
print(myParameter.userId); と print(myParameter.locale);: 渡されたMyParameterからuserIdとlocaleを取り出し、コンソールに出力します。
class MyParameter extends Equatable {
MyParameter({
required this.userId,
required this.locale,
});
final int userId;
final Locale locale;
@override
List<Object> get props => [userId, locale];
}
final exampleProvider = Provider.family<Something, MyParameter>((ref, myParameter) {
print(myParameter.userId);
print(myParameter.locale);
//userId と locale を使って何かする
});
@override
Widget build(BuildContext context, WidgetRef ref) {
int userId; //ユーザ ID をどこかで取得する
final locale = Localizations.localeOf(context);
final something = ref.watch(
exampleProvider(MyParameter(userId: userId, locale: locale)),
);
...
}
exampleProviderはProvider.familyを使用して定義されています。これにより、MyParameter オブジェクトをパラメータとして受け取り、そのパラメータに基づいてSomethingタイプのデータを提供するプロバイダーが生成されます。
プロバイダーの本体内では、myParameter.userId と myParameter.locale を使ってログを出力し、その後何か処理を行います(具体的な処理は示されていませんが、ユーザーIDとロケール情報に基づいたデータの取得や加工などが考えられます)。
.autoDispose
多くの場合、参照されなくなったプロバイダのステート(状態)は破棄することが望ましいはずです。 破棄する理由は様々ですが、例えば次のようなケースが考えられます。
- Firebase 使用時に、サービスとの接続を切って不必要な負荷を避けるため。
- ユーザが別の画面に遷移してまた戻って来る際に、ステートをリセットしてデータ取得をやり直すため。
Riverpod はこのようなケースにも .autoDispose 修飾子を使うことで対応することが可能です。
Usage
プロバイダ作成時に次のように .autoDispose 修飾子を付け加えてください。
final userProvider = StreamProvider.autoDispose<User>((ref) {
});
これで userProvider が参照されなくなった際に、ステートが自動的に破棄されるようになります。
ちなみに、ジェネリクスの型引数が .autoDispose の前ではなく後にあることから分かる通り、 autoDispose
は名前付きコンストラタではありません。
.autoDispose は他の修飾子と組み合わせることもできます。
final userProvider = StreamProvider.autoDispose.family<User, String>((ref, id) {
});
ref.keepAlive
プロバイダに .autoDispose
修飾子を付けると、ref
オブジェクトに keepAlive
というメソッドが追加されます。
この keepAlive
メソッドを実行することで、プロバイダが参照されなくなった際にもステートを維持するよう Riverpod に伝えることができます。
例えば、次のコードの通り HTTP リクエスト完了後に keepAlive
を実行するとします。
final myProvider = FutureProvider.autoDispose((ref) async {
final response = await httpClient.get(...);
ref.keepAlive();
return response;
});
すると、リクエスト完了前に画面を破棄して再度同じ画面に戻った場合は HTTP リクエストが再実行される一方、 リクエスト完了後に同じ動作を行った場合はステートが維持されるため、HTTP リクエストが再実行されることはありません。
使用例: プロバイダが参照されなくなったタイミングで HTTP リクエストをキャンセルする
.autoDispose 修飾子と FutureProvider、そして ref.onDispose を組み合わせて、 プロバイダが参照されなくなったタイミング(プロバイダのステートを監視するオブジェクトがなくなったタイミング)で HTTP リクエストをキャンセルすることができます。
実装したい動作は次の3点です。
- ユーザが画面に入ったら、HTTP リクエストを開始
- リクエスト完了前に画面を離れてステートが破棄されたら、HTTP リクエストをキャンセル
- リクエストが成功したら状態を維持し、再び同じ画面に入っても新たなリクエストが開始されないようにする
コードは以下の通りです。
final myProvider = FutureProvider.autoDispose((ref) async {
//httpリクエストのキャンセルを実行するための package:dio のオブジェクト
final cancelToken = CancelToken();
//プロバイダのステートが破棄されたら http リクエストをキャンセル
ref.onDispose(() => cancelToken.cancel());
//データを取得しつつキャンセル用の `cancelToken`を渡す
final response = await dio.get('path', cancelToken: cancelToken);
//リクエストが成功したらステートを維持する
ref.keepAlive();
return response;
});
上記コードは、Flutterのriverpodライブラリを用いて非同期のHTTPリクエストを管理する例です。FutureProvider.autoDisposeを使用して、特定のHTTPリクエストのライフサイクルを効率的に管理しています。
FutureProvider.autoDispose
FutureProvider.autoDispose: このプロバイダは非同期の操作を行い、プロバイダの使用が終了すると自動的にリソースを解放します。これは特に非同期操作がメモリや他のリソースを消費する場合に有用です。
CancelTokenの使用
CancelToken: package:dioに含まれるCancelTokenオブジェクトは、HTTPリクエストのキャンセルを管理するために使用されます。リクエストが不要になった場合やユーザーが画面を離れた場合に、進行中のリクエストをキャンセルしてリソースを節約することができます。
リクエストのキャンセル処理
ref.onDispose(() => cancelToken.cancel());: ref.onDisposeは、プロバイダが不要になり破棄されるときに実行されるクリーンアップ関数を登録します。ここでは、プロバイダが破棄された際にHTTPリクエストをキャンセルするよう設定されています。
データの取得とキャンセルトークンの渡し
await dio.get(‘path’, cancelToken: cancelToken);: 非同期にデータを取得しながら、cancelTokenを渡してリクエストがキャンセル可能であることを保証します。これにより、リクエストの途中で必要がなくなった場合にすぐに停止できます。
リクエスト成功時のステート維持
ref.keepAlive();: この行は、HTTPリクエストが成功しデータが取得された後、プロバイダが自動的に破棄されないように設定します。これにより、プロバイダがアクティブな間はデータが保持され、再利用が可能になります。
エラー:The argument type ‘AutoDisposeProvider’ can’t be assigned to the parameter type ‘AlwaysAliveProviderBase’
.autoDispose を使用していると、次のようなコンパイル時エラーに出くわすことがあるかもしれません。
The argument type ‘AutoDisposeProvider’ can’t be assigned to the parameter type ‘AlwaysAliveProviderBase’
(日本語訳: 引数型 ‘AutoDisposeProvider’ はパラメータ型 ‘AlwaysAliveProviderBase’ にアサインできません)
心配は無用です! これは書いたコードに起因するエラーのため、修正可能です。
原因は .autoDispose 修飾子付きのプロバイダを、そうではないプロバイダ内で利用したためです。 例えば…
final firstProvider = Provider.autoDispose((ref) => 0);
final secondProvider = Provider((ref) {
// エラー:The argument type 'AutoDisposeProvider<int>' can't be assigned to
// the parameter type 'AlwaysAliveProviderBase<Object, Null>'
ref.watch(firstProvider);
});
この場合 firstProvider が破棄されることはありません。
エラーを解消するには、secondProvider も同様に .autoDispose 修飾子を付ける必要があります。
final firstProvider = Provider.autoDispose((ref) => 0);
final secondProvider = Provider.autoDispose((ref) {
ref.watch(firstProvider);
});
ProviderObserver
ProviderObserver は ProviderContainer 内で起こる変化を監視します。
ProviderObserver クラスを継承するクラスを定義し、使用したいメソッドをオーバーライドして使用してください。
ProviderObserver には3つのメソッドがあります。
didAddProvider はプロバイダが初期化されるたびに呼び出されます。公開される値は value パラメータで利用できます。
didDisposeProvider はプロバイダが破棄されるたびに呼び出されます。
didUpdateProvider はプロバイダが変更通知を送信するたびに呼び出されます。
Usage
例えば didUpdateProvider メソッドをオーバーライドして、プロバイダのステート変化をログに残すという用途にも使用することができます。
// Riverpod を使用した Logger 付きのカウンターアプリの例
class Logger extends ProviderObserver {
@override
void didUpdateProvider(
ProviderBase<Object?> provider,
Object? previousValue,
Object? newValue,
ProviderContainer container,
) {
print('''
{
"provider": "${provider.name ?? provider.runtimeType}",
"newValue": "$newValue"
}''');
}
}
void main() {
runApp(
//ProviderScope を置くことで Riverpod が有効になる
//Logger インスタンスを observers のリストに追加する
ProviderScope(observers: [Logger()], child: const MyApp()),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(home: Home());
}
}
final counterProvider = StateProvider((ref) => 0, name: 'counter');
class Home extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: const Text('Counter example')),
body: Center(
child: Text('$count'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Icon(Icons.add),
),
);
}
}
これでプロバイダのステートが変化するたびに、Logger がログを取ってくれるようになりました。
I/flutter (16783): {
I/flutter (16783): "provider": "counter",
I/flutter (16783): "newValue": "1"
I/flutter (16783): }
StateController (StateProvider.state のステート)や ChangeNotifier のようにステートがミュータブル(可変)の場合は、 previousValue と newValue の値が変わることはありません。 それぞれ同じ StateController / ChangeNotifier を参照しているためです。
Scopes
Riverpod のスコープ設定は非常に強力な機能ですが、他の強力な機能と同様に、賢明かつ意図的に使用する必要があります。
スコープによって可能になること↓
- 特定のサブツリーのプロバイダの状態をオーバーライドします。(Flutterでテーマ設定とInheritedWidgetの機能に似ています)
- 非同期APIの同期プロバイダ作成
- ダイアログとオーバーレイが、表示されるウィジェットのサブツリーからプロバイダーの状態を継承できるようにします。
- ウィジェット コンストラクターからパラメーターを削除してウィジェットの再構築を最適化し、コンストラクターを const にできるようにします。
スコープを使いたい場合は、ファミリーを使うことができます。 ファミリーには、今いる特定のサブツリーにスコープされたステートだけでなく、ウィジェットツリーのどこからでもステートの各インスタンスにアクセスできるという利点があります。
スコープを使用してプロバイダーの状態の複数のインスタンスを作成することは、package:provider の仕組みと似ています。
ただし、スコープを使用してそのタスクを実行すると、そのスコープから他のインスタンスにアクセスするかどうかを決定できないため、より制限が厳しくなります。
したがって、使用するすべてのプロバイダーをスコープする前に、プロバイダーをスコープする理由を慎重に検討してください。
ProviderScope and ProviderContainer
スコープは ProviderContainer によって導入されます。このコンテナには、すべてのプロバイダーの現在の状態が保持されます。プロバイダー間のルックアップとサブスクリプションを管理します。
Flutter では、ProviderScope ウィジェットを使用する必要があります。これは内部に ProviderContainer を含み、ウィジェット ツリーの残りの部分にそのコンテナにアクセスする方法を提供します。
final valueProvider = StateProvider((ref) => 0);
//正しい使用方法:
void main() {
runApp(ProviderScope(child: MyApp()));
}
//間違った使用方法:
final myProviderContainer = ProviderContainer();
void main(){
runApp(MyApp());
}
ProviderContainerがどのように動作するかを理解せずに、複数のProviderContainerを使用しないでください。
それぞれのProviderContainerは、それぞれ独立したスレッドの状態を持ち、互いにアクセスすることはできません。
テストは、各テストの状態を他のテストから独立させるために、別々の ProviderContainer を使用したい場合の例です。
ProviderScope なしで ProviderContainer を作成するのは、テストおよび Dart のみの使用の場合のみです。
How Riverpod Finds a Provider
ウィジェットまたはプロバイダがプロバイダの値を要求すると、Riverpodは最も近いProviderScopeウィジェットでそのプロバイダの状態を検索します。 プロバイダまたは明示的にリストされた依存関係のいずれかが、そのスコープでオーバーライドされていない場合、Riverpodはウィジェットツリーでの検索を続行します。 プロバイダがどの Widget サブツリーでもオーバーライドされていない場合、ルックアップのデフォルトは、ルート ProviderScope 内の ProviderContainer になります。 この処理でプロバイダが存在するはずのスコープが見つかると、プロバイダがまだ作成されていないかどうかを判断します。 もしそうなら、プロバイダの状態を返します。 ただし、プロバイダが無効になっていたり、現在初期化されていない場合は、プロバイダのビルド・メソッドを使用して状態を作成します。
Initialization of Synchronous Provider for Async APIs
非同期 API の同期プロバイダーの初期化
多くの場合、SharedPreferencesやFirebaseAppのような依存関係の非同期初期化があるかもしれない。 他の多くのプロバイダがこれに依存している可能性があり、それぞれのプロバイダでエラーやロードの状態を扱うのは冗長です。 これらのプロバイダがエラーを起こさず、アプリの起動時に素早くロードされることを保証できるかもしれません。 では、このようなプロバイダの状態を同期的に利用できるようにするにはどうすればよいのでしょうか? ここでは、非同期APIの準備ができたときに、スコープによってダミーのプロバイダをオーバーライドする方法を示す例を示します。
//プロバイダーで共有設定のインスタンスを同期的に取得したいと考えています
final countProvider = StateProvider<int>((ref) {
final preferences = ref.watch(sharedPreferencesProvider);
final currentValue = preferences.getInt('count') ?? 0;
ref.listenSelf((prev, curr) {
preferences.setInt('count', curr);
});
return currentValue;
});
//SharedPreferences の実際のインスタンスはなく、非同期以外での取得はできません。
final sharedPreferencesProvider =
Provider<SharedPreferences>((ref) => throw UnimplementedError());
Future<void> main() async {
//アプリ全体を実行する前に読み込みインジケーターを表示する。
//(これを省略すると待機中にプラットフォームのロード画面が使用されます)
runApp(const LoadingScreen());
//共有設定のインスタンスを取得する
final prefs = await SharedPreferences.getInstance();
return runApp(
ProviderScope(
overrides: [
//プラグインから取得した値で未実装のプロバイダーをオーバーライドします。
sharedPreferencesProvider.overrideWithValue(prefs),
],
child: const MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
//非同期の問題に対処せずにプロバイダーを使用する
final count = ref.watch(countProvider);
return Text('$count');
}
}
Subtree Scoping
スコープを使うと、ウィジェットツリーの特定のサブツリーに対してプロバイダの状態をオーバーライドできます。 例えば、flutter では、ウィジェットツリーの特定のサブツリーの Theme を Theme ウィジェットでラップしてオーバーライドできます。
void main() {
runApp(
ProviderScope(
child: MaterialApp(
theme: ThemeData(primaryColor: Colors.blue),
home: const Home(),
),
),
);
}
// Have a counter that is being incremented
final counterProvider = StateProvider(
(ref) => 0,
);
class Home extends ConsumerWidget {
const Home({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Column(
children: [
// This counter will have a primary color of green
Theme(
data:
Theme.of(context).copyWith(primaryColor: Colors.green),
child:
const CounterDisplay(),
),
// This counter will have a primary color of blue
const CounterDisplay(),
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).state++;
},
child: const Text('Increment Count'),
),
],
)
);
}
}
class CounterDisplay extends ConsumerWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
final theme = Theme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$count',
style: theme.textTheme.displayMedium
?.copyWith(color: theme.primaryColor),
),
],
);
}
}
内部では、テーマは InheritedWidget であり、ウィジェットがテーマを検索すると、ウィジェット ツリー内でその上にある最も近いテーマ ウィジェットからテーマを取得します。
Riverpodは、通常、アプリケーションのすべての状態がルートProviderScopeウィジェットに格納されるため、動作が異なります。 これは、状態が変更されたときにアプリケーション全体が再構築されるわけではなく、ウィジェットツリーのどこからでも状態にアクセスできるようにするだけですので、ご安心ください。 どのページにいるかによって異なるプロバイダが必要な場合はどうすればよいですか? 最初に考慮すべきことは、提供される動作が何らかの形で異なるかどうかです。 そうであれば、別の名前で新しいプロバイダを作成し、そのページで使用します。
もしその場合 – > 別の名前で新しいプロバイダーを作成し、そのページでそれを使用します。
そうでない場合 -> .family の使用を検討してください。
ある特定のページでプロバイダーが必要なだけだと考えて始めても、後になって別のページで使いたくなることはよくあることだ。 ファミリーはこのような事態からあなたを守るものであり、パッケージ:プロバイダーから来た場合、あなたの考え方をどのように調整すべきかの大きな違いである。
ファミリが実際のユースケースに適合しない場合、次の例は、特定のサブツリーのプロバイダーをオーバーライドする方法を示しています。
//カウンタ([CounterDisplay]のボタンでカウントアップされる)
final counterProvider = StateProvider(
(ref) => 0,
);
final adjustedCountProvider = Provider(
(ref) => ref.watch(counterProvider) * 2,
//もしサブツリーに対してオーバーライドされるプロバイダに依存している場合、
//この依存関係リストに明示的に登録する必要があります。
dependencies: [counterProvider],
);
class Home extends ConsumerWidget {
const Home({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Column(
children: [
ProviderScope(
//もしサブツリーで複製が欲しい場合は、それを指定するだけです。
//
//[adjustedCountProvider]の様な依存プロバイダも複製できます。
//もし動作が好ましくない場合は、.familyの利用を検討して下さい。
overrides: [counterProvider],
child: const CounterDisplay(),
),
ProviderScope(
//サブツリーでの動作は変更できます。
overrides: [counterProvider.overrideWith((ref) => 1)],
child: const CounterDisplay(),
),
ProviderScope(
overrides: [
counterProvider,
//依存元プロバイダの動作も変更できます。
adjustedCountProvider.overrideWith(
(ref) => ref.watch(counterProvider) * 3,
),
],
child: const CounterDisplay(),
),
//ここの表示では、ルートProviderScopeからのプロバイダ状態が使用されます。
const CounterDisplay(),
],
)
);
}
}
class CounterDisplay extends ConsumerWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('$count'),
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).state++;
},
child: const Text('Increment Count'),
),
],
);
}
}
このコード例では、ProviderScopeが3つ使用されているのは、異なる部分木(subtree)でcounterProviderとその依存するadjustedCountProviderの振る舞いを変更するためです。これにより、同一のプロバイダーを異なるコンテキストや条件で使用することが可能になり、各CounterDisplayウィジェットが異なる挙動を示すようになっています。
各ProviderScopeの役割)
・最初のProviderScope:
このスコープはcounterProviderをそのまま子ウィジェットに提供しています。ここでは特定のオーバーライドは指定されておらず、このスコープ内のCounterDisplayは親スコープ(またはグローバルスコープ)からcounterProviderの状態を受け継ぎます。
・二番目のProviderScope:
ここではcounterProviderの挙動を変更しており、counterProvider.overrideWith((ref) => 1)を使用して常に1を返すようにしています。この変更により、このスコープ内のCounterDisplayはカウンターの値として常に1を表示します。これは例えば、特定の状況でカウンターをリセットする場合に便利です。
・三番目のProviderScope:
このスコープではcounterProviderとadjustedCountProviderの両方をオーバーライドしています。adjustedCountProviderはcounterProviderの値に依存しており、ここではその倍率を変更しています(元々は2倍だったものを3倍に変更)。これにより、このスコープ内のCounterDisplayはカウンターの値を3倍した値を表示します。
ProviderScopeの利点と目的)
カプセル化と再利用: ProviderScopeを使用することで、特定のウィジェットツリー内でのみ有効な状態変更をカプセル化できます。これにより、同じプロバイダーを異なる設定で再利用することが可能になります。
状態の局所性: 状態をグローバルではなく、局所的に管理することで、ウィジェットツリーの異なる部分が互いに影響を与えることなく、独立して振る舞うことができます。
柔軟な状態管理: 状態の変更が必要な場合に、その変更を局所的に適用することができ、大規模な状態変更の影響を最小限に抑えることができます。
このアプローチは、大規模なアプリケーションや複雑な状態管理が必要なアプリケーションで特に有効であり、各ウィジェットが独自の状態ビューを持つことを可能にします。
When to choose Scoped Providers or Families
スコープ指定されたプロバイダーまたはファミリーを選択する場合
スコープを理解することは重要ですが、スコープを使用するときに夢中になりがちです。 プロバイダの状態がウィジェットツリーのどこにあるかによって異なるインスタンスが必要な場合、いくつかの選択肢があります: Scoping、.Families、またはその組み合わせです。 適切な選択はユースケースによります。
- .Families:
- 長所: どのサブツリーに属していても、複数の状態を表示できる
- 長所: これにより、多くのユースケースに対して、より柔軟でスケーラブルなソリューションになります。
- Scoping:
- 短所: ウィジェット ツリー内で ProviderScope ウィジェットのネストがさらに増えることになります。
- 短所: ウィジェット ツリーのセクション内の 1 つのオーバーライドにしかアクセスできません。
- 短所: 最終的には、ほとんどのプロバイダーの依存関係を明示的にリストする必要があります。
- 長所: ウィジェット コンストラクターのパラメーターの数を減らすことができます。
- 長所: パフォーマンスがわずかに向上し、ウィジェット コンストラクターの一部を const にすることができる可能性があります。
2 つのアプローチを組み合わせて使用すると、両方のアプローチの長所を得ることができますが、それでもスコープ設定の短所に対処する必要があります。
注意)
スコープは、オーバーライドされたプロバイダや、オーバーライドされたプロバイダへの依存関係を持つプロバイダの状態の新しいインスタンスを導入することを覚えておいてください。 アプリの別のサブツリーで同じパラメータでオーバーライドした場合、それはプロバイダの状態の同じインスタンスにはなりません。 ファミリは一般的に柔軟性が高く、今後のコード生成機能を使えば、ファミリに複数のパラメータを使用することも簡単です。 多くの場合、ファミリーとスコープの両方を使うのが良い組み合わせです。 ファミリを使用して、アプリのどこにいても状態の一部への一般的なアクセスを提供し、スコープを使用して、ウィジェットツリーのどこにいるかによってファミリの状態の特定のインスタンスを提供します。
Less common usages of Scopes
あまり一般的ではないスコープの使用法
アプリの特定のサブツリーで、プロバイダーのセット全体をオーバーライドしたい場合があります。 各プロバイダの依存関係リストに共通のプロバイダをリストしておけば、共通のプロバイダをオーバーライドすることで、すべてのプロバイダの新しい状態を一度に簡単に作成できます。
このためにファミリを使おうとすると、同じパラメータを持つファミリがたくさんできてしまい、ウィジェットツリーのあちこちにそのパラメータを渡してしまうことになるので注意してください。 この場合、スコープを使用することもできます。
注意)
Scopeを使い始めたら、実行時の例外を防ぐために、依存関係を常にリストアップし、最新の状態に保つようにしてください。 これを支援するために、riverpod_lintを作成しました。これは、依存関係が欠落している場合に警告してくれます。 さらにriverpod_generatorを使うと、コードジェネレータが依存性リストを自動的に生成します。
Provider Lifecycles
When does my Provider get created and disposed?
私のプロバイダはいつ作成され、破棄されますか?
これについては全プロバイダで共通です。
- Uninitialized(未初期化)
- Alive(活動中)
- Paused(停止)
- Disposed(破棄)
破棄/未初期化
未初期化または破棄プロバイダは、その状態が初期化されないため、メモリを占有しません。 基本的には、必要なときにプロバイダの状態を作成する方法を定義しているだけです。 アライブ・プロバイダまたはUIからのWidgetRefがそれを読んだり、見たり、聞いたりするまで、その状態を維持します。
Creating(作成)→Alive(活動)
初期化されていないプロバイダが読み込まれたり、リッスンされたり、ウォッチされたりすると、その状態が作成されます。 作成中に、プロバイダのビルド関数が実行されます。 コールバックによって公開されたrefを使用して読み取りまたは監視するプロバイダは、必要に応じて作成され、その状態が取得されます。 この作成プロセス中に循環依存関係がある場合、Riverpodはエラーをスローします。 このエラーを修正する最善の方法は、依存関係を再設計して一方向のデータフローを持つようにすることです。 プロバイダの状態はProviderContainerに格納されます。 Flutterアプリでは、このコンテナはProviderScopeウィジェットの中にある。 そのため、状態(プロバイダ)を作成する方法の定義はグローバルであっても、状態は実際にはローカルで、ネストされたProviderScopeウィジェットとオーバーライドを使ってUIの異なる部分で異なることができる。 これはflutterウィジェットの仕組みにとても似ている。 定義に支払うのは一度だけで、必要に応じてツリーの異なる部分で状態を再利用できる。
Alive(活動中)
あなたのプロバイダがAliveであるとき、その状態への変更は、依存するプロバイダおよび/または依存するUIを再構築する原因となります。 もう1つの観点から、リアクティブフレームワークとして、その依存関係の1つが変更されるたびにプロバイダがそれ自身を再作成するように他のプロバイダを監視することができます。 他の状態に依存するいくつかの長期的な状態を持つ必要がある場合は、プロバイダの再構築を引き起こすことなく、他のプロバイダの変更をサブスクライブするためにRefのlistenメソッドを使用することができます。
副作用で他のプロバイダからの状態を使用する必要がある場合は、Ref の [read] メソッドを使用して、他のプロバイダから現在の状態を取得できます。 通常、StateNotifier または ChangeNotifier クラスを構築するときは、必要に応じて Notifier が依存関係の現在の値を取得できるように ref を渡す必要があります。 Riverpod 2.0からの新しいNotifierクラスとAsyncNotifierクラスを使用することで、refはクラスのインスタンスメンバとして既に利用可能です。
Alive(活動中)→ Paused(停止)
アライブ・プロバイダーは、他のプロバイダーやUIからリッスンされなくなると、Paused状態になる。 これは、リッスンしているプロバイダーの変更に反応しなくなることを意味する。 これは最適化であり、プロバイダーをリッスンしていないのであれば、プロバイダーを生かしておく必要はない。 使用されていないプロバイダはすべてPaused状態に戻され、アプリの計算負荷が軽減されます。 副次的な効果のためにプロバイダを生かしておく必要がある場合は、UIの適切な場所でプロバイダをリッスンするようにしてください。 プロバイダが一時停止しているときに何らかのアクションを実行する必要がある場合は、refのonCancelメソッドを使用してコールバックを登録します。
プロバイダが一時停止状態から Alive 状態に再開したときに何らかのアクションを実行する必要がある場合は、ref の onResume メソッドを使用してコールバックを登録します。 状態を破棄して、計算リソースを使用しないだけでなく、状態のメモリも破棄したい場合は、プロバイダ定義で .autoDispose 修飾子を使用します。 これにより、使用されなくなったときにPausedではなくDisposed状態に遷移します。
Alive -> Disposing
プロバイダが破棄される理由はいくつかあります。 .autoDispose モディファイアを使用して定義され、UI または別のプロバイダによって監視されなくなったとき プロバイダが手動でリフレッシュまたは無効化されるとき 監視されている依存関係の 1 つが変更されたためにプロバイダが再作成されるとき リフレッシュすると、プロバイダはすぐに作成プロセスを再実行しますが、無効化すると、プロバイダの次の読み取り/監視によってプロバイダが再構築されます。
Performing actions before the state destruction
状態が破壊される前にアクションを実行する
プロバイダーが破棄されるときに何らかのアクションを実行する必要がある場合は、ref の onDispose メソッドを使用してコールバックを登録します。 次の例では、onDispose を使用して StreamController を閉じます。
@riverpod
Stream<int> example(ExampleRef ref) {
final streamController = StreamController<int>();
ref.onDispose(() {
//Closes the StreamController when the state of this provider is destroyed.
streamController.close();
});
return streamController.stream;
}
Why Immutability
不変性とは何か?
不変性とは、オブジェクトのすべてのフィールドが Final または Late Final である場合です。これらは構築時に 1 回だけ設定されます。 不変性はさまざまな理由から望ましいものです。
- 参照の平等ではなく価値の平等
- コード部分に関するローカル推論
- 遠く離れたコード部分は参照を取得して、下からオブジェクトを変更することができません
- 非同期および並列タスクの推論が容易になる
- 他のコードは操作の間にオブジェクトを変更できません
- APIの安全性
- メソッドに渡す内容は、呼び出し先/呼び出し元では変更できません。
copyWith メソッドは、いくつかの点を変更しただけで新しいオブジェクトを作成するときの冗長性を軽減するのに役立ちます。
Dart は変更されていないサブオブジェクトへの参照を再利用できるため、コピーは思ったより効率的です。
オブジェクトが完全に不変であることを確認してください。そうでない場合は、何らかのディープ コピー メカニズムを実装する必要があります。
Best Practices
不変の状態を作成するには、任意のパッケージを使用できます。
不変オブジェクトの場合:
package:freezed
package:built_value
不変コレクション(Map、Set、List)の場合:
package:fast_immutable_collections
package:built_collection
package:kt_dart
package:dartz
freezed の使用が強く推奨されています。
これは、不変オブジェクトを作るだけでなく、以下のような追加機能があるからです。
- 生成される copyWith メソッド
- ネストされた freezed オブジェクトに対する深いコピー(copyWith)
- ユニオン型
- ユニオンマッピング機能
コード生成を使わずに不変の状態を扱うことは可能ですが、コード生成を利用する方がはるかに簡単です。
注意)
組み込みのコレクションを使用する場合、更新時にコレクションのコピーを作成するという規律を守る必要があります。コレクションをコピーしない場合の問題は、riverpod が新しい状態を発行するかどうかをオブジェクトの参照が変わったかどうかで判断するため、オブジェクトを変更するメソッドを単に呼び出すと参照が同じままになるためです。
Using immutable state
不変状態の使用
不変状態は、Notifierを使うのが最も適している。 Notifierを使うと、インターフェイスを公開することができ、そのインターフェイスを通して状態を「変異」させることができる。 Notifierを継承したクラスの外からステートを変更することはできません。 これは、関係性の分離を強制し、ビジネスロジックをUIの外側に保ちます。 以下は、アプリのテーマを変更するためのシンプルな不変設定クラスの例です。
@riverpod
class ThemeNotifier extends _$ThemeNotifier {
@override
ThemeSettings build() => const ThemeSettings(
mode: ThemeMode.light,
primaryColor: Colors.blue,
);
void toggle() {
state = state.copyWith(mode: state.mode.toggle);
}
void setDarkTheme() {
state = state.copyWith(mode: ThemeMode.dark);
}
void setLightTheme() {
state = state.copyWith(mode: ThemeMode.light);
}
void setSystemTheme() {
state = state.copyWith(mode: ThemeMode.system);
}
void setPrimaryColor(Color color) {
state = state.copyWith(primaryColor: color);
}
}
@freezed
class ThemeSettings with _$ThemeSettings {
const factory ThemeSettings({
required ThemeMode mode,
required Color primaryColor,
}) = _ThemeSettings;
}
extension ToggleTheme on ThemeMode {
ThemeMode get toggle {
switch (this) {
case ThemeMode.dark:
return ThemeMode.light;
case ThemeMode.light:
return ThemeMode.dark;
case ThemeMode.system:
return ThemeMode.system;
}
}
}
このコードを使用するには、freezed_annotation をインポートし、part ディレクティブを追加して、build_runner を実行してフリーズされたクラスを生成することを忘れないでください。
(次の記事はこちら)