はじめまして、Advance Tech Divisionでモバイルエンジニアをしている中塚です。今回はFlutterシリーズの第2弾ということで、Flutterでの状態管理パターンについて書こうと思います。Flutter での状態管理は様々なパターンが存在しており、当社でもプロジェクトによって利用しているパターンが異なっております。その中で、今回は状態管理で最近よく使われているRiverpodについて記載していきたいと思います。第1弾の記事については、こちらを参照ください。
(Flutter#1 〜ARISE analyticsのプロジェクトでFlutterを採用してみました〜 | 株式会社ARISE analytics(アライズ アナリティクス) )
状態管理パターンとは?
以下の動作を管理することを状態管理パターンといいます。
- 別々の画面で状態(値)を共有することができる
- 状態の変更に応じて、UIが更新される
Flutterでの状態管理パターン
現在Flutterでは、全てではありませんが以下のような状態管理パターンが使われております。今回はその中でも比較的新しくて、最近よく使われている StateNotifier + freezed + Riverpod + FlutterHooks のパターンについて紹介していきます。
- StatefulWidget
- InheritedWidget
- BLoC + Provider
- ChangeNotifier + Provider
- StateNotifier + freezed + Provider
- StateNotifier + freezed + Riverpod + Flutter Hooks (今回の紹介内容)
StateNotifier
ChangeNotifierでは、状態の変更を通知するために、状態を変更するたびに notifylisteners関数を呼ぶ必要があります。この煩わしさを解消してくれたのが、StateNotifierになります。
StateNotifierは、一つの状態(state)しか持つことができません。そのため、複数の状態を管理したい場合は、オブジェクトを作成して管理することになります。オブジェクトに作成については、後述するfreezedを参照ください。
下記は、カウンターの例になります。stateとしてint型を宣言し、0で初期化します。increment関数が呼ばれるたびにstateに1が足され、自動的にリスナーに変更が通知されます。
※使用している言語はdart
class CounterStateNotifier extends StateNotifier<int> {
CounterStateNotifier(): super(0);
void increment() {
state++;
}
}
freezed
StateNotifierで利用するState用のクラスをimmutableにすると、StateNotifier側の記述が冗長になってしまいます。これを解決するのがfreezedパッケージになります。(freezed – Dart API docs )
State用に作成したimmutableなクラスの例になります。
@immutable
class CounterState {
CounterState({
this.count = 0,
this.isEnabled = true,
});
final int count;
final bool isEnabled;
}
StateNotifierでStateを変更するたびにオブジェクトを再作成する必要があり、countのみを更新したいのにisEnableも定義する必要があり、記述が冗長になってしまいます。
class CounterStateNotifier extends StateNotifier<CounterState> {
CounterStateNotifier(): super(CounterStateNotifier());
void increment() {
state = CounterState(
count: state.count + 1,
isEnabled: state.isEnabled,
);
}
void disableCounter() {
state = CounterState(
count: state.count,
isEnabled: false,
);
}
}
freezedは、後述するRiverpodの作者が作成したパッケージになります。freezedを利用することで様々な関数を自動生成してくれます。これらを利用することで、冗長なコードが簡潔になります。
- copyWith
- Jsonのパース
- ==
- toString()
- 遅延初期化など
freezedを利用したState用のクラス例になります。
@freezed class CounterState with $_CounterState { factory CounterState({ int? count, bool? isEnabled, }) = _CounterState; }
ターミナルで以下のコマンドを実行すると、@freezedが付ているクラスを自動で検出し、コードを自動で生成してくれます。
flutter pub run build_runner build --delete--conflicting-outputs
freezedを用いた場合のStateNotifierは、先ほどのコードに比べると記述が簡潔になっていることが分かると思います。
class CounterStateNotifier extends StateNotifier<CounterState> {
CounterStateNotifier(): super(CounterState(count: 0, isEnabled: true));
void increment() {
state = state.copyWith(count: state.count + 1);
}
void disableCounter() {
state = state.copyWith(isEnabled: false);
}
}
Riverpod
Riverpodとは、状態管理パッケージで、Providerと同じ開発者が作成したものになります。Providerの欠点を補った改良版のProviderです。Riverpodという名前もProviderのアナグラムになっております。(Provider, but different | Riverpod )
Riverpodは、3種類あります。Riverpodの開発者は、hooks_riverpodの利用を推奨しております。hooks_riverpodで利用するFlutter Hooksについては、後述で説明します。
Riverpodのメリット・デメリットとしては以下があげられます。
Flutter Hooks
React HooksのFlutter版になります。こちらもRiverpodの開発者が作成しており、Riverpodとの併用を推奨しております。Hook Widgetを継承することで、便利なuseXXX関数を利用することができます。(flutter_hooks – Dart API docs )
Flutter HooksとRiverpodを組み合わせることで、state取得時にselectを利用することができます。
Flutter Hooks を使わない場合、ConsumerWidgetを継承するか、Consumerを利用することになります。
final provider = StateNotifierProvider< CounterStateNotifier, CounterState>((ref) => CounterStateNotifier());
class CounterWidget extends ConsumerWiddget {
@override
Widget build(BuildContext, context, ScopedReader watch) {
final CounterStateNotifier notifier = watch(provider.notifier);
final CounterState state = watch(provider);
}
}
FlutterHookを利用する場合、HookWidgetを継承してuseProviderを利用することになります。状態が変更された際に自動で再ビルドされます。
final provider = StateNotifierProvider< CounterStateNotifier, CounterState>((ref) => CounterStateNotifier());
class CounterWidget extends HookWidget {
@override
Widget build(BuildContext, context) {
final CounterStateNotifier notifier = useProvider(provider.notifier) ;
final CounterState state = useProvider(provider);
}
}
おわりに
今回はFlutterの状態管理パターンであるStateNotifier + freezed + Riverpod + Flutter Hooks について紹介しました。今回の例は、一つのパターンに過ぎず、今後多くのパターンが登場されると思います。その中でお気に入りのパターンを見つけていただければと思います。