#はじめに
Flutter の状態管理手法は数多くあります。
そして最近では新たに Riverpod というパッケージが登場しました。Riverpod は Provider パッケージと同じ方によるもので、("Riverpod" は "Provider" のアナグラムになっている。すごい!)状態管理手法に新たな選択肢を提示します。
筆者もこの Riverpod を使ってみようと思ったのですが、Riverpod 内の provider にもいくつか種類があり、何があるのか最初よくわからなかったのでこの記事にまとめてみます。
なお、筆者は Riverpod に全然詳しくなく、Provider についてもあまり使いこなせているわけではないので、内容に誤りがあったり改善点などがございましたら是非ご指摘ください。(むしろアドバイスや意見が欲しくて記事を書いてるまであります。)
##Provider
Provider は最も基本的な provider だと思います。
final cityProvider = Provider((ref) => 'London');
class CitySample extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final city = watch(cityProvider);
return Scaffold(
appBar: AppBar(
title: Text('City Sample'),
),
body: Center(
child: Text('I want to visit $city.'),
),
);
}
}
といった感じで定義して使用します。筆者の理解では Provider は基本的に後から値を変えることができないので、読み取り専用のデータを渡すために使っています。
##StateProvider
StateProvider もよく使う provider だと思います。
int や String など、シンプルな値を管理するための provider です。
より複雑な値(複数の値をまとめたオブジェクトなど)を管理したい場合は次の StateNotifierProvider を使ったほうが良さそうです。
Provider との違いは、状態の書き換えが可能なことだと考えています。
final countProvider = StateProvider<int>((ref) => 0);
class CountSample extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final count = watch(countProvider);
return Scaffold(
appBar: AppBar(
title: Text('Count Sample'),
),
body: Center(
child: Text('Tapped ${count.state} times.'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
count.state++;
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
といった感じで使用します。(筆者は現状、flutter_riverpod を使っているので ConsumerWidget を継承していますが、hooks_riverpod を使っている場合は若干書き方が変わるようです。)
##StateNotifierProvider
StateNotifierProvider は名前の通り、StateNotifier を渡す provider です。そもそも StateNotifier とは何かと思う方もいるかもしれませんが、これについては同作者による同名のパッケージが独立して存在しており、それについての記事もいくつか上がっているのでそちらをご参照ください。
StateNotifierを使ったFlutterのアプリ設計
Flutter state_notifierいい感じなので使ったほうが良いですよ
state_notifier と freezed を使って、Flutterのカウンターアプリをつくるよ
使い方は大体 StateProvider と同じなのですが、いくつか違う点もあります。
// 普通はもっと複雑になるとおもうので freezed パッケージを使う。
class CounterState {
CounterState(this.count);
final int count;
}
class Counter extends StateNotifier<CounterState> {
Counter(CounterState state) : super(state);
increment() {
state = CounterState(state.count + 1);
}
}
final counterProvider =
StateNotifierProvider<Counter>((ref) => Counter(CounterState(0)));
class CounterSample extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final counter = watch(counterProvider.state);
return Scaffold(
appBar: AppBar(
title: Text('Counter Sample'),
),
body: Center(
child: Text('Tapped ${counter.count} times.'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read(counterProvider).increment();
},
tooltip: 'Inclement',
child: Icon(Icons.add),
),
);
}
}
まあ大体こんな感じだと思います。
今回の例は、管理する値が一つしかないので全く StateNotifier を使う意味がありませんが、この値がたくさんある場合は有効になってくるはずです。(1つしかない場合はStateProvider を使う。)またそういう場合は同作者の freezed パッケージを使うことが推奨されるようです。
##FutureProvider
FutureProvider は、値を得るのに時間がかかる場合(HTTPリクエストやローカルファイルの読み込みなど)に使うようです。少し特殊なのが、値が AsyncValue という Riverpod 独自のオブジェクトで返される点です。
AsyncValue には when というメソッドがあり、これによってデータが得られた時、ローディング中、エラー発生時で処理を分けることができます。これは状態によって表示を分けたい場合非常に便利です。
final cityFutureProvider = FutureProvider<String>((ref) {
return Future.delayed(Duration(seconds: 5), () => 'Paris');
});
class CityFutureSample extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final city = watch(cityFutureProvider);
return Scaffold(
appBar: AppBar(
title: Text('City Future Sample'),
),
body: Center(
child: city.when(
data: (data) => Text('You want to visit $data.'),
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Error!')),
),
);
}
}
また、他の Provider から FutureProvider の値を使いたい場合は単純に次のようにしたら良いと思います。(たぶん)
final cityFutureProvider = FutureProvider<String>((ref) {
return Future.delayed(Duration(seconds: 5), () => 'Paris');
});
final textProvider = Provider<String>((ref) {
return ref.watch(cityFutureProvider).when(
data: (data) => 'You want to visit $data.',
loading: () => 'Now loading...',
error: (error, stack) => 'Error!');
});
##StreamProvider
StreamProvider は FutureProvider のストリーム版です。
これも AsyncValue を返します。
大体 FutureProvider と同じだと思うので例は割愛。
##ScopedProvider
ScopedProvider は少し特殊です。最初は null を返すものとして定義して、後から
上位の ProviderScope で値を上書きすることができます。
final cityFutureProvider = FutureProvider<String>((ref) {
return Future.delayed(Duration(seconds: 5), () => 'Paris');
});
class CityScopedSample extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final city = watch(cityFutureProvider);
return Scaffold(
appBar: AppBar(
title: Text('City Scoped Sample'),
),
body: Center(
child: city.when(
data: (data) => ProviderScope(
overrides: [cityProvider.overrideWithValue(data)],
child: _CityScopedSample()),
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Error!')),
),
);
}
}
final cityProvider = ScopedProvider<String>((ref) => null);
class _CityScopedSample extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final city = watch(cityProvider);
return Text('You want to visit $city.');
}
}
コンストラクタに渡さずに下位に値を与えることができます。
ただし、ScopedProvider の値を参照できるのはUIのみで、別の provider から ScopedProvider の値を利用したりはできないので注意が必要です。
今回は、FutureProvider から得られた値で cityProvider の初期値を上書きしています。(態々こんなことするより直接 AsyncValue を使ったほうが良さそう。)
正直、イマイチ使いみちがわかっていません。
また、この ProviderScope による値の上書きですが、ScopedProvider 以外の provider の値を上書きする場合には、最上位の ProviderScope で行わないとエラーになるようです。
```と言われます。
##.family
各 provider は .family を使用することができ、(ScopedProvider 除く)これによって引数をもとに動的に provider を生成することができます。
```dart
//定義
final familyProvider = Provider.family<'戻り値の型', '引数の型'>((ref, '引数'){});
//参照
ref.watch(familyProvider('引数'));
みたいな感じで使えます。
よくわからん例ですが、下のようなことができます。
enum Member {
father,
mother,
son,
daughter,
}
final familyAgeProvider = Provider.family<int, Member>((ref, member) {
switch (member) {
case Member.father:
return 40;
case Member.mother:
return 35;
case Member.son:
return 10;
case Member.daughter:
return 5;
default:
return 0;
}
});
class FamilySample extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final children = List<Widget>();
Member.values.forEach((member) {
final name = member.toString().split('.').last;
final age = watch(familyAgeProvider(member));
children.add(Text('The $name is $age years old.'));
});
return Scaffold(
appBar: AppBar(
title: Text('City Sample'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: children,
),
),
);
}
}
また、他に .autoDispose というものもあり、これを使うともう必要なくなったときに自動的に状態を破棄してくれるようです。( .autoDispose と .family は併用できる。)
#おわりに
最後までお読みいただきありがとうございました。
ここまでが、実際に使いそうな provider のまとめとなります。
Riverpod の情報は(特に日本語だと)まだまだ少ないので、この記事がこれから Riverpod を使おうとしてる方の役に立てば幸いです。
なお Riverpod はまだまだ発展途上のパッケージなので、今後仕様が変わったり、provider が増えたり減ったりするかもしれません。また、筆者もまだ使い始めたばかりですので、間違ってる情報があるかもしれません。
なので必ず API reference をご確認ください。
#参考
Riverpod を使い始める際に参考になったページです。
Riverpod
Flutterの状態管理手法の選定
[Flutter]Riverpodを使ってToDoアプリを作ってみた
RiverpodとFlutter Hooksを使う、はじめの一歩
【神パッケージ】 Riverpod の使い方【Flutter】
【Flutter】Providerのほぼ上位互換、Riverpodの基本的な使い方