こんにちは。
私は、現在FlutterアプリにRiverpod導入を考えています。導入に先立ってRiverpodの公式ドキュメントを読みました。
いつかドキュメント全体をざっくり見返すときにあまり頭を使いたくないため、メモの意味も兼ねてドキュメントを意訳して記事にまとめておくことにしました。
今回、意訳したページは以下のリンク先です。
意訳は正確でないこともあるのであらかじめご了承ください。
riverpod.dev
Providerをなぜ使うのか
- 様々な場所から状態へのアクセスを簡単にする
- 状態の組み合わせを単純にする
- パフォーマンスを最適化する
- テスタビリティを向上させる
- 簡単に統合する
Providerの生成
Providerは異なった方法で生成されるが動作は一緒
一般的に以下のように、グローバル定数として宣言される
final myProvider = Provider((ref) {
return MyValue();
});
Note
Providerをグローバル変数で宣言することを恐れる必要はない
Providerは完全に変更不可能だ。Providerを宣言することは関数を宣言することと変わらない、そしてテストやメンテナンスをしやすい
このスニペットは3要素から成り立っている
- final myProvider
という変数宣言
将来的にProviderから状態を読み込むための変数で、つねに変更不可能だ。
Provider
、使用するProviderのp種類
Provider
は一番基本的なProviderだ。二度と変更されないオブジェクトを公開する。状態をインタラクティブに変更するためにProvider
をStreamProvider
やStateNotifierProvider
に変更することもできる。
状態を共有する関数
関数は常にref
とよばれるオブジェクトを引数で受け取る。このオブジェクトはほかのProviderを読み取ることや、Providerが破棄されるときになんらかの処理を実行することを可能にする(フック的なやつ)。
関数によって作られたオブジェクトの型は、使用されるProviderに依存したProviderに渡される。
例えば、Providerの関数はなんらかのオブジェクトを生成できる。一方、StreamProvider
のコールバックはStream
を返すだろう。
Info
宣言可能なProviderはどれも制限を必要としない。package:provider
を使用した場合とは逆に、Riverpodでは同じ型のProviderを公開できる。
実際、ふたつのProviderはどちらもStringを生成しているが、問題はない。
Caution
Providerを使用する時、ProviderScope
をFlutterApplicationsのルートに追加してください。
Stateが破棄される前の処理の実行について
時には、状態についてのProviderは破棄・再生成される。一般的な使用例は、providerが破棄される前に状態を破棄する。たとえばStreamController
をcloseするように。
これはref.onDispose()
を使用することで実現する。
final example = StreamProvider.autoDispose((ref) {
final streamController = StreamController<int>();
ref.onDispose(() {
streamController.close();
});
return streamController.stream;
}
Note
使用するProviderによっては、事前にProviderを破棄する方法が用意されているかもしれない。
たとえば、StateNotifierProvider
はStateNotifier
のdispose
メソッドを呼ぶ
ProviderのModifierについて
すべてのProviderには、さらなる機能をついかする方法が用意されている。
ref
に新しい特徴を追加するか、ProviderをConsumeする方法を少し変更している。
final myAutoDisposeProvider = StateProvider.autoDispose<String>((ref) => 0);
final myFamilyProvider = Provider.family<String, int>((ref, id) => '$id');
ときには、二種類のModifiersを使用することが可能だ
.autoDispose
, 参照されなくなった時にProviderの状態を自動で破棄する
.family
, 外部のパラメータからProviderの生成を可能にする。
Note
はじめに複数のmodifiersを使う時
final userProvider = FeatureProvider.autoDispose.family<User, int>(ref, userId) async {
return fetchUser(userId);
});
Providerをreadする
このガイドを読む前にread about Providersを読んね
このガイドでは、どうやってProviderを使用するのかを知ることができる
providerをreadする方法はいくつかあって、基準・規格・仕様によって少し違う
手短に知るには以下の図にそってどのproviderをreadするかみてね
どこでProviderをreadするか?
- テストやDart-Only-PackageでreadするならProviderContainer.read
Widgetの中
- Dialog表示のためにlisteningするならProviderListener
onPressed
を覗くbuild
メソッドの中でreadしなければContext.read
- flutter-hooksを使うなら
useProvider
- flutter-hooksを使わないなら
Consumer
他のProviderの中
- providerの更新に伴ってproviderによって公開された値が、再生成されるべきか
- べきなら
ProviderReference.watch
- ちがうなら
providerReference.read
つぎにそれぞれ個別の場合をみながらどうやって動作するのか紹介します
このガイドでは以下のproviderで考えます。
final counterProvider = StateProvider((ref) => 0);
readの方法を決定する
listenしたいProviderによって、値をlistenする方法がいくつかあります
たとえば、以下のStreamProviderについてだと、
final userProvider = Streamprovider<User>(...);
userProviderをreadするとき、以下のような方法が可能である。
- userProvider自身で同期的に現在の状態をreadする
Widget build(BuildContext context, ScopeReader watch) {
AsyncValue<User> user = watch(userProvider);
return user.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => const Text('Oops'),
data: (user) => Text(user.name),
);
}
- Streamの連携に伴って変更される値をreadするなら
userProvider.stream
Widget build(BuildContext context, ScopeReader watch) {
Stream<User> user = watch(userProvider.stream);
}
- 最新の値で発火されるFutureで解決される値を取得するには、
userProvider.last
を使用する
Widget build(BuildContext context, Scopedreader watch) {
Future<User> user = watch(userProvider.last);
}
さらなる情報はAPIリファレンスみてね
この章では、ProviderをWidgetの中で使う方法について知ることができる
ConsumerWidget
はStatelessWidget
のような基底クラスだ。そして、providerをlistenすることができる
class Home extends ConsumerWidget {
@override
Widget build(BuildContext context, Scopedreader watch) {
int count = watch(counterProvider).state;
return Scaffold(
appBar: AppBar(title: const Text('Counter example'));
body: Center(
child: Text('$count'),
),
);
}
}
この例では、counterProvider
とcounterが変更される旅にText
がリビルドされるだろう。
counterProviderの値によって気づくことは、関数がwatchを呼ぶことだ。ConsumerWidget
で作られた関数は私たちのproviderをlistenし、値の変更が公開されるとリビルドする。
Caution
ConsumerWidgetの引数を渡すwatch()
は```onPressedのような関数では非同期で呼ばないべきだ。
もしユーザのイベントのレスポンスをproviderをreadする必要があるなr、myProvider.read(BuildContext)
を使用してください
Consumer
Consumer
はアプリケーション内でデータを扱う特定のウィジェットの再描画のパフォーマンスを最適化することができるConsumerWidget
だ
たとえば、ConsumerWidgetのスニペットを更新以下のコードでは、まえもってText
のみが再描画されることを認識できる。
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter example')),
body: Center(
child: Consumer(
builder: (context, watch, child) {
int count = watch(counterProvider).state;
return Text('$count');
},
),
),
);
}
}
context.read(myProvider)
ときには、listenする値が存在しないこともある。例えば、オブジェクトはonPressed
でのみ必要な場合。
私たちはConsumer
を使うことができる
Consumer(builder: (context, watch, _) {
StateController<int> counter = watch(counterProvider);
return RaisedButton(
onPressed: () => counter.state++,
child: Text('increment');
)
});
しかし、これは効果的ではない。providerのlistenによって、counterが変更される旅にRaisedButtonが再描画される。counterが実際にbuildに使われない場合でさえ。
解決方法としてcontext.read(myProvider)
が存在する。
これを使うことで前述のコードを以下のようにリファクターできる。
@override
Widget build(BuildContext context) {
retur RaisedButton(
onPressed: () => context.read(counterProvider).state++,
child: Text('increment'),
);
}
こうすることで、依然として、クリックすることで、ボタンはcounterをインクリメントする。しかし、もはやproviderをlistenしておらず、不必要な再描画を避けている。
context.read
がみつからない。どうしたらいい?
もしcontext.read
が見つからないなら、それはおそらく正しいパッケージをインポートしていないからだ。このメソッドを使うにはpackage:flutter_riverpod
か、package:hooks_riverpod
をインポートしなければならない。
Info
listenしているProviderによっては、これは必要ないかもしれない。
たとえば、StateNotifierProvider
はそれ自身の状態のlistenを除く、StateNotifierを取得する方法が用意されている。
class Counter extends StateNotifier<int> {
Counter(): super(0);
void increment() => state++;
}
final counterProvider = StateNotifierProvider((ref) => Counter());
// ...
@override
Widget build(BuildContext context, ScopedReader watch) {
final Counter counter = watch(counterProvider);
return RaisedButton(
onPressed: counter.increment,
child: Text('increment'),
);
}
Caution
context.read
をbuild()
の中で呼ぶことを回避してください。もし、再描画を最適化したいなら、値の変更のlistenをProviderの中に変更してください。
ProviderListener
ときには、Providerの変更のあとでダイアログの表示やウィジェットツリーへのルートの挿入を行いたい場合もある。
ProviderListener
ウィジェットを利用して以下のように実装してください
Widget build(BuildContext context) {
return ProviderListener<StateController<int>>(
provider: counterProvider,
onChange: (counter) {
if(counter.state == 5) {
show Dialog(...);
}
},
child: Whatever(),
)
}
これできっとカウントが5になるとダイアログが表示されます。
Providerの中で他のProviderをreadする
一般的なProviderの作成方法として他のオブジェクトからオブジェクトを生成することがある。
例えば、UserRepository
からUserController
を生成したい時、それらは違ったProviderによって公開されているとする。
このシナリオはproviderがパラメータを受け取るためのref
を使うことによって可能になる
final userRepositoryProvider = Provider((ref) => UserRepository());
final userControllerProvider = StateNotifierProvider((ref) {
return UserController(
repository: ref.watch(userRepositoryProvider),
);
});
CombineProviders
ガイドでより多くの情報を得られる。例えば以下のように
- 時間と共に変化する値からオブジェクトを作る時、どんなことがおこるか
- 良い方法について