Riverpod + StateNotifier + FreezedでState管理
Flutter アプリでの State 管理の話
こんにちは!CURUCUR の笠原です!
CURUCURU で運営している妊活ボイスにはアプリがあり、アプリは Flutter で開発しています。
「Flutter」「状態管理」などと検索すると多くの情報がヒットします。
- StatefulWidget パターン
- InheritedWidget パターン
- Provider パターン
- Provider + StateNotifier パターン
- Riverpod パターン
この中で最もオーソドックスなのが StatefulWidget パターンです。
class CounterPage extends StatefulWidget {
const CounterPage({Key? key}) : super(key:key);
@override
CounterPageState createState() => CounterPageState();
}
class CounterPageState extends State<CounterPage> {
int _count = 0;
@override
Widget build(BuildContext context) => Column(
children: [
Text(_counter),
OutlinedButton(onPressed: () => _increment(), child: Text('+')),
],
);
void _increment() {
setState(() => _count += 1);
}
}
setState()
を使ってフィールドの値を更新することで CounterPage
以下の Widget が再描画されます。
とてもシンプルで使え、簡単なアプリであればこれで十分な場合もあり妊活ボイスも当初は setState で State を管理していました。
しかし、シンプルすぎるゆえにいくつかの問題が発生しました。
const
を指定しない全ての子 Widget が再描画されるので描画が遅くなることがある- コンストラクタの引数で state を渡していくやり方なので、引数が増えると管理が面倒
- ロジックと View が同じファイルに書かれるため可読性が悪くなる
Provider の場合
InheritedWidget
は祖先 Widget の状態を効率的に取得でき、Provider
を使えば内部的に InheritedWidget を使うことになります。
Provider は ChangeNotifier
状態クラスを作成することでロジックと View を分離することができ、ソースの可読性などの問題が改善されています。
例えば以下のようなコードになります。
class CounterStore with ChangeNotifier {
var count = 0;
void increment() {
count += 1;
notifyListeners();
}
}
これはかなり便利で一時期は flutter の公式も押していましたがこちらにもいくつかの問題があり、
特に変更のたびに notifyListeners()
を呼び出すというのが面倒でした。
Riverpod の登場
setState() による問題を改善した Provider、さらにその Provider の問題を解決するべく登場したのが Riverpod
です。
Riverpod は Provider の作者自身が開発しています。
使い方も簡単で
- State をラップした Provider を global に定義
- Widget 側から Provider を指定して
read
orwatch
で State を取得
プロバイダについて詳しい説明はこちらを参照 https://riverpod.dev/ja/docs/concepts/providers
StateNotifierProvider で State 管理
StateNotifierProvider
は Riverpod が推奨する State 管理のソリューションです。
StateNotifierProvider を使うことで得られるメリットは2つです。
- Immutable な State を公開する(イベントによって変わることはある)
- State を変更するためのロジックを一元管理で保守性が高まる
StateNotifierProvider は StateNotifier
という state を immutable に関するための状態管理クラスを使います。
Provider の項目で登場した ChangeNotifier とは異なり変更時に明示的な通知が不要です。
Riverpod とStateNotifierProvider でロジックと View の分離、そして簡潔なコードが書けるようになりました。 しかし新たな問題として state の更新のたびに新たな state オブジェクトを作成し、プロパティを変更して書き込みという手順が必要になりました。
Freezed で immutable なモデルの生成
Freezed は Riverpod と同じ作者が開発している immutable オブジェクトのコードジェネレータです。 build_runner と合わせて使うことで immutable モデルを定義し、JSON converter も自動で生成することができます。
実際に使ってみる
Riverpod + StateNotifier + Freezed を使えばもろもろの問題が解決することがわかったところで、実際にコードを書いてみます。
ディレクトリ構成
myapp
├── build.yaml
└── lib
├── app.dart
├── main.dart
├── models
│ └── todo_state.dart
└── notifiers
└── todos_notifier.dart
build.yaml
- build_runner を動かすための設定
- include と exclude を設定することで高速化できる
json_serializable
でシリアライズする設定を行っている
targets:
$default:
builders:
freezed:
enabled: true
generate_for:
exclude:
- test
include:
- lib/**/*state.dart
json_serializable:
enabled: true
generate_for:
exclude:
- test
include:
- lib/**/*state.dart
options:
# Options configure how source code is generated for every
# `@JsonSerializable`-annotated class in the package.
#
# The default value for each is listed.
#
# For usage information, reference the corresponding field in
# `JsonSerializableGenerator`.
any_map: false
checked: true
create_factory: true
create_to_json: true
disallow_unrecognized_keys: false
explicit_to_json: true
field_rename: pascal
generic_argument_factories: false
ignore_unannotated: false
include_if_null: true
lib/models/todo_state.dart
// freezed によって生成されるファイル filename.freezed.dart で宣言
part 'todo_state.freezed.dart';
// json serialize が必要なら指定 filename.g.dart で宣言
part 'todo_state.g.dart';
@freezed
class TodoState with _$TodoState {
const TodoState._();
const factory TodoState({
@Default('') String id,
@Default('') String text,
@Default(false) bool done,
}) = _TodoState;
factory TodoState.fromJson(Map<String, dynamic> json) =>
_$TodoStateFromJson(json);
}
ファイル作成時には *.freezed.dart と *.g.dart ファイルが存在しないのでエラーが発生するが、build_runner を実行すればエラーは解消される
$ flutter packages pub run build_runner build --delete-conflicting-outputs
lib/notifiers/todos_notifier.dart
final todosProvider =>
StateNotifierProvider<TodosNotifier, List<TodoState>>((ref) =>
TodosNotifier(ref.read)
});
class TodosNotifier extends StateNotifier<List<TodoState>> {
TodosNotifier(this.read) : super([]);
void set(List<TodoState> items) {
state = items;
}
void add(TodoState item) {
state = [...state, item];
}
void remove(String id) {
state = state.where((t) => t.id != id).toList();
}
void toggleDone(String id) {
state = state
.map((t) => t.id == id ? t.copyWith(done: !t.done) : t)
.toList();
}
}
lib/main.dart
Future<void> main() async {
// ~~~ 初期化処理 ~~~
runApp(
// ProviderScope 以下の Widget で Riverpod が有効になるになる
ProviderScope(child: App()),
);
}
lib/app.dart
class App extends StatelessWidget {
const App({Key? key}) : super(key:key);
@override
Widget build(BuildContext context) => Column(
children, [
TodoForm(),
Expanded(child: TodoItems()),
],
);
}
class TodoForm class ConsumerStatefulWidget {
const TodoForm({Key? key}) : super(key:key);
@override
TodoFormState createState() => TodoFormState();
}
class TodoFormState class ConsumerState<TodoForm> {
String text;
@override
Widget build(BuildContext context) => Row(
children: [
TextField(onChange: handleChange),
OutlineButton(
onPressed: () => submit(),
child: Text('送信'),
)
],
);
void submit() {
final newTodo = TodoState(id: 'id0', text: text, done: false);
// providerName.notifier で notifier のメソッド呼び出し
ref.read(todosProvider.notifier).add(newTodo);
setState(() => text = '');
}
void handleChange(String s) {
setState(() => text = s);
}
}
class TodoItems extends ConsumerWidget {
const TodoItems({Key? key}) : super(key:key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// watch で変更を検知してこの Widget 以下の Widget を再描画
// watch をどこで宣言するかによって再描画する範囲を制御できる
ref.watch(todosProvider);
return ListView.builder(
itemBuilder: TodoItemBuilder, // Todo item Widget を削除したり done できる
itemCount: todosProvider.length,
),
}
}
省いている部分もありますが以上の方法で効率的に State を管理でき、ソースも見やすくなります。 妊活ボイスでは主要ページから Riverpod を使うように書き直している最中です。
最後に
CURUCURU では新しい技術やサービス作りが好きなエンジニアを募集中です! 「ITで女性のライフスタイルを豊かにする」というミッションの元やりがいのあるサービス作りに携わる事ができます。
CURUCURU でエンジニアとして働くことに興味がある方はよければオンラインでカジュアルにお話しましょう! https://www.wantedly.com/companies/curucuru/projects