providerパッケージの作者であるRemi Rousseletさんが今月リリースした
flutter_state_notifierパッケージが日本のFlutter界隈でも話題になっていますね
state_notifier、日本人の貢献が活発🇯🇵🙌 with @heavenOSK https://t.co/dG8HCs0QFH pic.twitter.com/gtian8p8yd
— mono 🎯 @自宅 💙 (@_mono) March 8, 2020
日本で有名なFlutterユーザの方達もコミットされていますね
まだ BLoC で消耗してるの?というブログもありましたが、
change_notifier
ベースの書き方より気持ちいいので、今後はstate_notifier
ベースが増えると思います
使い方
pubspec.yaml
stateを同作者によるfreezedを使って書くと気持ちがいいです
僕が書いたfreezedの雑な解説記事はこちらにあります
ちなみに、state_notifierはFlutterに依存しないパッケージで、flutter_state_notifierがFlutterと統合されたものになっています(具体的にはProviderやLocatorMixinを挟み込む部分をやってくれている)
ので、Flutterで用いる場合は後者で
dependencies:
flutter:
sdk: flutter
freezed_annotation:
flutter_state_notifier:
dev_dependencies:
flutter_test:
sdk: flutter
build_runner:
freezed:
基本的な考え方
- ViewにはViewのことしか書きたくない
- ネストしたくない(StreamBuidlerやProvider.Consumer/Selectorを使いたくない)
- 安全に状態を管理したい(Viewからは直接いじれないようにしたい)
書き方の例
HogeStateNotifier
の部分の命名はViewModel
だったりController
だったり色々考えられると思うのですが、
いったん本家のexampleに合わせました
好みで変えていいと思います
命名について追記
Counter vs CounterNotifier works too
— Remi Rousselet (@remi_rousselet) March 16, 2020
- hoge_page.dart
-
HogePage
- 状態を反映したビュー
-
- hoge_state.dart
-
HogeState
- 状態を持つイミュータブルなクラス
-
HogeStateNotifier
- Stateを操作するクラス
-
- hoge_state.freezed.dart
- 自動生成される
以下、それぞれのクラスについて見ていきます
(仕事中にいそいそ書いてるのでコードは全て疑似コードで、ツッコミは受け付けません)
State
@freezed
abstract class HogeState with _$HogeState {
const factory HogeState({
@Default(0) int page,
@Default(false) bool isEnabled,
String content
}) = _HogeDetailState;
}
freezedベースのイミュータブルなクラスです。
そのビューについての、変更を画面に通知したい変数を集約させているイメージです
@required
を使うよりは@Default
で初期値を与えるのがクールだと思います(page = 0
でないのに注意)
後述しますが更新時はstate = state.copyWith(page: 1)
のような感じで
StateNotifier
class HogeStateNotifier extends StateNotifier<HogeDetailState> {
final int hogeId;
String draft = '';
bool _isLoading = false;
HogeStateNotifier({@required this.hogeId}): super(const HogeState()) {
fetch();
}
void setPage(int value) {
state = state.copyWith(page: value);
state = state.copyWith(isEnabled: _isEnabled);
}
Future<void> fetch() {
if (_isLoading) return;
// いい感じにfetchしてるとする
state = state.copyWith(content: content);
}
bool _isEnabled() {
// stateをもとにいい感じに計算してるとする
return true;
}
}
stateを内部に持つ、ロジック部分を担当するクラスです
ここで注目してほしいのは、
-
変更を画面に通知したいわけではない変数はStateNotifierが持っている
- ここでは
draft
、例えば遷移時に次の画面にわたす変数だとしましょう
- ここでは
- ロジックの内部でのみ使う変数もprivateでStateNotifierが持っている
- ここでは
_isLoading
- ここでは
- stateの更新は全てStateNotifierで行う。Viewから弄りたいときはアクセサを生やす
- ここでは
setPage
- ここでは
点ですね
あと、好みだと思いますが、僕は複雑な遷移処理(例えばbottomSheetを出し、そこでの反応に応じて画面遷移する、など)もStateNotifierに書いてしまっています
Viewを複雑にしたくないという気持ちがあります
Future<void> pushToWebView(BuildContext context) async {
// 遷移処理やモーダル出したりの処理
}
あとこれも好みでしょうが、孫Viewからも参照したりしそうなHogeController
の類もここで持っています(disposeを忘れないように)
Page
親のbuild
@override
Widget build(BuildContext context) {
return StateNotifierProvider<HogeStateNotifier, HogeState>(
child: const _Scaffold(),
create: (BuildContext context) => HogeStateNotifier(hogeId: widget.hogeId),
);
}
子での参照
以下を使い分けネストとリビルドの少ないコードを書きましょう
-
context.read<HogeStateNotifier>()
- State更新時にリビルドされない
- StateNotifierのメソッドを呼びたい、Stateの値を更新したい時など
-
context.read<HogeState>()
- State更新時にリビルドされない
- Stateの現在の値を参照したい(画面遷移で渡すなど)
-
context.select<HogeState, FugaClass>((s) => s.fuga)
- State更新時、fugaが変化している場合のみリビルドされる
-
context.select((HogeState s) => s.fuga)
でもいいよ - 状態の変化をViewに反映させたいとき
-
context.watch<HogeState>()
- State更新時、常にリビルドされる
- あまり使わないイメージ
スニペット
AndroidStudioでは以下をLive Templateに登録しておくと開発が捗りそう
命名に応じて変えて下さい
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:state_notifier/state_notifier.dart';
part '$FILE_NAME$_state.freezed.dart';
@freezed
abstract class $CLASS_NAME$State with _$$$CLASS_NAME$State {
const factory $CLASS_NAME$State({}) = _$CLASS_NAME$State;
}
class $CLASS_NAME$StateNotifier extends StateNotifier<$CLASS_NAME$State> {
$CLASS_NAME$StateNotifier() : super(const $CLASS_NAME$State()) {}
}
結論
StateとNotifierを分離したことがChangeNotifier
と比べた際の大きなメリットだと思います
notifyListener()
を忘れることもなくなるし…
僕はこれから徐々に業務コードのリプレイスをやっていきます
皆さんはどうですか?