Lesson 69 of 77 advanced

Riverpod 2.x & Provider — The Other State Management

Master Riverpod's compile-safe, testable state management — and know when to choose it over BLoC

Open interactive version (quiz + challenge)

Real-world analogy

If BLoC is like a factory with conveyor belts (streams in, states out, strict process), Riverpod is like a smart warehouse where every shelf has a barcode. You can grab any item by scanning its code (ref.watch), items auto-restock when dependencies change, and shelves you stop visiting get cleaned up automatically (autoDispose). Provider (the package) was the early prototype warehouse — functional but without barcodes. Riverpod is the v2 warehouse with full inventory tracking.

What is it?

Riverpod is a reactive state management and dependency injection framework for Flutter that provides compile-safe, testable, and composable providers. Unlike the original Provider package, Riverpod providers are declared globally, don't depend on BuildContext, and support autoDispose, family parameterization, and code generation.

Real-world relevance

In a production Flutter app like a social media client, Riverpod manages: user auth state (AsyncNotifierProvider), feed posts with pagination (family + autoDispose), theme preferences (StateProvider), API client configuration (Provider), and WebSocket connections (StreamProvider). Each provider declares its dependencies explicitly, and testing requires only overrides in ProviderScope.

Key points

Code example

// Riverpod 2.x — NotifierProvider with AsyncNotifier example

// 1. Simple StateProvider
final counterProvider = StateProvider<int>((ref) => 0);

// 2. NotifierProvider (Riverpod 2.x style)
class TodoListNotifier extends Notifier<List<Todo>> {
  @override
  List<Todo> build() => []; // initial state

  void addTodo(Todo todo) {
    state = [...state, todo]; // immutable update
  }

  void toggleTodo(int id) {
    state = [
      for (final todo in state)
        if (todo.id == id) todo.copyWith(done: !todo.done)
        else todo,
    ];
  }

  void removeTodo(int id) {
    state = state.where((t) => t.id != id).toList();
  }
}

final todoListProvider =
    NotifierProvider<TodoListNotifier, List<Todo>>(TodoListNotifier.new);

// 3. AsyncNotifierProvider — fetching from API
class UserProfileNotifier extends AsyncNotifier<UserProfile> {
  @override
  Future<UserProfile> build() async {
    final repo = ref.watch(userRepoProvider);
    return repo.fetchCurrentUser();
  }

  Future<void> updateName(String name) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      final repo = ref.read(userRepoProvider);
      return repo.updateName(name);
    });
  }
}

final userProfileProvider =
    AsyncNotifierProvider<UserProfileNotifier, UserProfile>(
  UserProfileNotifier.new,
);

// 4. Family + AutoDispose — parameterized provider
final userByIdProvider =
    FutureProvider.autoDispose.family<User, int>((ref, userId) async {
  final repo = ref.watch(userRepoProvider);
  return repo.getUser(userId);
});

// 5. Widget usage
class UserScreen extends ConsumerWidget {
  const UserScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ref.watch — reactive, rebuilds on change
    final profile = ref.watch(userProfileProvider);
    final todos = ref.watch(todoListProvider);

    // select — granular rebuild only when name changes
    final userName = ref.watch(
      userProfileProvider.select((p) => p.valueOrNull?.name ?? ''),
    );

    return profile.when(
      data: (user) => Column(
        children: [
          Text('Hello, ${user.name}'),
          ElevatedButton(
            // ref.read — one-time read in callback
            onPressed: () => ref.read(counterProvider.notifier).state++,
            child: const Text('Increment'),
          ),
          // ref.listen — side effect without rebuild
        ],
      ),
      loading: () => const CircularProgressIndicator(),
      error: (e, st) => Text('Error: $e'),
    );
  }
}

// 6. Testing with overrides
void main() {
  testWidgets('shows user name', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          userRepoProvider.overrideWithValue(MockUserRepo()),
          userProfileProvider.overrideWith(() => MockUserProfileNotifier()),
        ],
        child: const MaterialApp(home: UserScreen()),
      ),
    );
    await tester.pumpAndSettle();
    expect(find.text('Hello, Test User'), findsOneWidget);
  });
}

Line-by-line walkthrough

  1. 1. counterProvider: simplest Riverpod provider — StateProvider holds a single int, ref.read(counterProvider.notifier).state++ to mutate
  2. 2. TodoListNotifier extends Notifier> — the Riverpod 2.x pattern replaces StateNotifier
  3. 3. build() returns initial state — called on creation and on invalidation (ref.invalidate)
  4. 4. addTodo creates a new list with spread operator — immutable state updates, never mutate in place
  5. 5. toggleTodo uses collection-for with conditional — idiomatic Dart for immutable list transformations
  6. 6. NotifierProvider declaration connects the Notifier class to its provider — TodoListNotifier.new is a tear-off constructor
  7. 7. UserProfileNotifier extends AsyncNotifier — build() returns Future, state is automatically AsyncValue
  8. 8. AsyncValue.guard wraps try/catch — state becomes AsyncError on failure, AsyncData on success
  9. 9. userByIdProvider: FutureProvider.autoDispose.family — parameterized by userId, auto-cleaned when widget disposes
  10. 10. ConsumerWidget gives you WidgetRef in build() — ref.watch for reactive reads, ref.read for callbacks
  11. 11. profile.when() destructures AsyncValue into data/loading/error — no manual loading boolean needed
  12. 12. Testing: ProviderScope overrides inject mocks — no DI container, no setup, just declare overrides in the array

Spot the bug

class CartNotifier extends Notifier<List<CartItem>> {
  @override
  List<CartItem> build() => [];

  void addItem(CartItem item) {
    state.add(item); // Add to cart
  }

  void removeItem(int id) {
    state.removeWhere((item) => item.id == id);
  }

  double get total =>
    state.fold(0, (sum, item) => sum + item.price * item.qty);
}
Need a hint?
The add and remove methods modify state but widgets watching this provider never rebuild. The state reference itself never changes.
Show answer
Bug: state.add(item) and state.removeWhere() mutate the existing list in place — but Riverpod only notifies listeners when the state REFERENCE changes (==). Since the same List object is being mutated, state == state is still true, so no rebuild is triggered. Fix: addItem should be `state = [...state, item];` and removeItem should be `state = state.where((item) => item.id != id).toList();`. Always create a NEW list/object when updating Riverpod state — immutable update pattern. This is the #1 Riverpod bug in production code and a favorite interview question.

Explain like I'm 5

Imagine you have a magic notebook where you write down things you need — like 'I need the weather' or 'I need the user's name.' Riverpod is like a helper who watches that notebook. Whenever something you wrote changes, the helper taps you on the shoulder and says 'hey, this updated!' If you stop caring about the weather page, the helper stops watching it and cleans it up. ref.watch is asking the helper to keep you updated. ref.read is peeking at the notebook once without asking for updates.

Fun fact

Remi Rousselet created both Provider and Riverpod. The name 'Riverpod' is an anagram of 'Provider' — he literally rearranged the letters to signal that it's a reimagining of the same concept. The anagram was intentional from day one.

Hands-on challenge

Build a mini task manager using Riverpod 2.x with the following: (1) An AsyncNotifierProvider that fetches tasks from a mock API. (2) A NotifierProvider for a filter (all/completed/pending). (3) A computed Provider that combines the task list with the filter. (4) Use .select() to only rebuild the task count badge when the count changes. (5) Write a widget test using ProviderScope overrides to inject a mock repository.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery