Flutterにおける状態管理
RiverpodとFreezedを用いた状態管理まとめ
想定読者
- Flutterの最低限の知識がある
- Providerにおける状態管理の構造を理解している
必ずしもProviderを理解しておく必要はないが、Providerを理解していると、よりRiverpodも理解しやすくなる - RiverpodでstateProviderでの状態管理を理解している。
その上で、StateNotifierの理解に苦しんでいる方
導入方法
Riverpodの導入
Riverpodの導入方法はRiverpod公式Documentを参照してください。
pubspec.yamlを編集してもいいが、以下のようにターミナルで行うと便利
$ flutter pub add 追加したいパッケージ名
Freezedの導入
importするのはfreezed_annotaionのみで、下2つはbuildするときに必要となる。
$ flutter pub add freezed_annotation
$ flutter pub add --dev freezed
$ flutter pub add --dev build_runner
StateNotifier における状態管理の構造
大きく分けて3つのclassを用いる
- 状態変数をまとめたclass
- 状態の操作をするclass
- UIから状態にアクセスすることを可能にするclass
これらを簡単にまとると以下のようになる
-
状態変数をまとめたclass
このclass内で状態変数を定義する。このclassは状態変数の保守性を高めるためにimmutableに宣言する。immutableであるが、もちろんイベント(2で定義する)を応じてかえることができる。 -
状態を操作をするclass
このclassでは1で定義した状態変数を変更するメソッドを定義する。
StateNotifierを継承する。
stateで、1で定義された状態変数にアクセスすることができる。
changeNotifierとは異なり値の変更を通知する際に、NotifyListers()が不要で
stateが更新されると自動的に値の変更を通知する。
1で書いたように、stateはimmutableであるため、freezedパッケージを持ちいない場合は新たに1のclassを作り直す必要があり、コードが冗長になる。 -
UIから状態にアクセスすることを可能にするclass
StateNotifierProviderを継承する。
UIから状態にアクセスする際には、このclassを通じて行う
実装
例としてユーザー情報を管理するStateNotifierを作ってみる。
Providerの実装
管理する状態変数は
- UserName
- age
の3つをとする。
import 'package:freezed_annotation/freezed_annotation.dart';
// ファイル名.freezed.dart
part 'userdataProvider.freezed.dart';
// 以下で示すbuildをするまではErrorが出ると思うが、無視して良い。
// 状態変数を定義するclass
@freezed
// _$UserDataをmixinし、コンストラクタに_UserDataを代入する
class UserData with _$UserData {
const factory UserData({
// @Default(初期値) 型 変数名
// @Defaultは不要であるが、NullSafety対策にもなる
@Dafault('') String userName,
@Default('') String email,
@Default(0) int age,
}) = _UserData;
}
次にGeneratorを走らせる。UserData.freezed.dartというファイルが生成されエラーが解消される。
(UserData classに変更を加える度にこのコマンドを走らせる必要がある)
Freezedに関連するエラー以外がある場合にはうまくいかない。
$ flutter pub run build_runner build --delete-conflicting-outputs
次に状態を操作をするclassは次のようになる
// StateNotifierを継承する
class UserDataNotifier extends StateNotifier<UserData>{
// 初期化
UserDataNotifier() : super(const UserData());
// 状態を変更するメソッド
// Usernameを変更するメソッド
void changeUserName(String newUserName) {
// freezedパッケージによりcopywithが使える。
// stateはimmutableであるが、copywithを使うことで、簡単に書ける。
state = state.copywith(userName: newUserName);
}
// 年齢を変更するメソッド
// 上と同様に書ける
void changeAge(int newAge) {
state = state.copywith(age: newAge);
}
// ...
}
最後にStateNotifierProviderの実装
// providerはglobalで宣言 immutableのため特に気にする必要はない
// StateNotifierProvider<2のclass, 1のclass>
// return 2のclass
final userDataProvider = StateNotifierProvider<UserDataNotifier, UserData>((ref) {
return UserDataNotifier();
});
UIに表示させる
次にUI側で状態変数にアクセスしたり、状態変数を変更したりする部分の実装。
そのためには、ref オブジェクトを参照する必要があり、そのためのWidgetが以下のようにriverpodには用意されている。
Flutter | Riverpod |
---|---|
StatelessWidget | ConsumerWidget |
StatefulWdiget | ConsumerStatefulWidget |
State | ConsumerState |
これらのWidgetは、従来のFlutterのWidgetと大差はないが、buildメソッドに対してBuildContextに加え、WidgetRefを引数として追加する。
また、ConsumerというWidgetが用意されており、refオブジェクトに参照するためにclassを作らなくてもよい。
ref.watch, ref.read, ref.listenについても触れておく
-
ref.watch
状態変数を監視し、変化するたびにWidgetをRebuildする。
initStateや、onPressed内といった非同期処理ではwatchは使われるべきではなく、その代わりにref.readを使う -
ref.read
状態変数は監視しないが、その時点でのstateを取得することができる。
普通、ボタンが押されたときの処理などに使われる。
公式documentによると、ref.readはできるだけ使用を避けるべきとのこと。 -
ref.listen
基本はref.watchと同じであるが、ref.listenは任意の関数を呼び出すことができる
注)今回は扱わない
以下では、ConsumeWidgetを継承したUserNameを変更するclassと、Consumerを使った年齢を表示するWidgetである。
class UserSettings extends ConsumerWidget {
const UserSettings({Key? key}) ; super(key:key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
children:[
// ...
TextFormField(
// ...
onChanged:(String value){
// 値を変更するときは、次の様に行う
// ref.read(StateNotifierProviderを継承するclass.notifier).メソッド
ref.read(UserDataProvider.notifier).changeUserName(value);
},
// ...
),
// ...
],
);
}
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) ; super(key:key);
@override
Widget build(BuildContext context) {
return Scaffold(
body:Center(
child:Column(
children:[
// ...
Consumer(
builder: (context, ref, child) {
return Text(
// 状態変数にアクセスしたいときは、
// ref.watch(Provider).状態変数名
// 値が変更されたときにrebuildされるのはConsumer内のWidgetである、
// できるだけConsumerは小さく。
ref.watch(userDataProvider).userName,
);
}
),
// ...
],
);
);
);
}
}
参考文献
Riverpod公式Document
freezed Dart Package
まとめ
自分が、StateNotifierを理解するのに苦労したため、まとめてみた。
StateNotifierProviderが自分で実装できるようになると、より複雑な状態管理が必要になっても、シンプルな形で書けるようになり非常に便利であると感じた。
今後、さらにhooksを加えて、
Riverpod x Freezed x Hooks
などで状態管理もしてみたい