RiverPod
プロバイダについて
-
状態管理を行うためのライブラリ(ツリーの中でバケツリレー的なステートの受け渡しをなくして、直接ステートを参照できるようにするもの)
- プロバイダを使用する場合にはFlutterのアプリルートに
ProviderScope
を置く必要がある
- プロバイダを使用する場合にはFlutterのアプリルートに
void main() {
runApp(ProviderScope(child: MyApp()));
}
- プロバイダはグローバル定数として、宣言するのが一般的である
- その際にイミュータブル(不変)であることを意識する(イメージ的には関数をグローバルに定義する感覚と同じ)
例
final myProvider = Provider((ref) {
return MyValue();
});
-
final myProvider
で変数を宣言する。この際にはfinalで宣言する- この変数を用いてステートを呼び出す
-
Provider((ref) {return MyValue();});
で使用するプロバイダの種類を確定する -
ref
オブジェクトをパラメータとして、他のプロバイダを使用したり、ステートが破棄される際のコールバック関数を登録したりする
プロバイダの種類
-
Provider (型は任意)
- サービスクラス / 算出プロパティ(リストのフィルタなど)
-
StateProvider (型は任意)
- フィルタの条件 / シンプルなステートオブジェクト
-
StateProvider (型はFuture)
- APIの呼び出し結果
-
StreamProvider (型はStream)
- API の呼び出し結果の Stream
-
StateNotifierProvider (StateNotifierのサブクラス)
- イミュータブル(インタフェースを介さない限り)で複雑なステートオブジェクト
-
ChangeNotifierProvider (ChangeNotifierのサブクラス)
* ミュータブルで複雑なステートオブジェクト(ミュータブル(可変)であり、特有のユースケースに対応する為に存在している)
プロバイダ修飾子
- .autoDispose はプロバイダの監視が終わったタイミングで、プロバイダに自動でステートを破棄させることができるようになる
- .family はプロバイダ外部の値を用いてプロバイダを作成できるようになる
final myAutoDisposeProvider = StateProvider.autoDispose<int>((ref) => 0);
final myFamilyProvider = Provider.family<String, int>((ref, id) => '$id');
// 同時利用も可能
final userProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
return fetchUser(userId);
});
プロバイダの利用方法
- プロバイダを利用するには
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('...');
}
}
Widgetからの取得
- 通常のウィジェットでは ref を使用することができないため、 Riverpod では複数の方法を用意しています。
StatelessWidget の代わりに ConsumerWidget を継承する
* [ConsumerWidget](https://pub.dev/documentation/flutter_riverpod/latest/flutter_riverpod/ConsumerWidget-class.html) は [StatelessWidget](https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html) とほぼ同等のものであり、build メソッドに`ref`を渡す第2パラメータが存在する以外の違いはない。
class HomeView extends ConsumerWidget {
const HomeView({Key? key}) : super(key: key);
@override
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({Key? key}) : super(key: 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` は 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({Key? key}) : super(key: key);
@override
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({Key? key}) : super(key: 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) {
// 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 の使い方
- 3通りある
-
ref.watch
: プロバイダの値を取得した上で、その変化を監視する。値が変化すると、その値に依存するウィジェットやプロバイダの更新が行われる。 -
ref.listen
: プロバイダの値を監視し、値が変化するたびに呼び出されるコールバック関数(画面遷移、ダイアログの表示など)を登録する。 -
ref.read
: プロバイダの値を取得する(監視はしない)。クリックイベント等の発生時に、その時点での値を取得する場合に使用できる。
機能の実装時には可能な限り ref.watch を使用すること。 ref.watch によりアプリはリアクティブかつ宣言的になり、コードの保守性を高まる。
watch と ListenメソッドはElevatedButtonのonPressed 内など、非同期的な場面で呼び出さないこと。またinitState を始め、Stateのライフサイクルメソッド内での使用も避ける。これらの場合は代わりに ref.read を使用すること
-
ref.watch
-
ウィジェットあるいはプロバイダ内で ref.watch を使ってプロバイダを監視することができる
- プロバイダ内での使用
- ref.watch でプロバイダに別の複数のプロバイダを監視させ、それらの値を組み合わせて新たに値を生成するということも可能
- プロバイダ内での使用
// 複数のプロバイダを監視は Todo リストのフィルタリングにも活用できます。 例えば、Todo アプリに次のプロバイダがあるとします。
//• filterTypeProvider: 現在のフィルタの種類を公開するプロバイダ(なし、完了のみ表示、未完了のみ表示...)
//• todosProvider: Todo リストの内容をすべて公開するプロバイダ
//• filteredTodoListProvider: Todo リストの完了と未完了をフィルタリングするプロバイダ
// 現在のフィルタの種類を公開するプロバイダ(なし、完了のみ表示、未完了のみ表示...)
final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
// Todo リストの内容をすべて公開するプロバイダ
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;
}
});
//filterTypeProvider や todosProvider の値が変わると自動的に更新されます。 逆に言えば、いずれかが変わらない限りは再計算されることはありません。
- ウィジェット内での使用
final counterProvider = StateProvider((ref) => 0);
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// `ref` を使ってプロバイダを監視する
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
//値が変わるたびに UI を更新させることができる
ref.listen
-
ref.listen
はref.watch
と同様にプロバイダを監視することができるが、最大の違いはref.watch
が値の変化に応じてウィジェットやプロバイダを更新するのに対して、ref.listen
は任意の関数を呼び出してくれるという点です。- 使用ケースとしては、エラー発生時のスナックバー表示など、何かしらの変化に反応して処理を実行したいときなど
-
ref.listen
メソッドは2つの位置引数が必要である。 ::第1引数にプロバイダ::、::第2引数にステート(状態)が変化した際に実行するコールバック関数::を渡す。 このコールバック関数には呼び出し時に、プロバイダの直前のステートと新しいステートの値が渡されるため、それぞれをパラメータとして使用できる
プロバイダ内
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
});
ウィジェット
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
return Container();
}
}
ref.read
ref.read はリアクティブではないため、可能な限り使用を避けること
【重要】 ref.read は build メソッドの中で使わない。
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 の使用は安全であるという部分から使用すると、リアクティブにする変更をしなければならない瞬間に値が変化することをアプリケーションは受け入れなければならない。であれば、最初からリアクティブなwatchを使用するべきである。(以下公式)
//ソフトウェアに変更はつきものです。そして将来、絶対に変わらないと考えられていた値が、変わる必要に迫られることは十分あり得ることです。 そしてもしこの値の取得に ref.read が使われていたら、その時にはコードを振り返り ref.read が使われているところをすべて ref.watch に変更する必要があります。これはエラーを招くおそれがありますし、変更し忘れる箇所がいくつか出てくる可能性もあります。一方、最初から ref.watch を使用していれば、リファクタリング時に生じる問題は比較的少ないはずです。
-
ref.read
メソッドを使うことでプロバイダのその時点でのステートを取得することができる。純粋な取得以外の副作用はない。-
ref.read
はユーザ操作によって呼び出される関数内で使用するのが一般的である。 例えば、ボタンクリックイベント発生時にカウンターの数字を変更する場合に使用できる
-
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// `Counter` クラスの `increment()` メソッドを呼び出す
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
selectを使って更新の条件を限定する
- プロバイダを監視するということは、そのプロバイダが公開するオブジェクト全体のステートを監視することであり、 その監視の範囲を狭めて特定のプロパティのみを監視対象としたい場合がある
// Userオブジェクト
abstract class User {
String get name;
int get age;
}
// nameプロパティしか関心がないが、 userProvider を普通に監視すると無関係な age プロパティの変化もウィジェット更新のトリガーとなってしまう
Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}
// selectでnameプロパティだけに関心を絞る
Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}
// listenもOK
ref.listen<String>(
userProvider.select((user) => user.name),
(String? previousName, String newName) {
print('The user name changed $newName');
}
);
-
select を使って監視対象にするプロパティを返す関数を指定することができる。User オブジェクトに変化があるたびに、Riverpod はこの関数を呼び出し、対象プロパティの古い値と新しい値を比較する。 値が異なる場合は Riverpod はウィジェットを更新、 対象プロパティ以外の値が変わっただけで対象プロパティの値自体が不変の場合は、Riverpod はウィジェットの更新をしない
-
select で明示する値は、必ずしも対象オブジェクトのプロパティそのものである必要はない。 == 演算子のオーバーライドなどでオブジェクトの等価性が定義されていれば何を返しても問題はない
final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));
プロバイダのステートを組み合わせる
- 基本的にプロバイダを組み合わせる場合は
ref
オブジェクトのwatach
メソッドを使用する- 状態を監視してくれるため、依存先のプロバイダの値が変更されたら、依存元のプロバイダも再評価してくれる
final cityProvider = Provider((ref) => 'London');
final weatherProvider = FutureProvider((ref) async {
// `ref.watch` により他のプロバイダの値を取得・監視します。
// 利用するプロバイダ(ここでは cityProvider)を引数として渡します。
final city = ref.watch(cityProvider);
// 最後に `cityProvider` の値をもとに行った計算結果を返します。
return fetchWeather(city: city);
});
// ===============実践的===============
// Todoリストクラス
class TodoList extends StateNotifier<List<Todo>> {
TodoList(): super(const []);
}
// Todoリスト外部公開するためのプロバイダ
final todoListProvider = StateNotifierProvider((ref) => TodoList());
// フィルタなし、完了、未完了のステータス
enum Filter {
none,
completed,
uncompleted,
}
// フィルタのステータスを外部公開するためのプロバイダ
final filterProvider = StateProvider((ref) => Filter.none);
// Todoリストプロバイダとステータスプロバイダを組み合わせて、ステータスに応じたTodoリストを外部公開するプロバイダを作成
final filteredTodoListProvider = Provider<List<Todo>>((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 も自動的に再構築される。
これで他のプロバイダの値に依存するプロバイダを作ることができた。
プロバイダオブザーバー
- ProviderObserver は ProviderContainer 内で起こる変化を監視する
プロバイダのライフサイクル
- didAddProvider
- プロバイダが初期化されるたびに呼び出される。公開される値は value パラメータで利用可能
- didDisposeProvider
- プロバイダが破棄される度に呼ばれる
- didUpdateProvider
- プロバイダが変更通知を送信するたびに呼び出される
使用方法
- ProviderObserver クラスを継承するクラスを定義し、使用したいメソッドをオーバーライドして使用する
- 例としてidUpdateProvider メソッドをオーバーライドして、プロバイダのステート変化をログに残すという用途にも使用することができる
// Riverpod を使用した Logger 付きのカウンターアプリの例
class Logger extends ProviderObserver {
@override
void didUpdateProvider(
ProviderBase 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({Key? key}) : super(key: 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),
),
);
}
}
// ステートが変化する度にログ取るようになる
I/flutter (16783): {
I/flutter (16783): "provider": "counter",
I/flutter (16783): "newValue": "1"
I/flutter (16783): }