Your First Watch Functions
Watch functions are the core of watch_it - they make your widgets automatically rebuild when data changes. Let's start with the most common one.
The Simplest Watch: watchValue
The most common way to watch data is with watchValue(). It watches a ValueListenable property from an object registered in get_it.
Basic Counter Example
// 1. Create a manager with reactive state
class CounterManager {
final count = ValueNotifier<int>(0);
void increment() => count.value++;
}
// 2. Register it in get_it
void setupCounter() {
di.registerSingleton<CounterManager>(CounterManager());
}
// 3. Watch it in your widget
class CounterWidget extends WatchingWidget {
const CounterWidget({super.key});
@override
Widget build(BuildContext context) {
// This one line makes it reactive!
final count = watchValue((CounterManager m) => m.count);
return Scaffold(
body: Center(
child: Text('Count: $count', style: TextStyle(fontSize: 48)),
),
floatingActionButton: FloatingActionButton(
onPressed: () => di<CounterManager>().increment(),
child: Icon(Icons.add),
),
);
}
}What happens:
watchValue()accessesCounterManagerfrom get_it- Watches the
countproperty - Widget rebuilds automatically when count changes
- No manual listeners, no cleanup needed
Type Inference Magic
Notice how we specify the type of the parent object in the selector function:
(CounterManager m) => m.count
By declaring the parent object type CounterManager, Dart automatically infers both generic type parameters:
// ✅ Recommended - Dart infers types automatically
final count = watchValue((CounterManager m) => m.count);Method signature:
R watchValue<T extends Object, R>(
ValueListenable<R> Function(T) selectProperty, {
bool allowObservableChange = false,
String? instanceName,
GetIt? getIt,
})Dart infers:
T = CounterManager(from the parent object type)R = int(fromm.countwhich isValueListenable<int>)
Without the type annotation, you'd need to specify both generics manually:
// ❌️ More verbose - manual type parameters required
final count = watchValue<CounterManager, int>((m) => m.count);Bottom line: Always specify the parent object type in your selector function for cleaner, more readable code!
Watching Multiple Objects
Need to watch data from different managers? Just add more watch calls:
class DashboardWidget extends WatchingWidget {
const DashboardWidget({super.key});
@override
Widget build(BuildContext context) {
// Watch different objects
final count = watchValue((CounterManager m) => m.count);
final userName = watchValue((SimpleUserManager m) => m.name);
final isLoading = watchValue((DataManager m) => m.isLoading);
return Column(
children: [
Text('Welcome, $userName!'),
Text('Counter: $count'),
if (isLoading) CircularProgressIndicator(),
],
);
}
}When ANY of them change, the widget rebuilds. That's it!
Compare with ValueListenableBuilder:
class DashboardWidgetWithBuilders extends StatelessWidget {
const DashboardWidgetWithBuilders({super.key});
@override
Widget build(BuildContext context) {
final counter = di<CounterManager>();
final userManager = di<SimpleUserManager>();
final dataManager = di<DataManager>();
return ValueListenableBuilder<int>(
valueListenable: counter.count,
builder: (context, count, _) {
return ValueListenableBuilder<String>(
valueListenable: userManager.name,
builder: (context, userName, _) {
return ValueListenableBuilder<bool>(
valueListenable: dataManager.isLoading,
builder: (context, isLoading, _) {
return Column(
children: [
Text('Welcome, $userName!'),
Text('Counter: $count'),
if (isLoading) CircularProgressIndicator(),
],
);
},
);
},
);
},
);
}
}Three levels of nesting! With watch_it, it's just three simple lines.
Real Example: Todo List
class TodoManager {
final todos = ValueNotifier<List<String>>([]);
void addTodo(String todo) {
todos.value = [...todos.value, todo]; // New list triggers update
}
}
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]),
);
}
}Add a todo? Widget rebuilds automatically. No setState, no StreamBuilder.
Common Pattern: Loading States
class DataWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final isLoading = watchValue((DataManager m) => m.isLoading);
final data = watchValue((DataManager m) => m.data);
// Initialize data on first build
callOnce((_) {
di<DataManager>().fetchData();
});
if (isLoading) {
return CircularProgressIndicator();
}
return Text('Data: $data');
}
}Try It Yourself
Create a
ValueNotifierin your manager:dartclass MyManager { final message = ValueNotifier<String>('Hello'); }Register it:
dartvoid setupMyManager() { di.registerSingleton<MyManager>(MyManager()); }Watch it:
dartclass MyWidget extends WatchingWidget { @override Widget build(BuildContext context) { final message = watchValue((MyManager m) => m.message); return Text(message); } }Change it and watch the magic:
dartvoid changeMessage() { di<MyManager>().message.value = 'World!'; // Widget rebuilds! }
Key Takeaways
✅ watchValue() is your go-to function ✅ One line replaces manual listeners and setState ✅ Works with any ValueListenable<T> ✅ Automatic subscription and cleanup ✅ Multiple watch calls = multiple subscriptions
Next: Learn about more watch functions for different use cases.
See Also
- WatchingWidgets - Which widget type to use (WatchingWidget, mixins, StatefulWidget)
- More Watch Functions - watchIt, watchPropertyValue, and more
- Watching Multiple Values - Advanced patterns for combining values
- Watch Functions Reference - Complete API reference