signals Watch.builder を使ってみた
StatelessWidgetでも状態管理ができた?
最近話題のSignalsなるものを試してみている。あまり情報がないニッチなパッケージなので、記事書いて見ようと思いました。
最近仲良くさせていただいているaq
さんという方もご興味あるみたいで、情報交換しております。UIから、ロジックを分離させて、ViewModelのように使う方法もあるとか?
flutter_hooksに似てるけど、あれとの違いって、HookWidgetの中でしか使えないところかな。signalsはどこでも使えるみたい?
2個記事を書いたけど、StatefulWidgetでしか状態管理してなかった😅
他に方法があるみたいだ。
Watch.builderなるものを使えば実現することができる。
Watch
Watch
To watch a signal for changes in Flutter, use the Watch widget. This will only rebuild this widget method and not the entire widget tree.
ウォッチ
Flutterでシグナルの変更を監視するには、Watchウィジェットを使います。これはウィジェットツリー全体ではなく、このウィジェットメソッドのみを再構築します。
final signal = signal(10);
...
Widget build(BuildContext context) {
return Watch((context) => Text('$signal'));
}
This will also automatically unsubscribe when the widget is disposed.
Any inherited widgets referenced to inside the Watch scope will be subscribed to for updates (MediaQuery, Theme, etc.) and retrigger the builder method.
There is also a drop in replacement for builder:
また、ウィジェットが破棄されると、自動的に購読が解除されます。
Watch スコープの内部で参照されている継承されたウィジェットは、更新(MediaQuery、Theme など)に対してサブスクライブされ、builder メソッドを再トリガーします。
builder のドロップイン置き換えもあります:
こちらの動画が参考になった
今回はこちらのパッケージを追加して使ってみましょう。
サンプルコード使う時は、main.dart
でインポートして、試してみてください。
import 'package:flutter/material.dart';
import 'package:signals_app/http_view.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 HttpView(),
);
}
}
カウンターを作ってみた
入門レベルのカウンターだと、プロバイダーのようなグローバル変数かな。これを定義して、Watch.builderの中で、参照すると、値を取得できる。
状態の変更を監視してくれていて、更新することもできる。Watch.builderは、Widgetを再構築してくれるそうだ。
import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';
// global variable
final counter = signal<int>(0);
class CounterView extends StatelessWidget {
const CounterView({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Watch.builder'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
counter.value++;
},
child: const Icon(Icons.add),
),
body: Center(
// state watch
child: Watch.builder(builder: (context) {
return Text('Counter: $counter');
}),
),
);
}
}
リストの表示・チェックボックス
動画の内容を簡単にしたものですが、リストを表示して、チェックボックスの切り替えをするサンプルです。null checkをして、タップすると、状態を更新。切り替えができます。
本当は、データベースに保存するなりして、記録しておいた方がいいんですけどね。練習ってことで
import 'package:flutter/material.dart';
import 'package:signals/signals.dart';
import 'package:signals/signals_flutter.dart';
typedef Todo = ({
String label,
bool completed,
});
final List<Todo> initialState = [
(label: 'Learn Flutter', completed: true),
(label: 'Learn Dart', completed: false),
(label: 'Learn Signals', completed: false),
];
final todos = <Todo>[...initialState].toSignal();
class TodoView extends StatelessWidget {
const TodoView({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Watch.builder'),
),
body: Center(
// state watch
child: Watch.builder(builder: (context) {
return ListView.builder(
itemCount: todos.value.length,
itemBuilder: (context, index) {
final todo = todos[index];
return Card(
child: ListTile(
title: Text(todo.label),
trailing: Checkbox(
value: todo.completed,
onChanged: (value) {
// nullを返さないようにする
if(value != null) {
todos[index] = (label: todo.label, completed: value);
}
},
),
),
);
},
);
}),
),
);
}
}
APIからデータを取得
ジョークAPIなるものを動画では、使ってるけど、よく使うリストにデータ表示する方をやりたかった。複数のデータ表示するときは、?つけないとエラーにハマったりした😅
AsyncStateを使って、jsonplaceholderから、ダミーのユーザーデータの取得をやってみました。
AsyncState
AsyncState is class commonly used with Future/Stream signals to represent the states the signal can be in.
AsyncSignal
AsyncState is the default state if you want to create a AsyncSignal directly:
非同期ステート
AsyncState は、Future/Stream シグナルでよく使用されるクラスで、シグナルが取り得る状態を表します。
AsyncSignal
AsyncState は、AsyncSignal を直接作成する場合のデフォルトの状態です:
.hasValue
Returns true if a value has been set regardless of the state.
.hasValue
状態に関係なく値が設定されている場合に真を返す。
final s = asyncSignal<int>(AsyncState.loading());
print(s.hasValue); // false
s.value = AsyncState.data(1);
print(s.hasValue); // true
.hasError
Returns true if a error has been set regardless of the state.
.hasError
状態に関係なくエラーが設定されている場合にtrueを返す。
final s = asyncSignal<int>(AsyncState.loading());
print(s.hasError); // false
s.value = AsyncState.error('error', null);
print(s.hasError); // true
.isReloading
Returns true if the state is reloading with having a value or error, and is the loading state.
.isReloading
値があるかエラーがある状態でリロード中であり、ローディング状態である場合に真を返します。
final s = asyncSignal<int>(AsyncState.loading());
print(s.isReloading); // false
s.value = AsyncState.loading(data: 1);
print(s.isReloading); // true
s.value = AsyncState.loading(error: ('error', null));
print(s.isReloading); // true
FutureBuilder, FutureProviderみたいなものですね。
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:signals/signals_flutter.dart';
const url = 'https://jsonplaceholder.typicode.com/users';
class User {
final String name;
final String email;
User({required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
name: json['name'],
email: json['email'],
);
}
}
final u = asyncSignal<List<User>>(AsyncState.data([]));
Future<void> getUser() async {
u.value = AsyncState.loading();
try {
final response = await http.get(Uri.parse(url));
final List<dynamic> data = jsonDecode(response.body);
if (data.isEmpty) {
u.value = AsyncState.error('No data', null);
return;
}
final users = data.map((json) => User.fromJson(json)).toList();
u.value = AsyncState.data(users);
} catch (e) {
u.value = AsyncState.error('Exception: $e', null);
}
}
class HttpView extends StatelessWidget {
const HttpView({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Watch.builder'),
),
body: Center(
child: Column(
children: [
IconButton(
onPressed: () {
getUser();
},
icon: const Icon(Icons.refresh),
),
Expanded(
child: Watch.builder(builder: (context) {
if (u.value.isLoading) {
return const CircularProgressIndicator();
}
if (u.value.hasError) {
return Text('Error: ${u.value.error}');
}
final users = u.value.value;
if (users!.isEmpty) {
return const Text('No data');
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text('名前: ${user.name}'),
subtitle: Text('メール: ${user.email}'),
);
},
);
}),
),
],
),
),
);
}
}
icon buttonをタップすると、こんな感じでデータを取得できます。
まとめ
シンプルに状態管理ができるライブラリだった。今後国内で流行るかはわからないですね。ですが、riverpod以外の選択肢を作ることができるかもしれないですね。どうしてもriverpodだと、覚えることが多すぎて、Flutterを始めたばかりの人には、キツイ、学習コストが高い💦
と言って、使うのは、強制されるので、避けては通れません。
でも個人アプリだったら、使っていいはず。どこまで、複雑な状態管理ができるかは分かりませんが🤔
Discussion