最近は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にてどうぞ。