【Flutter, Dart】状態管理パッケージ「Signals」の紹介
signalsという状態管理パッケージが面白い仕様だったのでご紹介します。
signalsはReactの軽量版であるPreactのSignalsに準拠しています。
Signal
int
型の値を状態管理したい場合、Flutter Hooks だと以下の書き方になります。
// StatelessWidgetやStatefulWidgetのbuildメソッド内に書く
final counter = useState(0);
// Textウィジットで表示したい時
Text("$counter");
// 値を更新したい時
counter.value += 1;
Signalsだと以下のようになります。
// 特に記述場所の制限は無いのでView側とModel側どちらでもOK
late final counter = signal(0);
// Textウィジットで表示したい時
Watch((context) => Text("$counter"));
// 値を更新したい時
counter.value += 1;
Computed
Computed
はRiverpodのProvider
や Flutter Hooks のuseMemoized
と役割が似ています。
late final counter = signal(0);
// counterが奇数だったらtrue
late final isOdd = computed(() => counter.value.isOdd);
// Textウィジットで表示したい時
Watch((context) => Text("$isOdd"));
// counterの値を更新するとisOddも再計算される
counter.value += 1;
値の変更を受け取る
Riverpodのref.listen
や Flutter Hooks のuseEffect
、useValueChanged
のように、Flutterの画面更新以外の場所で変更を受け取る方法です。
effect
effect
内の関数でvalue
を使用すると、そのSignal
の値が更新される度にその関数が実行されます。
final dipose = effect(() {
// 最初に一回、その後はcounterの値が更新される度にprintが呼ばれる
print(counter);
});
// 戻り値を実行すると変更の受け取りを終了 (今回の場合printが呼ばれなくなる)
dispose();
普通はcounter.listen
のように、何の更新を受け取るか直接指定すると思いますが、
signals
の場合はeffect
メソッド内で使われているSignal
の変更があると自動で呼ばれます。面白いですね。
二つ以上のSignal
を使った場合も、そのいずれかのSignal
が変更されると呼ばれます。
代わりに最初に一度effect
内の関数が実行されます。
今回の場合、例えばStreamのlisten
だと1、2、3…とcounterの変更が通知されますが、effect
の場合最初に0が出力され、その後1、2、3…となります。
最初に一回実行した際にどのSignal
が呼び出されたのかを検出し、そのSignal
の変更を受け取る、という仕組みなのだと思います。
subscribe
subscribe
はSignal
を指定して変更を受け取ります。
StreamやRiverpodのlisten
と同じように書きたい人用のもので、内部実装はeffect
を使用した短いコードになっています。
final dispose = counter.subscribe((value) {
// 最初に一回、その後はcounterの値が更新される度にprintが呼ばれる
print(value);
});
// 戻り値を実行すると変更の受け取りを終了 (今回の場合printが呼ばれなくなる)
dispose();
signals_flutter
パッケージを使う
先ほどのeffect
とsubscribe
をWidget
のbuild
メソッド内に書くと、画面が再描画される度に値の変更を新しく受け取るようになってしまいます。
build(BuildContext context) {
// このWidgetが再描画されてbuildが呼ばれる度にeffectも呼ばれてしまう
effect(() {
// counterの値が一回更新されるだけで何回もprintが呼ばれてしまう
print(counter); // 0、1、2、2、3、3、3…
});
// subscribeも同様
counter.subscribe((value) {
print(value); // 0、1、2、2、3、3、3…
});
return ...
}
signals_flutter
はそれを防ぐための機能が揃っているパッケージです。
使用するにはStatefulWidget
のState
にSignalsMixin
をwith
します。
class ExamplePage extends StatefulWidget {
const ExamplePage({super.key});
State<ExamplePage> createState() => ExamplePageState();
}
class ExamplePageState extends State<ExamplePage> with SignalsMixin {
...
次に、effect
の代わりにcreateEffect
を、subscribe
の代わりにthis.listenSignal
を使用します。
class ExamplePageState extends State<ExamplePage> with SignalsMixin {
late final counter = signal(0);
build(BuildContext context) {
// このWidgetが再描画され、再度呼ばれても一回分のみ通知を受け取る
// effectの代わり
createEffect(() {
print(counter); // 0、1、2、3…
});
// subscribeの代わり
this.listenSignal(counter, () {
print(counter); // 0、1、2、3…
});
return ...
}
}
良い点
- 他の状態管理パッケージと比べると短く簡潔に書ける
- Flutter Hooks と違ってWidgetの外で状態管理できる
- ただしSignalsはRiverpodなどの状態管理パッケージの仲間なので、Widget内のコードを短くするためにある Flutter Hooks とは役割が異なる
- 変更通知の回数を減らすための機能が多い
- 本記事では紹介していないが
select
、untracked
、peek
などがある
- 本記事では紹介していないが
-
runApp
の直下に管理用Widgetを置かなくても良いので、他の状態管理パッケージと併用できる- Riverpodや Flutter Hooks と共存可能
- アプリ全体の状態管理はRiverpodを使用し、
StatefulWidget
内で完結する場合はsetState
の代わりに用いるなど
おわりに
signalsには他にもautoDispose
やpreviousValue
、onDispose
など様々な機能が用意されています。
また、もちろんFutureSignal
やStreamSignal
などもあります。
Signalsは設計も素晴らしいのですが、コードが洗練されているので読んでいて勉強になります。
5.3.0 → 5.4.0 の主な変更
Signalsは5.4.0で大きな破壊的変更が行われています。
(deprecateになっているだけなので、5.3.0の記法でも動作します)
本記事は公開当初5.3.0を基に書いていたため、主要な変更点を以下に挙げていきます。
-
signals_flutter
を使用するにはState
にSignalsMixin
をwith
(Mixin)するように -
createSignal
が不要に
5.3.0のcreateSignal
がdeprecateになり、this.createSignal
が追加されました。
ただし、両者は名前こそ似ているものの全くの別物です。
5.3.0のcreateSignal
は、signal
の代わりに用いることでコードが簡潔になるものでした。
late final counter = signal(0);
late final counter2 = createSignal(0, context);
Watch((context) => Text("$counter"));
// createSignalの方がすっきりと書ける
Text("$counter2");
ただ、createSignal
とsignal
のどちらもSignal
型を返すので判別がしづらく、実質的にコードが複雑化していました。
5.4.0のthis.createSignal
とsignal
の違いは、StatefulWidget
が破棄される際にonDispose
が呼ばれるかどうかしかなく、onDispose
が不要であればsignal
だけで問題なくなりました。
ちなみに、onDispose
は React Hooks のクリーンアップに相当する機能ではありますが、個人的にSignalsの概要の説明に必ずしも必須だと感じなかったので、this.createSignal
とonDispose
の説明は本記事ではしていません。
createEffect
の追加
5.3.0ではbuild
メソッド内でeffect
を使うと不具合が起きるため、listen
という単一のSignal
の変更だけを受け取るメソッドを使う必要がありました。
5.4.0からはcreateEffect
が登場したためlisten
はdeprecatedになりました。
this.createListen
を使えば以前と同じように単一のSignal
の変更だけを受け取ることも可能です。
備考
本記事は以下の環境で検証しました
Flutter 3.24.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 5874a72aa4 (8 days ago) • 2024-08-20 16:46:00 -0500
Engine • revision c9b9d5780d
Tools • Dart 3.5.1 • DevTools 2.37.2
Discussion