WatchingWidgets
Why Do You Need Special Widgets?
You might wonder: "Why can't I just use watchValue() in a regular StatelessWidget?"
The problem: watch_it needs to hook into your widget's lifecycle to:
- Subscribe to changes when the widget builds
- Unsubscribe when the widget is disposed (prevent memory leaks)
- Rebuild the widget when data changes
Regular StatelessWidget doesn't give watch_it access to these lifecycle events. You need a widget that watch_it can hook into.
WatchingWidget - For Widgets Without Local State
Replace StatelessWidget with WatchingWidget:
class TodoList extends WatchingWidget {
@override
Widget build(BuildContext context) {
final todos = watchValue((TodoManager m) => m.todos);
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) => Text(todos[index].title),
);
}
}Use this when:
- Writing new widgets
- You don't need local state (
setState) - Simple reactive UI
WatchingStatefulWidget - For Widgets With Local State
Use when you need both setState AND reactive state:
class TodoListWithFilter extends WatchingStatefulWidget {
@override
State createState() => _TodoListWithFilterState();
}
class _TodoListWithFilterState extends State<TodoListWithFilter> {
bool _showCompleted = true; // Local UI state
@override
Widget build(BuildContext context) {
// Reactive state - rebuilds when todos change
final todos = watchValue((TodoManager m) => m.todos);
// Filter based on local state
final filtered = _showCompleted
? todos
: todos.where((todo) => !todo.completed).toList();
return Column(
children: [
SwitchListTile(
title: Text('Show completed'),
value: _showCompleted,
onChanged: (value) => setState(() => _showCompleted = value),
),
Expanded(
child: ListView.builder(
itemCount: filtered.length,
itemBuilder: (context, index) => CheckboxListTile(
title: Text(filtered[index].title),
value: filtered[index].completed,
onChanged: (_) => di<TodoManager>().updateTodoCommand.run(
filtered[index]
.copyWith(completed: !filtered[index].completed)),
),
),
),
],
);
}
}Use this when:
- You need local UI state (filter toggles, expansion state)
- Mix
setStatewith reactive updates
Note: Your State class automatically gets all watch functions - no mixin needed!
Pattern: Local state (_showCompleted) for UI-only preferences, reactive state (todos) from manager, and checkboxes call back into the manager to update data.
💡 Important: With
watch_it, you'll rarely need StatefulWidget anymore. Most state belongs in your managers and is accessed reactively. EvenTextEditingControllerandAnimationControllercan be created withcreateOnce()inWatchingWidget- no StatefulWidget needed! Only use StatefulWidget for truly local UI state that requiressetState.
Alternative: Using Mixins
If you have existing widgets you don't want to change, use mixins instead:
For Existing StatelessWidget
class TodoListWithMixin extends StatelessWidget with WatchItMixin {
const TodoListWithMixin({super.key}); // Can use const!
@override
Widget build(BuildContext context) {
final todos = watchValue((TodoManager m) => m.todos);
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) => Text(todos[index].title),
);
}
}For Existing StatefulWidget
class TodoListWithFilterMixin extends StatefulWidget
with WatchItStatefulWidgetMixin {
const TodoListWithFilterMixin({super.key});
@override
State createState() => _TodoListWithFilterMixinState();
}
class _TodoListWithFilterMixinState extends State<TodoListWithFilterMixin> {
bool _showCompleted = true;
@override
Widget build(BuildContext context) {
final todos = watchValue((TodoManager m) => m.todos);
final filtered = _showCompleted
? todos
: todos.where((todo) => !todo.completed).toList();
return Column(
children: [
SwitchListTile(
title: Text('Show completed'),
value: _showCompleted,
onChanged: (value) => setState(() => _showCompleted = value),
),
Expanded(
child: ListView.builder(
itemCount: filtered.length,
itemBuilder: (context, index) => CheckboxListTile(
title: Text(filtered[index].title),
value: filtered[index].completed,
onChanged: (_) => di<TodoManager>().updateTodoCommand.run(
filtered[index]
.copyWith(completed: !filtered[index].completed)),
),
),
),
],
);
}
}Why use mixins?
- Keep existing class hierarchy
- Can use
constconstructors withWatchItMixin - Minimal changes to existing code
- Perfect for gradual migration
Quick Decision Guide
New widget, no local state? → Use WatchingWidget
New widget WITH local state? → Use WatchingStatefulWidget
Migrating existing StatelessWidget? → Add with WatchItMixin
Migrating existing StatefulWidget? → Add with WatchItStatefulWidgetMixin to the StatefulWidget (not the State!)
Common Patterns
Combining with Other Mixins
class AnimatedCard extends WatchingStatefulWidget {
@override
State createState() => _AnimatedCardState();
}
class _AnimatedCardState extends State<AnimatedCard>
with SingleTickerProviderStateMixin {
// Mix with other mixins!
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 1));
}
@override
Widget build(BuildContext context) {
final data = watchValue((DataManager m) => m.data);
return AnimatedBuilder(
animation: _controller,
builder: (context, child) => Transform.scale(
scale: _controller.value,
child: Text(data),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}See Also
- Getting Started - Basic
watch_itusage - Your First Watch Functions - Learn
watchValue() - Watch Ordering Rules - CRITICAL rules