Sparround

Ref rules: watch, read, listen, invalidate

The official docs describe Ref as the primary way to interact with providers — a role comparable to Flutter's BuildContext.

Four core methods and where they belong:

  • `ref.watch` — declarative listening. The docs say plainly it is the most common way to listen to providers, and should be your go-to choice. Where: a widget's build and a provider's body.
  • `ref.read` — reading the current value without subscribing. Where: only in user interactions such as button callbacks.
  • `ref.listen` — reacting to a change with a side effect: showing a dialog, navigating, logging. It is safe inside build; outside build (in initState) use ref.listenManual.
  • `ref.onDispose` — cleanup when the provider is disposed (subscriptions, timers, controllers).

To reset state: ref.invalidate(provider) discards it and recomputes on the next read; ref.refresh(provider) is, in the docs' words, syntax sugar for invalidate plus read.

A specific warning from the docs: do not use `ref.read` as a means to "optimize" your code by avoiding `ref.watch` — this will make your code more brittle. That is the official answer to the frequently asked "can I use read for performance?": no, because read does not see the change and the UI silently keeps a stale value.

dart
class TodoPage extends ConsumerWidget {
  const TodoPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // watch: deklarativ oxunuş, dəyişiklikdə rebuild.
    final todos = ref.watch(todosProvider);

    // listen: yan effekt — build içində təhlükəsizdir.
    ref.listen(todosProvider, (previous, next) {
      if (next.hasError) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Yükləmə alınmadı')),
        );
      }
    });

    return Scaffold(
      body: switch (todos) {
        AsyncValue(:final value?) => TodoList(items: value),
        AsyncValue(:final error?) => ErrorView(message: '$error'),
        _ => const Center(child: CircularProgressIndicator()),
      },
      floatingActionButton: FloatingActionButton(
        // read: istifadəçi hərəkəti, abunəlik lazım deyil.
        onPressed: () => ref.read(todosProvider.notifier).add('Yeni tapşırıq'),
        child: const Icon(Icons.add),
      ),
    );
  }
}

// Provider daxilində: watch → asılılıq, onDispose → təmizləmə.
final chatProvider = StreamProvider<List<Message>>((ref) {
  final socket = ref.watch(socketProvider);
  final controller = socket.subscribe('chat');
  ref.onDispose(controller.close);
  return controller.stream;
});

Each method in its place.

MethodSubscriptionWhere it is allowedTypical use
`ref.watch`Yes — rebuild or recompute on change`build`, a provider's bodyDisplaying a value, declaring a dependency
`ref.read`NoCallbacks (`onPressed`, `onTap`)Calling a method on a notifier
`ref.listen`Yes, but it does not rebuild`build` (outside: `listenManual`)A snackbar, dialog, navigation, logging
`ref.invalidate` / `ref.refresh`Callbacks, notifier methodsReloading, pull-to-refresh

The official DO/DON'T rules (worth quoting directly in an interview):

  • Avoid initialising a provider from a widget — the provider should initialize itself; otherwise you risk race conditions.
  • Avoid using providers for ephemeral state (form state, the currently selected item, controllers).
  • Don't perform side effects during provider initialisation (such as submitting a form) — a provider represents a read operation.
  • Use ref.watch/read/listen with statically known providers — passing a provider as a parameter defeats static analysis and lints.
  • Create providers only as top-level final variables.

📚 Sources and documentation