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分层,手动管理生命周期,保证可维护性。 
其余关于 Signals.dart 的一些使用、分析等等可以查看这个 gist: dart_signals_example.dart
