SignalProvider使ってみた
What SignalProvider?
Signalsという状態管理ライブラリに最近興味があり試しています。新しい機能に、SignalProviderというものがあり使ってみました。
SignalProvider is an InheritedNotifier widget that allows you to pass signals around the widget tree.
SignalProviderはInheritedNotifierウィジェットで、ウィジェットツリー上でシグナルを受け渡すことができます。
InheritedNotifier<T extends Listenable> class
An inherited widget for a Listenable notifier, which updates its dependencies when the notifier is triggered.
This is a variant of InheritedWidget, specialized for subclasses of Listenable, such as ChangeNotifier or ValueNotifier.
Dependents are notified whenever the notifier sends notifications, or whenever the identity of the notifier changes.
Multiple notifications are coalesced, so that dependents only rebuild once even if the notifier fires multiple times between two frames.
Typically this class is subclassed with a class that provides an of static method that calls BuildContext.dependOnInheritedWidgetOfExactType with that class.
The updateShouldNotify method may also be overridden, to change the logic in the cases where notifier itself is changed. The updateShouldNotify method is called with the old notifier in the case of the notifier being changed. When it returns true, the dependents are marked as needing to be rebuilt this frame.
ListenableProvider
は、 Listenable
のサブクラス (例えば ChangeNotifier
や ValueNotifier
) を監視するための継承ウィジェットです。このウィジェットは、監視対象の Listenable
が通知を送信したり、その Listenable
の識別子が変更された際に、依存しているウィジェットを更新します。
複数の通知が集約されるため、 Listenable
が連続して通知を送っても、依存ウィジェットは1回だけ再構築されます。
通常、このクラスはサブクラス化されて、 of
というstaticメソッドを提供します。このメソッドは BuildContext
から該当するタイプの InheritedWidget
を取得するためのものです。
また、 updateShouldNotify
メソッドをオーバーライドすることで、 Listenable
自体が変更された際の更新ロジックを変更することができます。 updateShouldNotify
メソッドは、 Listenable
が変更された際に呼び出され、前の Listenable
が引数として渡されます。メソッドが true
を返すと、依存ウィジェットは今フレームで再構築されます。
公式のカウンターでの使用例
providerというパッケージに書き方がそっくりですね。昔のFlutterはこれで状態管理をすることが多かった。私がエンジニアになった頃は、hooks_riverpodが流行っていたのでこちらに乗り換えましたね。
import 'package:signals/signals_flutter.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const Example(),
);
}
}
class Counter extends FlutterSignal<int> {
Counter([super.value = 0]);
void increment() => value++;
}
class Example extends StatelessWidget {
const Example({super.key});
Widget build(BuildContext context) {
return SignalProvider<Counter>(
create: () => Counter(0),
child: Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Builder(builder: (context) {
final counter = SignalProvider.of<Counter>(context);
return Text(
'$counter',
style: Theme.of(context).textTheme.headlineMedium,
);
}),
],
),
),
floatingActionButton: Builder(builder: (context) {
final counter = SignalProvider.of<Counter>(context, listen: false)!;
return FloatingActionButton(
onPressed: counter.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
);
}),
),
);
}
}
checkboxの使用例
.value
と書くところは、flutter_hooksにそっくりですね。
import 'package:signals/signals_flutter.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const Example(),
);
}
}
class CheckSignal extends FlutterSignal<bool> {
CheckSignal([super.value = false]);
void toggle() => value = !value;
}
class Example extends StatelessWidget {
const Example({super.key});
Widget build(BuildContext context) {
return SignalProvider<CheckSignal>(
create: () => CheckSignal(false),
child: Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('CheckBox'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Builder(builder: (context) {
final counter = SignalProvider.of<CheckSignal>(context);
return Checkbox(
value: counter?.value,
onChanged: (value) {
if (value != null) {
counter?.value = value;
}
});
}),
],
),
),
floatingActionButton: Builder(builder: (context) {
final check = SignalProvider.of<CheckSignal>(context, listen: false)!;
return FloatingActionButton(
onPressed: check.toggle,
tooltip: 'Increment',
child: const Icon(Icons.add),
);
}),
),
);
}
}
これだけだと面白くないので、昔YouTubeの動画で、Providerを使用してBottomNavigationBar classの状態管理をしたものがあったので、SignalProviderでもできるのではと思いやってみた。
import 'package:signals/signals_flutter.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const Example(),
);
}
}
class TabIndex extends FlutterSignal<int> {
TabIndex([super.value = 0]);
void changeTab(int index) {
value = index;
}
}
class Example extends StatelessWidget {
const Example({super.key});
Widget build(BuildContext context) {
return SignalProvider<TabIndex>(
create: () => TabIndex(0),
child: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Bottom Navigation Demo'),
),
body: Watch((context) {
final tabIndex = SignalProvider.of<TabIndex>(context);
return IndexedStack(
index: tabIndex!.value,
children: const [
Center(child: Text('Home Page', style: TextStyle(fontSize: 24))),
Center(child: Text('Search Page', style: TextStyle(fontSize: 24))),
Center(child: Text('Profile Page', style: TextStyle(fontSize: 24))),
],
);
}),
bottomNavigationBar: Watch((context) {
final tabIndex = SignalProvider.of<TabIndex>(context);
return BottomNavigationBar(
backgroundColor: Colors.blueAccent,
currentIndex: tabIndex!.value,
onTap: tabIndex.changeTab,
items: const [
BottomNavigationBarItem(
tooltip: 'Home',
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
tooltip: 'Search',
icon: Icon(Icons.search),
label: 'Search',
),
BottomNavigationBarItem(
tooltip: 'Profile',
icon: Icon(Icons.person),
label: 'Profile',
),
],
);
}),
);
}
}
最後に
登場してまだ9ヶ月のこのパッケージを使いこなすのはまだできておりません。情報もあまりないので、手探りでやっていますね。最近作った個人アプリに試しに導入してみました。flutter_hooksに似てますが、Widgetの外でも使えるのに魅力を感じて色々と試してみたいと思い気になる機能があれば実験してますね。
Discussion