Signals.dart使用介绍
本文旨在为 Dart 和 Flutter 开发者提供 Signals.dart 框架的全面介绍。Signals.dart 借鉴了现代前端响应式系统(如 SolidJS 或 Vue 3)的核心理念,提供了一种基于 Pull-Based(拉取式) 架构的、高效且极简的状态管理方案。
文档重点阐述了框架的核心 API、Flutter 集成能力以及两种主流的应用架构模式(轻量级局部状态和分层 MVC 模式),旨在帮助开发者快速掌握 Signals 的基础使用。
声明:本文部分内容由 AI 参考官方文档整理,但所有核心要点用法均经过人工核对
官方文档要点(Signals.dart Overview)
核心理念
- Pull-Based 架构:Signals 采用"拉取"模式,只有在读取值时才会触发计算(区别于传统的 push-based
notifyListeners
) - 极简更新(Minimal Updates):
- 如果从不读取某个
computed
,它的回调永远不会被调用 - 链式 computed 会缓存值,只在依赖变化且被读取时才重新计算
- 使用链表实现依赖追踪(signal boosting),性能更优
- 如果从不读取某个
示例:懒计算
final a = signal(0);
final b = computed(() => a.value + 1);
final c = computed(() => b.value + 1);
final d = computed(() => c.value + 1);
// 如果从不读取 d.value,所有回调都不会执行
print(d.value); // 3 - 此时才会计算整个链路
print(d.value); // 3 - 直接返回缓存值,无需重新计算
Core 核心 API
API | 作用 | 用法 |
---|---|---|
signal(value) | 创建响应式值 | final count = signal(0); |
computed(() => ...) | 创建派生值,自动追踪依赖 | final double = computed(() => count.value * 2); |
effect(() { ... }) | 注册副作用,依赖变化时执行 | effect(() => print(count.value)); |
untracked(() => ...) | 在回调中读取信号但不建立依赖 | untracked(() => count.value); |
batch(() { ... }) | 批量更新多个信号,只触发一次通知 | batch(() { a.value = 1; b.value = 2; }); |
Flutter 集成(重要更新)
Widget/工具 | 功能 |
---|---|
Watch((context) => ...) | 在 widget 中自动追踪依赖并重建(类似 Riverpod 的 Consumer) |
SignalProvider | 提供依赖注入能力,在 widget 树中共享信号(单个 signal) |
FlutterSignal | Flutter 专用信号,集成 Flutter 生命周期 |
FlutterComputed | Flutter 专用计算信号,自动 dispose |
ValueListenable | 将 Signal 转换为 ValueListenable(兼容旧 API) |
ValueNotifier | 将 ValueNotifier 转换为 Signal |
Hooks | 支持 Flutter Hooks(useSignal 、useComputed 等) |
核心差异:
createSignal
/createComputed
→ 自动绑定 widget 生命周期,无需手动 disposesignal
/computed
→ 全局或手动管理,需要手动 dispose(如果没有直接引用会在 GC 的时候被回收,此时不会调用 onDispose)
Mixins 扩展能力
提供了丰富的 Mixin 让自定义类支持响应式:
SignalsMixin
- 基础信号混入ValueNotifierSignalMixin
🆕 - 让 Signal 变成 ValueNotifierValueListenableSignalMixin
🆕 - 让 Signal 变成 ValueListenableIterableSignalMixin
/ListSignalMixin
/SetSignalMixin
🆕 - 集合类型响应式StreamSignalMixin
🆕 - Stream 集成
Async 异步支持
类型 | 用途 |
---|---|
AsyncState<T> | 异步状态封装(loading / data / error) |
FutureSignal | 将 Future 转为响应式状态 |
StreamSignal | 将 Stream 转为响应式状态 |
Computed Async | 异步计算信号 |
connect() | 连接异步数据源到信号 |
Value 类型信号
提供常用数据结构的响应式版本:
ListSignal<T>
- 响应式列表(支持add
、remove
等操作)MapSignal<K, V>
- 响应式 MapSetSignal<T>
- 响应式 SetIterableSignal<T>
- 响应式迭代器ChangeStack<T>
- 支持撤销/重做的信号
对于 Signals 有两种用法,一种对于简单的 Widget,直接使用在 State 中createXXX
创建并使用:
这样的好处是框架自动处理了生命周期,数据和 widget 在同一处方便处理,但是对于复杂项目可能难以维护优化,不方便共享数据。
还有一种可以使用类似 MVC 等模式,将数据,操作使用单独文件处理,然后再在 widget 中使用,
这里只介绍 MVC 模式,其余参考官方文档,比如clean_architecture。
方式一:在 Widget 中直接使用 createSignal
(官方推荐的轻量写法)
这种方式适合局部状态(如登录表单),信号直接定义在 widget 内部,生命周期与 widget 绑定。
在 widget 内用 createSignal/createComputed 管理局部状态,UI 用 Watch 或 SignalsMixin 绑定,副作用用 createEffect,这样信号会自动追踪依赖并在 widget 销毁时自动回收。
import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';
class LoginPageSimple extends StatefulWidget {
const LoginPageSimple({super.key});
@override
State<LoginPageSimple> createState() => _LoginPageSimpleState();
}
class _LoginPageSimpleState extends State<LoginPageSimple> with SignalsMixin {
// 在 widget 中直接创建局部信号:随 widget 销毁自动回收,不需要手动 dispose
late final username = createSignal('');
late final password = createSignal('');
// 基于其他信号计算派生值,自动追踪依赖
late final canLogin = createComputed(
() => username.value.isNotEmpty && password.value.isNotEmpty,
);
@override
void initState() {
super.initState();
// 在 initState 中使用 createEffect 监听信号变化(副作用)
// 会自动注销,不需要主动调用 dispose()。
// 不要在 build 等方法中调用,也不要在此方法中创建新的 signal
createEffect(() {
debugPrint('✨ Effect: username changed to "${username.value}"');
});
createEffect(() {
debugPrint('✨ Effect: canLogin changed to ${canLogin.value}');
if (canLogin.value) {
debugPrint('🎉 用户现在可以登录了!');
}
});
}
@override
Widget build(BuildContext context) {
// 注意:因为用了 SignalsMixin,这里直接用 .value 就会被追踪
return Scaffold(
appBar: AppBar(title: const Text("登录 - SignalsMixin 用法")),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
decoration: const InputDecoration(labelText: '用户名'),
onChanged: (v) => username.value = v,
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(labelText: '密码'),
onChanged: (v) => password.value = v,
),
const SizedBox(height: 20),
// ✅ 直接使用 .value,不需要 Watch
ElevatedButton(
onPressed: canLogin.value ? _login : null,
child: const Text("登录按钮(SignalsMixin)"),
),
const SizedBox(height: 12),
],
),
),
);
}
void _login() {
debugPrint("🔐 登录中: ${username.value}/${password.value}");
}
}
方式二:View + Controller + State 分层架构
这种方式更接近传统 MVC/MVVM,把业务逻辑抽离到 Controller,UI 只负责展示。适合复杂业务或跨页面共享状态。
import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';
/// 状态模型
class LoginState {
final String username;
final String password;
final bool isLoading;
final String? error;
const LoginState({
this.username = '',
this.password = '',
this.isLoading = false,
this.error,
});
LoginState copyWith({
String? username,
String? password,
bool? isLoading,
String? error,
}) {
return LoginState(
username: username ?? this.username,
password: password ?? this.password,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
);
}
}
/// 控制器
class LoginController {
// 这里创建的 signal 和 computed 在没有被强引用的情况下,会随着 LoginController 的回收
// 而被 GC 回收(被 GC 回收的时候不会调用 onDispose() 方法)
// 所以也不需要主动调用 dispose() 方法回收,但是如果是 effect 的话则要主动回收
final state = signal(const LoginState());
late final canLogin = computed(
() => state.value.username.isNotEmpty && state.value.password.isNotEmpty,
);
EffectCleanup? _effectCleanup;
LoginController() {
_effectCleanup = effect(() {
debugPrint('✨ effect: canLogin changed to ${canLogin.value}');
});
}
void updateUsername(String v) =>
state.value = state.value.copyWith(username: v);
void updatePassword(String v) =>
state.value = state.value.copyWith(password: v);
Future<void> login() async {
state.value = state.value.copyWith(isLoading: true, error: null);
await Future.delayed(const Duration(seconds: 1));
if (state.value.username == "admin" && state.value.password == "123456") {
debugPrint("✅ 登录成功");
} else {
state.value = state.value.copyWith(error: "用户名或密码错误");
}
state.value = state.value.copyWith(isLoading: false);
}
void dispose() {
// effect 必须主动调用回收,否则会一直持有监听
_effectCleanup?.call();
// signal 和 computed 本质是普通对象,如果没有强引用,会随 GC 自动回收。
// GC 回收时不会触发 onDispose(),只有显式调用 dispose() 才会触发。
// 因此:
// - 纯 signal/computed:不需要手动 dispose。
// - 有 effect 或全局持有:必须手动清理。
// - 为了架构安全,通常仍建议在 Controller 提供 dispose() 方法。
state.dispose();
canLogin.dispose();
}
}
/// 视图
class LoginPageMVC extends StatefulWidget {
const LoginPageMVC({super.key});
@override
State<LoginPageMVC> createState() => _LoginPageMVCState();
}
class _LoginPageMVCState extends State<LoginPageMVC> {
late final LoginController controller;
@override
void initState() {
super.initState();
controller = LoginController();
}
@override
void dispose() {
controller.dispose(); // 可选,清理信号
super.dispose();
}
@override
Widget build(BuildContext context) {
final c = controller;
return Scaffold(
appBar: AppBar(title: const Text("登录 - MVC版")),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(onChanged: c.updateUsername, decoration: const InputDecoration(labelText: "用户名")),
TextField(onChanged: c.updatePassword, decoration: const InputDecoration(labelText: "密码")),
const SizedBox(height: 20),
// ========== 方式1: Watch ==========
const Text("方式1: Watch", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue)),
Watch((context) {
return ElevatedButton(
onPressed: c.canLogin.value ? c.login : null,
child: c.state.value.isLoading
? const CircularProgressIndicator()
: const Text("登录"),
);
}),
const Divider(),
// ========== 方式2: Watch.builder ==========
const Text("方式2: Watch.builder", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.green)),
Watch.builder(builder: (context) {
return Text("canLogin: ${c.canLogin.value}");
}),
const Divider(),
// ========== 方式3: WatchBuilder ==========
const Text("方式3: WatchBuilder", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.orange)),
WatchBuilder(
builder: (context, child) {
return Column(
children: [
Text("username: ${c.state.value.username}"),
Text("password: ${c.state.value.password}"),
],
);
},
),
const Divider(),
// ========== 方式4: .watch(context) ==========
// 只会触发直接使用他的这个 Text 组件的重建
const Text("方式4: .watch(context)", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.purple)),
Text("username: ${c.state.select((s) => s.username).watch(context)}"),
Text("canLogin: ${c.canLogin.watch(context)}"),
const Divider(),
// ========== 方式5: .value(需配合 Watch) ==========
const Text("方式5: .value(需配合 Watch)", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red)),
Watch(() {
return Text("直接读取 .value: ${c.state.value.username}");
}),
// ❌ 错误示范:直接在 build 中用 .value 不会更新
Text("❌ 错误: 直接用 .value(不会更新): ${c.state.value.username}"),
],
),
),
);
}
}
如上代码所述,signal 和 computed 本质上是普通 Dart 对象,没有外部强引用时,会被 Dart GC 自动回收。 → 所以如果 LoginController 本身被释放,且没有其他地方持有 state 或 canLogin 的引用,它们也会被 GC 清理。
GC 回收不会触发 onDispose(): onDispose 是 Signals 库提供的手动清理钩子,不是 Dart VM 的生命周期事件。
effect 必须手动清理: 官方文档明确提到,effect 会注册一个订阅/副作用,它会保持对依赖信号的引用。 如果不调用返回的 EffectCleanup,引用链就存在,对象不会被 GC 回收。
这里注意 signal 在 Widget 的几种使用方法:
方式 | 语法 | 特点 | 适用场景 |
---|---|---|---|
1. Watch() | Watch(() { ... }) | 最简洁,自动追踪依赖,内部访问的信号变化时会重建 | 包裹一小段 UI,响应式更新 |
2. Watch.builder | Watch.builder(builder: (context) { ... }) | 提供 context 参数,方便使用 Theme.of(context) 、MediaQuery 等 | 需要上下文的响应式 UI |
3. WatchBuilder | WatchBuilder(builder: (context, child) { ... }) | 提供 context 和 child ,可传递静态子组件避免重建 | 性能优化场景,大组件中局部响应式 |
4. .watch(context) | signal.watch(context) | 在 build 中直接调用,自动注册依赖并触发当前 widget 重建(直接使用他的这个 widget) | 简单的文本展示、内联使用 |
5. .value(配合 Watch) | signal.value | 在 Watch 内使用会追踪依赖;在普通 build 中直接用只是快照,不会更新 | 需要在 Watch 内部读取多个信号时 |
6. SignalsMixin | signal.value (直接在 build 中) | 混入 SignalsMixin 后,build 中访问的 .value 会自动追踪依赖 | 想让整个 build 方法天然响应式,无需显式 Watch |
特点:
- 状态与逻辑集中在
LoginController
,UI 更干净。 - 适合复杂业务、跨页面共享、可测试性更强。
- (可选)需要手动
dispose()
清理信号。
两种方式对比
维度 | Widget 内部 createXXX | View + Controller + State |
---|---|---|
生命周期 | 自动随 widget 销毁 | 需要手动 dispose |
代码复杂度 | 简单,几行搞定 | 更复杂,分层清晰 |
适用场景 | 局部状态、轻量交互 | 跨页面、复杂业务、团队协作 |
可测试性 | 较弱 | 强,可单测 Controller |
总结:
- 局部状态 → 用
createSignal
/createComputed
直接在 widget 内定义,简洁高效。 - 复杂业务/跨页面状态 → 用
Controller + State
分层,手动管理生命周期,保证可维护性。