Phase 02

Core Concepts & Ref Lifecycle

Calculating reading time... Module 1: Foundations

🎯 Learning Objective

Master the Ref object hierarchy, automatic cache disposal (autoDispose), state preservation (keepAlive), manual state invalidation (ref.invalidate()), lifecycle hooks (ref.onDispose()), and protective unmount guards (ref.mounted).

📋 Prerequisites

1. The Ref Hierarchy Explained

In Riverpod, Ref is the central hub. It allows widgets to interact with providers, and allows providers to interact with other providers.

Ref Types Breakdown
1
Ref (Base Type)
The base interface for interacting with providers. Used inside functional and class-based providers.
2
WidgetRef
Passed into ConsumerWidget.build(). Allows Flutter UI widgets to watch, read, or listen to providers.
3
ProviderRef
Passed into provider functions/notifiers. Allows providers to read other providers or register lifecycle callbacks.

2. State Lifecycle: autoDispose vs keepAlive()

By default in Riverpod 3 (with code generation), providers automatically dispose of their state when they are no longer watched by any widget. This prevents memory leaks.

Should this provider keep its state in memory when unused?
NO (Transient / Form Screen)
autoDispose (Default)
Provider state is destroyed as soon as the user leaves the screen.
YES (Global Profile / Cache)
keepAlive() / @Riverpod(keepAlive: true)
State remains cached in memory permanently until manually invalidated.
💡 How to keep state alive dynamically

Inside a provider's build() method, calling ref.keepAlive() prevents auto-disposal, keeping state cached until explicit invalidation.

3. Manual State Invalidation & Refresh

When data becomes stale (e.g. after a pull-to-refresh action or database mutation), you can force a provider to re-execute its build() method.

ref.invalidate(provider) — Lazy Invalidation

WHAT: Marks the provider as stale immediately. Destruction and re-fetching occur lazily when the provider is next watched.

BEST FOR: Triggering background cache invalidations after network POST/PUT mutations.

ref.refresh(provider) — Eager Re-Fetch

WHAT: Invalidates the provider and immediately forces a re-read, returning the new value.

BEST FOR: Pull-to-refresh UI gestures where the user expects immediate loading spinners.

ref.invalidateSelf() — Self Invalidation

WHAT: Called inside a provider notifier to force itself to re-execute its own build() method.

BEST FOR: Internal error recovery and retry logic inside Notifiers.

4. Provider Lifecycle Hooks & ref.mounted Safety

Providers can register cleanup callbacks that run when the provider is disposed or cancelled:

Dart Lifecycle Callbacks Pattern
final timerProvider = NotifierProvider<TimerNotifier, int>(TimerNotifier.new);

class TimerNotifier extends Notifier<int> {
  @override
  int build() {
    final timer = Timer.periodic(const Duration(seconds: 1), (_) => state++);
    
    // Register cleanup callback on disposal
    ref.onDispose(() {
      timer.cancel(); // Prevents timer memory leaks
    });

    return 0;
  }
}
🚨 Protective Unmount Guard (ref.mounted)

When a provider performs an asynchronous operation (e.g. network call), the provider might be disposed before the request completes. Modifying state on an unmounted notifier will throw a fatal error.
Always protect async assignments: if (!ref.mounted) return;

5. Real Production Examples

Example 1: Auto-Disposing Search Input

Demonstrates auto-disposal when closing a search screen:

Dart lib/features/search/search_provider.dart
final searchQueryProvider = NotifierProvider.autoDispose<SearchQueryNotifier, String>(
  SearchQueryNotifier.new,
);

class SearchQueryNotifier extends AutoDisposeNotifier<String> {
  @override
  String build() => '';

  void updateQuery(String query) => state = query;
}

Example 2: Debounced Live Search with Network Cancellation

Production debounced API search notifier that cancels pending network calls on disposal using ref.onDispose():

Dart lib/features/search/live_search_notifier.dart
class LiveSearchNotifier extends AutoDisposeNotifier<AsyncValue<List<String>>> {
  @override
  AsyncValue<List<String>> build() {
    return const AsyncValue.data([]);
  }

  Future<void> search(String query) async {
    final client = http.Client();
    ref.onDispose(() {
      client.close(); // Cancel active HTTP request if user leaves screen
    });

    state = const AsyncValue.loading();
    try {
      final res = await client.get(Uri.parse('https://api.example.com/search?q=$query'));
      if (!ref.mounted) return; // Guard unmounted state
      state = AsyncValue.data(parseResults(res.body));
    } catch (err, stack) {
      if (!ref.mounted) return;
      state = AsyncValue.error(err, stack);
    }
  }
}

6. Knowledge Check & Mini Challenge

❓ Question: What happens if you modify state after ref.mounted becomes false?
A) State updates silently without errors
B) The provider auto-recreates itself
C) Riverpod throws a fatal runtime state error

🏋️ Mini Challenge: Timer Cleanup

Write a StopwatchNotifier using ref.onDispose() to ensure a periodic Timer is cancelled whenever the provider is disposed.

Phase 02 Summary

You have mastered the Ref hierarchy, automatic disposal vs keepAlive, manual invalidation with ref.invalidate(), and safety guards using ref.mounted and ref.onDispose().