Riverpodの基本的な使い方を整理してみる
改めてRipverpodの基本的な使い方を整理しておきたくなったので書き出してみる。
Riverpod
Compile safe
No more ProviderNotFoundException or forgetting to handle loading state.
Using Riverpod, if your code compiles, it works.
定義したグローバル変数を直接参照するので、コンパイルできる=型があっていれば、動作が保証されると。より安全にプログラミングできるってことですね。
Provider, without its limitations
Riverpod has support for multiple providers of the same type;
combining asynchronous providers; adding providers fro anywhere, ...
同じ型でも使い分けられて、非同期処理を組み合わせられて、どこからでも追加できると。様々なユースケースをサポートしてる感じですね。
Doesn't depend on Flutter
Create/share/tests providers, with no dependency on Flutter.
This includes being able to listen to providers without a BuildContext.
Flutterには依存してませんよと。テストコードが書きやすかったり、サーバーサイドでも使えたりする感じですね。
値を渡す・受け取る
まずは値の受け渡しをやってみる。
Consumer
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod/riverpod.dart';
final helloWorldProvider = Provider((ref) => 'Hello World');
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer(
builder: (context, watch, child) {
final helloWorld = watch(helloWorldProvider);
return MaterialApp(
home: Scaffold(
body: Center(
child: Text(helloWorld),
),
),
);
},
);
}
}
テキストを参照して表示してみた。グローバル変数としてProviderを定義し、Consumerのコールバック経由でwatchという名のScopedReaderが使えるので、こいつとhelloWorldProviderを組み合わせると、'Hello World'が参照できるようになると。
値の受け渡しはとてもシンプルに実装できた。ほかにも、値の参照方法はいくつか種類があるらしい。
ConsumerWidget
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final helloWorld = watch(helloWorldProvider);
return MaterialApp(
home: Scaffold(
body: Center(
child: Text(helloWorld),
),
),
);
}
}
ConsumerWidgetを継承するとbuildメソッドからScopedReaderが受け取れるようになると。こっちの方が手っ取り早い感じがするが、継承したWidget全体が再描画の対象範囲になってしまうので、パフォーマンスをすごく気にする場合はConsumerを使って再描画の対象範囲を制限すると良いかもしれない。
context.read
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final helloWorld = context.read(helloWorldProvider);
return MaterialApp(
home: Scaffold(
body: Center(
child: Text(helloWorld),
),
),
);
}
}
BuildContextのextensionも提供されている。これはProviderの値を検知できないので、ボタンをタップしたときなど、その時点での値を参照したいときに使うやつ。
HookWidget/userProvider
class MyApp extends HookWidget {
@override
Widget build(BuildContext context) {
final helloWorld = useProvider(helloWorldProvider);
return MaterialApp(
home: Scaffold(
body: Center(
child: Text(helloWorld),
),
),
);
}
}
Reactユーザーには馴染みのHooksスタイルも提供されている。FlutterHooksを使っている場合はConsumerWidgetではなくこの書き方にすると良さそう。
で、結局どれを使えばよいのか?
どれでも良い。しかし、ボタンをタップした時など、値の更新を検知する必要がない場合はcontext.readを使う。
迷ったらHooksスタイルにしておくと、パッケージ開発者のイメージしている世界観になるかもしれない。(flutter_hooksも同じ人が開発しているので)
値を更新する
次は値を更新してみる。
StateProvider
final countStateProvider = StateProvider((ref) => 0);
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends HookWidget {
@override
Widget build(BuildContext context) {
final count = useProvider(countStateProvider).state;
return MaterialApp(
home: Scaffold(
body: Center(
child: Text('$count'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read(countStateProvider).state = count + 1,
child: Icon(Icons.add),
),
),
);
}
}
さっきまでは値を渡すのにProviderを使っていたが、更新可能な値を渡す場合はStateProviderを使う必要がある。
context.read(countStateProvider)で渡ってくるのはStateControllerである。これはStateNotifierを継承しているのでstateを更新することで参照している箇所に自動的に更新を検知できる感じになっている。
しかし、この方法では値を更新する処理をWidget内に書く感じになってしまい、処理が複雑になったときは厳しくなってくる。
その場合は、自身でStateNotifierなどを継承したクラスを定義し、それを値として渡す必要が出てくる。
StateNotifierProvider
final countStateProvider = StateNotifierProvider((ref) => CountState(0));
class CountState extends StateNotifier {
CountState(int count) : super(count);
void increment() => state = state + 1;
}
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends HookWidget {
@override
Widget build(BuildContext context) {
final count = useProvider(countStateProvider.state);
return MaterialApp(
home: Scaffold(
body: Center(
child: Text('$count'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read(countStateProvider).increment(),
child: Icon(Icons.add),
),
),
);
}
}
値を参照するのにuseProvider(countStateProvider.state)だったりuseProvider(countStateProvider).stateだったりと微妙に使い方が異なっているのが若干混乱する。
基本的な使い方はStateProviderと同じであるが、StateNotifierを継承したクラスを定義しその中に値の更新処理を閉じ込めることが出来た。
細かい末端のWidgetはStateProviderでサクッと実装しつつ、それ以外の重要な部分はStateNotifierProviderなどを使ってある程度の複雑さに耐えうるよう使い分けると良いかもしれない。
Providerを組み合わせる
Providerは単純に読み書きだけでなく、組み合わせて使うことも可能であると。
final countStateProvider = StateProvider((ref) => 0);
final count10Provider = Provider((ref) {
final count = ref.watch(countStateProvider).state;
return count * 10;
});
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends HookWidget {
@override
Widget build(BuildContext context) {
final count10 = useProvider(count10Provider);
return MaterialApp(
home: Scaffold(
body: Center(
child: Text('$count10'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read(countStateProvider).state++,
child: Icon(Icons.add),
),
),
);
}
}
シンプルな用途としては、値のフィルタリングだろうか。フィルタリングする前の値は0,1,2,3...であるが、表示するフィルタリングした値は0,10,20,30...としてみた。
使い方はConsumerなどで値を参照する時と同じ感じで、Providerを定義するときにrew.watchで他のProviderを参照し、後は好きに値を書き換えればOKである。
これの良いところは、更新部分は変えずに、表示部分だけフィルタリングした値に差し替えることができる所だろうか。
最後に
Riverpodを使った基本的な値の読み書き・Providerの組み合わせを整理できた。これ以外にも出来ることは沢山あるので、もうちょっと細かい部分は次の投稿で整理してみたいと思う。