最佳实践
控制响应范围
在 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,
),
],
);
}
}