Skip to content

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

dart
// 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() accesses CounterManager from get_it
  • Watches the count property
  • 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:

dart
// ✅ Recommended - Dart infers types automatically
final count = watchValue((CounterManager m) => m.count);

Method signature:

dart
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 (from m.count which is ValueListenable<int>)

Without the type annotation, you'd need to specify both generics manually:

dart
// ❌️ 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:

dart
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:

dart
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

dart
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

dart
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

  1. Create a ValueNotifier in your manager:

    dart
    class MyManager {
      final message = ValueNotifier<String>('Hello');
    }
  2. Register it:

    dart
    void setupMyManager() {
      di.registerSingleton<MyManager>(MyManager());
    }
  3. Watch it:

    dart
    class MyWidget extends WatchingWidget {
      @override
      Widget build(BuildContext context) {
        final message = watchValue((MyManager m) => m.message);
        return Text(message);
      }
    }
  4. Change it and watch the magic:

    dart
    void 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

Released under the MIT License.