Watch & Computed
Read state, get subscribed. Watch rebuilds from whatever state you read inside it, and computed derives new state that recomputes when its dependencies change — no manual dependency lists.
Watch((context) => Text('${vm.count.value}'))Watch
Watch tracks every state.value read inside its builder and rebuilds when any of them changes. It subscribes to exactly what you touched — nothing more.
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final vm = context.vm<CounterViewModel>();
return Watch((context) => Text('${vm.count.value}'));
}
}Multiple values
Reading several values in one Watch replaces StateBuilder2/StateBuilder3 and nested builders:
Watch((context) => vm.loading.value
? const CircularProgressIndicator()
: Text(vm.name.value))Conditional dependencies
Dependencies are re-tracked on every build, so branches only subscribe to what they actually read:
Watch((context) {
if (vm.useMetric.value) return Text('${vm.celsius.value}°C');
return Text('${vm.fahrenheit.value}°F'); // celsius is not a dependency here
})Watch vs. build()
.build() stays the shorthand for a single leaf value — it's shorter and states its dependency explicitly:
vm.count.build((count) => Text('$count')) // one valueReach for Watch when a widget reads two or more values or has conditional dependencies. Prefer one Watch around a meaningful subtree over many tiny nested ones.
computed()
computed derives a value from other state. It recomputes when a dependency changes and notifies only when the result actually differs.
late final fullName = computed(() => '${vm.first.value} ${vm.last.value}');Inside a ViewModel, use the computed() helper so the derived state is disposed automatically:
class TodosViewModel extends ViewModel {
late final todos = bindAsync<List<Todo>>(repo.todosStream);
late final pending = computed(
() => todos.value.dataOrNull?.where((t) => !t.completed).toList() ?? const [],
equals: (a, b) => listEquals(a, b),
);
}A Watch reading vm.pending.value rebuilds whenever the derived list changes.
Equality
By default a computed compares results with ==. When the derivation allocates a new instance every run (a filtered .toList(), a mapped record), pass equals so an unchanged result doesn't notify:
final evens = computed(
() => numbers.value.where((n) => n.isEven).toList(),
equals: (a, b) => listEquals(a, b),
);Composition
Computeds can read other computeds. Cycles are detected and throw a StateError:
final doubled = computed(() => count.value * 2);
final label = computed(() => 'value: ${doubled.value}');Rules
- Reads are tracked, writes are not. Writing state inside a
Watchbuilder or acomputedfunction throws — move the write to an event handler. - One
Watchper subtree. Wrap the meaningful build body, not everyText. computedfor derived state. A plain getter is not reactive and is recomputed on every access;computedis both.
API Summary
| API | Returns | Use |
|---|---|---|
Watch((context) => …) | Widget | Rebuild from state read in the builder |
computed(() => …, {equals}) | Computed<T> | Derived, auto-recomputing state |
state.build((v) => …) | Widget | Shorthand for a single leaf value |
