最近はFlutterの状態管理にRiverpodを使用しています。
Flutterでプロジェクトを作成したときに生成されるカウンターアプリは、状態管理にStatefulWidgetを使用していますが、Riverpodを使用したバージョンを備忘録として残しておきます。
本記事で使用している構成は、以下の組み合わせです。
- hooks_riverpod : ^1.0.0
flutter_hooks : ^0.18.0- flutter_state_notifier : ^0.7.1
- freezed : ^0.14.2
※2021/11/6 versionを更新
Riverpodで状態管理する場合に、使用するパッケージ
Riverpodを使用して状態管理する場合、目的により使用するパッケージが異なってきます。
一般的なパッケージ構成は以下の通りです。
- hooks_riverpod
flutter_hooks- flutter_state_notifier
- freezed
※Riverpodの公式リリース(V1.0.0)に伴い、使用するWidgetが変わったため、本サンプルでは「flutter_hooks」が不要になりました。なお、useEffectなどを使用する場合は必要です。
いずれも、pub.devにあがっているので、pubspec.yamlに追加すればインストールすることができます。
ちなみにRiverpod作者のRemi Rousseletさんも上記のような構成が好きなようです。
https://twitter.com/remi_rousselet/status/1277504792672325635?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1277504792672325635%7Ctwgr%5E%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fcdn.embedly.com%2Fwidgets%2Fmedia.html%3Ftype%3Dtext2Fhtmlkey%3Da19fcc184b9711e1b4764040d3dc5c07schema%3Dtwitterurl%3Dhttps3A%2F%2Ftwitter.com%2Fremi_rousselet%2Fstatus%2F1277504792672325635image%3D
Riverpodを使用した、カウンターアプリのサンプル
私は個人的にMVVMの構成を使うことが多いので、今回のサンプルもMVVMで構成しています。(ただし、今回Modelは使用していません。)
ファイル構成は以下にしています。
ポイントをかいつまんで説明します。
main()でProviderScopeを使用する
main()でProviderScopeを使用します。
void main() {
runApp(ProviderScope(
child: MyApp(),
));
}
MyAppでProviderを使用できるようになります。
viewはHookConsumerWidgetを継承する
viewではProviderを使用するために、HookWidgetを継承します。
class MyHomePage extends HookConsumerWidget {
final String title;
MyHomePage({required this.title});
@override
Widget build(BuildContext context, WidgetRef ref) {
// counterProviderの状態
final counter = ref.watch(counterProvider).counter;
// CounterControllerの関数を使用するために読み取り
final notifier = ref.watch(counterProvider.notifier);
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'You have pushed the button this many times:',
),
Text(
'$counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
// CounterControllerに置いてある処理を呼ぶ
onPressed: () => notifier.increment(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
HookConsumerWidgetを継承することで、ref.watchを使用してProviderのstateにアクセスできるようになります。
また、カウントする処理もProviderに移してあり、こちらはref.watchの引数にprovider.notifierで読み取ります。
StateクラスにProviderで管理する状態を定義する
Stateクラスで管理したい状態を定義する。
@freezed
class CounterState with _$CounterState {
const CounterState._();
const factory CounterState({
required int counter,
}) = _CounterState;
}
カウンターアプリではcounterしかないため1つですが、複数ある場合は複数定義します。
freezedパッケージによりimmutableなクラスとなっています。
freezedパッケージの解説は以下の記事をご覧ください。
ControllerクラスでグローバルなProviderを定義する
ControllerクラスにてStateNotifierを継承したControllerと、グローバルなProviderを用意します。
final counterProvider = StateNotifierProvider<CounterController, CounterState>(
(ref) => CounterController(ref.read));
class CounterController extends StateNotifier {
CounterController(this._reader)
: super(CounterState(
// Stateを初期化する。counterは必須にしているため、初期値設定しないとエラー
counter: 0,
)) {
// DBからのデータ初期取得処理などあれば、取得処理を書く
}
// 他のproviderを使用する場面などで使用する
final Reader _reader;
// increment処理をControllerに定義
void increment() {
state = state.copyWith(
counter: state.counter + 1,
);
}
}
カウンターの初期値と、カウンターを1増やす処理を用意しています。
CounterStateで定義した状態を操作する場合、stateの値を更新します。
freezedパッケージでCounterStateはimmutableになっているので、stateを更新する場合はfreezedパッケージで生成されたcopyWithを使用します。
まとめ
Riverpodを使用したカウンターアプリのサンプルでした。
人それぞれ書き方は色々あると思いますが、個人的にはこれが1番好きです。Riverpodを使う前に使っていた、ProviderのときにMVVM構成にしていたので、近い感じで使用できていい感じです。
今回のサンプルの全容を確認したい場合は、Githubにてどうぞ。

