【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が呼ばれる度にsubscribeも呼ばれてしまう
counter.subscribe((value) {
print(value); // 0、1、2、2、3、3、3…
});
// effectも同様
effect(() {
// counterの値が一回更新されるだけで何回もprintが呼ばれてしまう
print(counter); // 0、1、2、2、3、3、3…
});
return ...
}
また、Streamのlistenど同様に、どこかで受け取りを止めないと別の画面に移動しても受け取る処理が残ってしまいます。
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の代わりにlistenSignalを使用します。
ただしlistenSignalはbuildメソッドの中で書けますが、createEffectは主にinitStateで用います。
いくつものSignalの通知を監視する仕組み上、まだ難しいのかもしれません。
class ExamplePageState extends State<ExamplePage> with SignalsMixin {
late final counter = signal(0);
void initState() {
// effectの代わりだが、buildメソッドの中で書けないのはそのまま
createEffect(() {
print(counter); // 0、1、2、3…
});
super.initState();
}
build(BuildContext context) {
// このWidgetが再描画され、再度呼ばれても一回分のみ通知を受け取る
// subscribeの代わり
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は設計も素晴らしいのですが、コードが洗練されているので読んでいて勉強になります。
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.27.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 17025dd882 (3 weeks ago) • 2024-12-17 03:23:09 +0900
Engine • revision cb4b5fff73
Tools • Dart 3.6.0 • DevTools 2.40.2
Discussion