Skip to content

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:

  1. Subscribe to changes when the widget builds
  2. Unsubscribe when the widget is disposed (prevent memory leaks)
  3. 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:

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

dart
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 setState with 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. Even TextEditingController and AnimationController can be created with createOnce() in WatchingWidget - no StatefulWidget needed! Only use StatefulWidget for truly local UI state that requires setState.

Alternative: Using Mixins

If you have existing widgets you don't want to change, use mixins instead:

For Existing StatelessWidget

dart
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

dart
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 const constructors with WatchItMixin
  • 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

dart
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

Released under the MIT License.