Skip to content

最佳实践

控制响应范围

在 widget build 内创建信号,会自动绑定到组件生命周期:

dart
final count = signal(context, 0);

如果在组件外创建,请传 null,并手动释放 effect。

用 Computed 表达派生状态

派生状态尽量放在 computed 中:

dart
final total = computed(context, (_) => items.length);

不要在 computed 内部写入值。

用 SignalBuilder 限制重建

只包裹需要更新的局部:

dart
SignalBuilder(
  builder: (context) => Text('Count: ${count()}'),
);

适合使用 SignalBuilder 的场景:

  • 只有局部子树依赖信号/computed
  • 不想重建整棵 widget 树
  • 列表项/卡片等细粒度更新

用 Batch 合并更新

减少重复重算:

dart
batch(() {
  a.set(a() + 1);
  b.set(b() + 1);
});

适合使用 batch 的场景:

  • 一次用户操作更新多个信号
  • 集合有大量 mutation(如 sort、clear + addAll)
  • 希望 effect/computed 只重新计算一次

集合用 ReactiveList/Map/Set

需要就地修改时使用响应式集合:

dart
final todos = ReactiveList.scoped(context, ['A', 'B']);
todos.add('C');

控制依赖跟踪

在 effect 内使用 untrack 避免多余依赖:

dart
effect(context, () {
  final snapshot = untrack(() => expensive());
  debugPrint('snapshot: $snapshot');
});

Flutter 示例(来自 example 应用)

dart
class CounterSection extends StatelessWidget {
  const CounterSection({super.key});

  @override
  Widget build(BuildContext context) {
    final count = signal<double>(context, 2);
    final doubled = computed<double>(context, (_) => count() * 2);
    final squared = writableComputed<double>(
      context,
      get: (_) => count() * count(),
      set: (value) {
        final safe = value < 0 ? 0.0 : value;
        count.set(math.sqrt(safe));
      },
    );

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('count: ${count().toStringAsFixed(1)}'),
        Text('doubled (computed): ${doubled().toStringAsFixed(1)}'),
        Text('squared (writable): ${squared().toStringAsFixed(1)}'),
        const SizedBox(height: 8),
        Wrap(
          spacing: 8,
          children: [
            ElevatedButton(
              onPressed: () => count.set(count() + 1),
              child: const Text('Increment'),
            ),
            OutlinedButton(
              onPressed: () => squared.set(81),
              child: const Text('Set squared = 81'),
            ),
          ],
        ),
      ],
    );
  }
}
dart
class EffectBatchSection extends StatelessWidget {
  const EffectBatchSection({super.key});

  @override
  Widget build(BuildContext context) {
    final a = signal<int>(context, 1);
    final b = signal<int>(context, 2);
    final sum = computed<int>(context, (_) => a() + b());
    final effectRuns = signal<int>(context, 0);

    effect(context, () {
      sum();
      final current = untrack(() => effectRuns());
      effectRuns.set(current + 1);
    });

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('a: ${a()}  b: ${b()}  sum (computed): ${sum()}'),
        Text('effect runs: ${effectRuns()}'),
        const SizedBox(height: 8),
        Wrap(
          spacing: 8,
          children: [
            ElevatedButton(
              onPressed: () => a.set(a() + 1),
              child: const Text('Increment A'),
            ),
            ElevatedButton(
              onPressed: () => b.set(b() + 1),
              child: const Text('Increment B'),
            ),
            OutlinedButton(
              onPressed: () {
                batch(() {
                  a.set(a() + 1);
                  b.set(b() + 1);
                });
              },
              child: const Text('Batch +1 both'),
            ),
          ],
        ),
      ],
    );
  }
}
dart
class UntrackSection extends StatelessWidget {
  const UntrackSection({super.key});

  @override
  Widget build(BuildContext context) {
    final source = signal<int>(context, 1);
    final noise = signal<int>(context, 100);
    final tracked = computed<int>(context, (_) => source() + noise());
    final untracked = computed<int>(
      context,
      (_) => source() + untrack(() => noise()),
    );

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        SignalBuilder(
          builder: (context) => Text('source: ${source()}  noise: ${noise()}'),
        ),
        SignalBuilder(builder: (context) => Text('tracked: ${tracked()}')),
        SignalBuilder(builder: (context) => Text('untracked: ${untracked()}')),
        const SizedBox(height: 8),
        Wrap(
          spacing: 8,
          children: [
            ElevatedButton(
              onPressed: () => source.set(source() + 1),
              child: const Text('Bump source'),
            ),
            OutlinedButton(
              onPressed: () => noise.set(noise() + 10),
              child: const Text('Bump noise'),
            ),
          ],
        ),
        const SizedBox(height: 4),
        Text(
          'Note: untracked ignores noise changes.',
          style: Theme.of(context).textTheme.bodySmall,
        ),
      ],
    );
  }
}