← Back to blog
Mastering Riverpod: Provider Types · Part 8 of 8
August 15, 202621 min read

100 Questions to Master Riverpod Provider Types

RiverpodFlutterDart

100 Questions to Master Riverpod Provider Types

This is Part 8 — the finale of Mastering Riverpod: Provider Types. The previous seven parts walked the whole provider grid; now you prove you own it.

How to use this bank:

  • 100 questions, grouped by topic, tagged [Basic], [Medium], [Advanced] and marked (Theory) or (Coding).
  • Each has a Hint and a separate Solution. Try cold first, peek at the hint if stuck, then check the solution.
  • For (Coding) questions, write real Riverpod 3.0 code (flutter_riverpod: ^3.0.0).
  • After the 100 there are 10 coding mini-exercises with full solutions, ending in a CRUD capstone.

If you can pick the right provider for any scenario and explain why, you've mastered provider types. Let's go.


Section A — The grid & Provider (Q1–16)

Q1. [Basic] (Theory) What two axes define the provider grid?

Hint

Value kind × mutability. Part 1.

Solution

Value kind (sync / Future / Stream) and mutability (read-only / has methods). 3×2 = 6 providers.

Q2. [Basic] (Theory) Name the six provider types.

Hint

Three pairs.

Solution

Provider, FutureProvider, StreamProvider (read-only); NotifierProvider, AsyncNotifierProvider, StreamNotifierProvider (mutable).

Q3. [Basic] (Theory) What does plain Provider expose?

Hint

Read-only sync.

Solution

A read-only, cached synchronous value (no mutation methods).

Q4. [Basic] (Coding) Declare a Provider for a fixed base URL.

Hint

Return a String.

Solution
final baseUrlProvider = Provider<String>((ref) => 'https://api.example.com');

Q5. [Basic] (Theory) Three jobs of plain Provider?

Hint

Derive/DI/combine.

Solution

Derived values, dependency injection (services/repos), and combining providers.

Q6. [Medium] (Coding) Derive cartTotalProvider from cartItemsProvider.

Hint

watch + fold.

Solution
final cartTotalProvider = Provider<double>((ref) =>
  ref.watch(cartItemsProvider).fold(0.0, (s, i) => s + i.price));

Q7. [Medium] (Coding) Inject a Dio into a UserRepository provider.

Hint

watch dio.

Solution
final dioProvider = Provider((ref) => Dio());
final userRepoProvider = Provider((ref) => UserRepository(ref.watch(dioProvider)));

Q8. [Medium] (Theory) Why derive a value in a Provider rather than in each widget?

Hint

Cache once.

Solution

It's computed once and cached, recomputed only on dependency change; all widgets share the result — less duplication, more testable.

Q9. [Medium] (Theory) Smell test: you want a .toggle() on a Provider. What do you need?

Hint

Mutable.

Solution

A NotifierProviderProvider is read-only; wanting a mutation method means you want a Notifier.

Q10. [Medium] (Theory) Smell test: you wish a Provider could await. What do you need?

Hint

Async.

Solution

FutureProvider (read-only) or AsyncNotifierProvider (mutable) — for async values.

Q11. [Medium] (Coding) Combine userProvider and settingsProvider into a dashboardProvider.

Hint

watch both.

Solution
final dashboardProvider = Provider((ref) =>
  DashboardData(ref.watch(userProvider), ref.watch(settingsProvider)));

Q12. [Medium] (Theory) When does a Provider's create function run?

Hint

Lazy.

Solution

Lazily on first read, then cached (recomputed on dependency change/invalidation).

Q13. [Advanced] (Theory) Why is layered DI (client → repo → UI) good architecture?

Hint

Explicit + testable.

Solution

Dependencies are explicit in the graph, instances are shared/cached, and each layer is overridable for tests — clean boundaries without prop drilling.

Q14. [Advanced] (Coding) Make a Provider that exposes a Logger, used by another Provider.

Hint

Chain.

Solution
final loggerProvider = Provider((ref) => Logger());
final serviceProvider = Provider((ref) => Service(ref.watch(loggerProvider)));

Q15. [Advanced] (Theory) What do all the async providers (Future/Stream rows) expose their value as?

Hint

Sealed type.

Solution

An AsyncValue (loading/data/error) — covered deeply in Series 3.

Q16. [Advanced] (Theory) State the grid's symmetry in one sentence.

Hint

Right = left + methods.

Solution

The mutable column is the read-only column plus methods; each row only changes the value kind (sync/Future/Stream).


Section B — FutureProvider (Q17–30)

Q17. [Basic] (Theory) What does FutureProvider wrap?

Hint

A Future. Part 2.

Solution

An async function returning a Future<T>; it exposes the result as an AsyncValue<T>.

Q18. [Basic] (Coding) Declare a FutureProvider<User> fetching from a repo.

Hint

async.

Solution
final userProvider = FutureProvider<User>((ref) async =>
  ref.watch(userRepoProvider).fetch());

Q19. [Basic] (Theory) What does ref.watch(futureProvider) return?

Hint

Not the raw value.

Solution

An AsyncValue<T>, not the raw T.

Q20. [Basic] (Coding) Render a FutureProvider with .when.

Hint

loading/error/data.

Solution
ref.watch(userProvider).when(
  loading: () => const CircularProgressIndicator(),
  error: (e, _) => Text('$e'),
  data: (u) => Text(u.name));

Q21. [Medium] (Theory) Three states of AsyncValue?

Hint

Loading/data/error.

Solution

AsyncLoading, AsyncData(value), AsyncError(error, stack).

Q22. [Medium] (Coding) Render with a Dart 3 switch instead of .when.

Hint

Sealed.

Solution
switch (ref.watch(userProvider)) {
  AsyncData(:final value) => Text(value.name),
  AsyncError(:final error) => Text('$error'),
  _ => const CircularProgressIndicator(),
}

Q23. [Medium] (Theory) Why is FutureProvider better than FutureBuilder?

Hint

Cache + states.

Solution

It caches the result (one fetch shared, not re-fired on rebuild), handles loading/data/error in one .when, auto-retries, and is testable/overridable.

Q24. [Medium] (Coding) Await one FutureProvider inside another.

Hint

.future.

Solution
final w = FutureProvider((ref) async {
  final city = await ref.watch(cityProvider.future);
  return ref.watch(weatherRepo).fetch(city);
});

Q25. [Medium] (Theory) Difference between ref.watch(p) and ref.watch(p.future) for a FutureProvider?

Hint

AsyncValue vs Future.

Solution

ref.watch(p) gives the current AsyncValue (to inspect state); ref.watch(p.future) gives the underlying Future (to await and chain).

Q26. [Medium] (Coding) Re-fetch a FutureProvider on pull-to-refresh.

Hint

refresh/invalidate.

Solution
onRefresh: () => ref.refresh(userProvider.future),

Q27. [Advanced] (Theory) What does skipLoadingOnRefresh do in .when?

Hint

No flicker.

Solution

Keeps showing existing data (no spinner flash) during a refresh, rather than reverting to the loading widget.

Q28. [Advanced] (Coding) Get the value-or-null from an AsyncValue.

Hint

valueOrNull.

Solution
final user = ref.watch(userProvider).valueOrNull; // User?

Q29. [Advanced] (Theory) What's the limitation of FutureProvider?

Hint

Read-only.

Solution

It's read-only — no methods to mutate the fetched data. For mutations, use AsyncNotifierProvider.

Q30. [Advanced] (Theory) Riverpod 3.0: what happens if a FutureProvider fails during init?

Hint

Auto-retry.

Solution

It auto-retries with exponential backoff by default (configurable via the retry parameter).


Section C — StreamProvider (Q31–44)

Q31. [Basic] (Theory) What does StreamProvider wrap and return?

Hint

Stream → AsyncValue. Part 3.

Solution

A Stream<T>; ref.watch returns an AsyncValue<T> that updates on every event.

Q32. [Basic] (Coding) Declare a StreamProvider<int> emitting every second.

Hint

Stream.periodic.

Solution
final tick = StreamProvider<int>((ref) =>
  Stream.periodic(const Duration(seconds: 1), (i) => i));

Q33. [Basic] (Theory) How is StreamProvider like FutureProvider?

Hint

Same AsyncValue.

Solution

Both expose AsyncValue and are read-only, handled identically with .when/switch; the source differs (Stream vs Future).

Q34. [Basic] (Theory) When does a StreamProvider show loading?

Hint

Before first event.

Solution

After subscribing but before the first event arrives.

Q35. [Medium] (Theory) What does Riverpod manage that you'd do manually with a raw Stream?

Hint

Subscription.

Solution

The StreamSubscription — subscribe on first watch, cancel on dispose. No manual dispose/cancel, no leak.

Q36. [Medium] (Coding) Expose a Firestore document live.

Hint

snapshots().map.

Solution
final userDoc = StreamProvider<User>((ref) =>
  firestore.collection('users').doc(uid).snapshots().map((s) => User.fromMap(s.data()!)));

Q37. [Medium] (Coding) Re-subscribe a messages stream when roomIdProvider changes.

Hint

watch input.

Solution
final messages = StreamProvider((ref) {
  final id = ref.watch(roomIdProvider);
  return ref.watch(chatRepo).messageStream(id);
});

Q38. [Medium] (Theory) What happens when a watched input of a StreamProvider changes?

Hint

Dispose + re-subscribe.

Solution

Riverpod disposes the old subscription and re-runs the function, subscribing to the new stream.

Q39. [Medium] (Coding) Use authStateChanges() as a provider.

Hint

User?.

Solution
final authState = StreamProvider<User?>((ref) => FirebaseAuth.instance.authStateChanges());

Q40. [Medium] (Theory) What if the stream emits an error?

Hint

AsyncError.

Solution

The provider's state becomes AsyncError, handled by the error: branch of .when/switch.

Q41. [Advanced] (Theory) Why is StreamProvider better than StreamBuilder?

Hint

Cache + no leak.

Solution

It caches the latest value (shared across watchers), auto-manages the subscription (no leak), and isn't re-created on rebuild like a StreamBuilder placed in build.

Q42. [Advanced] (Coding) Await the first value of a StreamProvider.

Hint

.future.

Solution
final first = await ref.watch(messagesProvider.future);

Q43. [Advanced] (Theory) Limitation of StreamProvider?

Hint

Read-only.

Solution

Read-only — it exposes the stream but offers no mutation methods. Combine stream + actions with StreamNotifierProvider.

Q44. [Advanced] (Theory) Stream-only price feed vs chat (stream + send) — which providers?

Hint

read-only vs mutable.

Solution

Price feed → StreamProvider; chat → StreamNotifierProvider.


Section D — NotifierProvider (Q45–62)

Q45. [Basic] (Theory) What does a Notifier override and return?

Hint

build(). Part 4.

Solution

build(), returning the initial (synchronous) state.

Q46. [Basic] (Coding) Write a counter Notifier + provider.

Hint

state++.

Solution
class Counter extends Notifier<int> {
  @override int build() => 0;
  void increment() => state++;
}
final counterProvider = NotifierProvider<Counter, int>(Counter.new);

Q47. [Basic] (Theory) How do you change a Notifier's state?

Hint

state =.

Solution

Assign to the state property (state = ...); it notifies listeners and rebuilds watchers.

Q48. [Basic] (Coding) Read the state vs call increment from a widget.

Hint

watch / read.notifier.

Solution
final n = ref.watch(counterProvider);                       // state
onPressed: () => ref.read(counterProvider.notifier).increment();

Q49. [Medium] (Theory) Two type args of NotifierProvider<C, T>?

Hint

Class + state.

Solution

The Notifier class C and the state type T; construct with C.new.

Q50. [Medium] (Coding) Add a todo immutably.

Hint

spread.

Solution
void add(Todo t) => state = [...state, t];

Q51. [Medium] (Theory) Why is state.add(x) wrong?

Hint

== filtering.

Solution

It mutates in place; the list identity doesn't change, so Riverpod's == update-filter may skip notifying listeners (stale UI). Assign a new list.

Q52. [Medium] (Coding) Toggle a todo's done immutably.

Hint

copyWith in a new list.

Solution
state = [for (final t in state) t.id == id ? t.copyWith(done: !t.done) : t];

Q53. [Medium] (Theory) Where does ref come from in a Notifier?

Hint

Property.

Solution

It's a class property — use ref.watch/ref.read anywhere without passing it in.

Q54. [Medium] (Coding) Make build() depend on another provider.

Hint

watch in build.

Solution
@override
List<Item> build() {
  final all = ref.watch(allItemsProvider);
  return all.where(/* ... */).toList();
}

build() re-runs when the dependency changes.

Q55. [Medium] (Theory) Analogy: Notifier is like which Flutter concept?

Hint

State object.

Solution

A State object — build() ≈ initial state, state =setState, methods ≈ actions — but app-wide in a provider.

Q56. [Medium] (Theory) When to use Notifier vs AsyncNotifier?

Hint

Sync vs await.

Solution

Notifier if build() returns state synchronously; AsyncNotifier if build() must await.

Q57. [Medium] (Coding) Theme-mode toggle notifier.

Hint

bool.

Solution
class ThemeN extends Notifier<bool> {
  @override bool build() => false;
  void toggle() => state = !state;
}
final isDarkProvider = NotifierProvider<ThemeN, bool>(ThemeN.new);

Q58. [Advanced] (Theory) Why never call a notifier method during build?

Hint

Loop.

Solution

Mutating state during build triggers a rebuild that mutates again → infinite loop. Mutations belong in callbacks/listeners/lifecycle.

Q59. [Advanced] (Coding) Reset state to initial.

Hint

state = initial.

Solution
void reset() => state = 0; // or rebuild via ref.invalidateSelf()

Q60. [Advanced] (Theory) Why is freezed a good companion to Notifier?

Hint

copyWith + ==.

Solution

It generates copyWith and value ==/hashCode for immutable state classes — exactly what immutable state = updates and == filtering need.

Q61. [Advanced] (Coding) Read the current state inside a method without rebuilding.

Hint

state property.

Solution
void addOne() => state = state + 1; // `state` reads current value directly

(Inside the notifier, just use state; no ref.read needed for own state.)

Q62. [Advanced] (Theory) What does Riverpod 3.0 use to filter Notifier updates, and how to customize?

Hint

==, updateShouldNotify.

Solution

It compares old/new state with ==; override updateShouldNotify in the Notifier to customize when listeners are notified.


Section E — AsyncNotifierProvider (Q63–78)

Q63. [Basic] (Theory) How does AsyncNotifier differ from Notifier?

Hint

Async build. Part 5.

Solution

build() is async (returns FutureOr<T>) and state is an AsyncValue<T>.

Q64. [Basic] (Coding) Write an AsyncNotifier<List<Todo>> with async build.

Hint

FutureOr.

Solution
class TodoC extends AsyncNotifier<List<Todo>> {
  @override
  FutureOr<List<Todo>> build() => ref.watch(todoRepo).fetchAll();
}
final todoProvider = AsyncNotifierProvider<TodoC, List<Todo>>(TodoC.new);

Q65. [Basic] (Theory) What does ref.watch(asyncNotifierProvider) return?

Hint

AsyncValue.

Solution

An AsyncValue<T> (loading/data/error), handled with .when/switch.

Q66. [Medium] (Coding) The canonical async mutation idiom.

Hint

loading + guard.

Solution
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
  await repo.add(x);
  return repo.fetchAll();
});

Q67. [Medium] (Theory) What does AsyncValue.guard do?

Hint

Try/catch → AsyncValue.

Solution

Runs an async function and returns AsyncData on success or AsyncError on failure — automatic error handling, no try/catch.

Q68. [Medium] (Coding) Keep old data visible during a mutation.

Hint

copyWithPrevious.

Solution
state = const AsyncLoading<List<Todo>>().copyWithPrevious(state);

Q69. [Medium] (Theory) Why use state = AsyncLoading() in a mutation?

Hint

Show progress.

Solution

To reflect the in-progress action in the UI (spinner/disabled state) while the async work runs.

Q70. [Medium] (Coding) Render an AsyncNotifier's state.

Hint

when.

Solution
ref.watch(todoProvider).when(
  loading: () => const CircularProgressIndicator(),
  error: (e, _) => Text('$e'),
  data: (todos) => TodoList(todos));

Q71. [Medium] (Theory) When promote FutureProviderAsyncNotifierProvider?

Hint

Mutate.

Solution

When you need to mutate the fetched data (add/edit/delete/refresh) with per-action loading/error.

Q72. [Medium] (Coding) Refresh an AsyncNotifier from the UI.

Hint

invalidate.

Solution
onRefresh: () => ref.invalidate(todoProvider), // re-runs build()

Q73. [Advanced] (Coding) Optimistic toggle with rollback.

Hint

set AsyncData, catch → restore.

Solution
final prev = state.valueOrNull ?? [];
state = AsyncData([for (final t in prev) t.id==id ? t.copyWith(done:!t.done) : t]);
try { await repo.toggle(id); } catch (_) { state = AsyncData(prev); }

Q74. [Advanced] (Theory) Why call ref.read (not watch) for the repo inside a mutation method?

Hint

Action.

Solution

Mutation methods are event-handler-like actions; use ref.read to grab the repo without creating a reactive dependency (watch belongs in build).

Q75. [Advanced] (Theory) What does an AsyncNotifier<void> model?

Hint

Action controller.

Solution

A controller for actions with no meaningful data state (e.g. auth sign-in) — build initializes, methods perform async operations and reflect loading/error via state.

Q76. [Advanced] (Coding) Sign-in method on an AsyncNotifier<void>.

Hint

guard signIn.

Solution
Future<void> signIn() async {
  state = const AsyncLoading();
  state = await AsyncValue.guard(ref.read(authRepo).signIn);
}

Q77. [Advanced] (Theory) What does skipLoadingOnRefresh pair with for smooth mutations?

Hint

copyWithPrevious.

Solution

copyWithPrevious(state) on the loading state — together they keep showing old data during a refresh/mutation ("stale-while-revalidate").

Q78. [Advanced] (Theory) Notifier vs AsyncNotifier vs FutureProvider — one line each.

Hint

Sync-mutable / async-mutable / async-read-only.

Solution

Notifier = sync mutable; AsyncNotifier = async mutable (CRUD); FutureProvider = async read-only fetch.


Section F — StreamNotifierProvider (Q79–88)

Q79. [Basic] (Theory) What does a StreamNotifier's build() return?

Hint

Stream. Part 6.

Solution

A Stream<T>; the provider exposes a live AsyncValue<T> plus methods.

Q80. [Basic] (Theory) Relationship to StreamProvider?

Hint
  • methods.
Solution

It's StreamProvider plus mutation methods (live stream + actions).

Q81. [Basic] (Coding) A StreamNotifier<int> ticking every second.

Hint

periodic.

Solution
class Ticker extends StreamNotifier<int> {
  @override Stream<int> build() => Stream.periodic(const Duration(seconds: 1), (i) => i);
}
final tickerProvider = StreamNotifierProvider<Ticker, int>(Ticker.new);

Q82. [Medium] (Coding) Chat controller streaming messages + sendMessage.

Hint

build returns stream; method writes.

Solution
class ChatC extends StreamNotifier<List<Message>> {
  @override Stream<List<Message>> build() => ref.watch(chatRepo).messageStream();
  Future<void> sendMessage(String t) => ref.read(chatRepo).send(t);
}

Q83. [Medium] (Theory) Read state vs call method?

Hint

watch / read.notifier.

Solution

ref.watch(provider) for the live AsyncValue; ref.read(provider.notifier).method() for actions.

Q84. [Medium] (Theory) Who manages the subscription?

Hint

Riverpod.

Solution

Riverpod — subscribe on watch, cancel on dispose (like StreamProvider).

Q85. [Medium] (Theory) When does the chat list update after sendMessage?

Hint

Stream emits.

Solution

When the underlying stream re-emits with the new message — you usually don't set state manually (the stream is the source of truth).

Q86. [Advanced] (Theory) When choose StreamNotifier over StreamProvider + separate action?

Hint

One feature.

Solution

When the live data and actions are one cohesive feature (chat/collaborative doc) — the controller encapsulates both, methods can read current streamed state, and it's one testable unit.

Q87. [Advanced] (Coding) Render chat list + wire send input.

Hint

when + onSend.

Solution
ref.watch(chatProvider).when(
  data: (m) => MessageList(m), loading: () => const Spinner(), error: (e,_) => Text('$e'));
// onSend: (t) => ref.read(chatProvider.notifier).sendMessage(t)

Q88. [Advanced] (Theory) Fill the mutable column: sync / future / stream.

Hint

Notifier trio.

Solution

NotifierProvider, AsyncNotifierProvider, StreamNotifierProvider.


Section G — Legacy & migration (Q89–100)

Q89. [Basic] (Theory) Which three providers are legacy in 3.0?

Hint

State*/Change*. Part 7.

Solution

StateProvider, StateNotifierProvider, ChangeNotifierProvider.

Q90. [Basic] (Theory) Where do legacy providers live in 3.0?

Hint

Import.

Solution

A separate legacy import (package:flutter_riverpod/legacy.dart).

Q91. [Basic] (Theory) StateProvider is replaced by…?

Hint

Notifier.

Solution

NotifierProvider.

Q92. [Medium] (Coding) Migrate StateProvider<int>((ref)=>0) with an increment.

Hint

Notifier class.

Solution
class C extends Notifier<int> { @override int build()=>0; void inc()=>state++; }
final p = NotifierProvider<C,int>(C.new);

Q93. [Medium] (Theory) Migrating StateNotifier: what replaces super(initial)?

Hint

build().

Solution

Overriding build() to return the initial state.

Q94. [Medium] (Coding) Migrate StateNotifierProvider<TodosSN, List<Todo>>((ref)=>TodosSN()).

Hint

.new.

Solution
final p = NotifierProvider<Todos, List<Todo>>(Todos.new);

(plus TodosSN extends StateNotifierTodos extends Notifier, super([])build()=>[])

Q95. [Medium] (Theory) Do read-consumers change much when migrating StateProvider?

Hint

watch same.

Solution

No — ref.watch(provider) is unchanged; only writes change from .notifier.state = to named methods.

Q96. [Medium] (Theory) Async StateNotifier migrates to…?

Hint

AsyncNotifier.

Solution

AsyncNotifier + AsyncNotifierProvider.

Q97. [Advanced] (Theory) Why migrate ChangeNotifierProvider most eagerly?

Hint

Mutable + notifyListeners.

Solution

ChangeNotifier mutates fields in place and calls notifyListeners() — opposite of Riverpod's immutable state = model; a Notifier with immutable state is more predictable.

Q98. [Advanced] (Theory) Inside a Notifier, ref is a ___ (vs StateNotifier)?

Hint

Property.

Solution

A property (in StateNotifier you had to pass ref in) — cleaner access to dependencies.

Q99. [Advanced] (Theory) Two reasons 3.0 consolidated on Notifier.

Hint

Uniform + codegen.

Solution

Any two: uniform model mirroring read-only providers; ref as property; better @riverpod code-gen; immutable-state discipline; unified auto-dispose.

Q100. [Advanced] (Theory) Should you rewrite a working app's legacy providers immediately?

Hint

Opportunistic.

Solution

No — they still work. Use the modern Notifier family for new code and migrate legacy opportunistically when you touch that code.


Coding Mini-Exercises

Ten larger problems combining provider types. Build and run each.

Exercise 1 — DI chain. DioApiClientPostRepository, all as providers.

Show solution
final dioProvider = Provider((ref) => Dio());
final apiProvider = Provider((ref) => ApiClient(ref.watch(dioProvider)));
final postRepoProvider = Provider((ref) => PostRepository(ref.watch(apiProvider)));

Exercise 2 — Fetch + render. A FutureProvider<List<Post>> rendered with .when.

Show solution
final postsProvider = FutureProvider((ref) => ref.watch(postRepoProvider).fetchAll());
// build: ref.watch(postsProvider).when(loading:..., error:..., data: (p)=>PostList(p));

Exercise 3 — Live feed. A StreamProvider of connectivity status.

Show solution
final connectivityProvider = StreamProvider<bool>((ref) =>
  Connectivity().onConnectivityChanged.map((r) => r != ConnectivityResult.none));

Exercise 4 — Counter notifier. Full Notifier counter wired to a screen.

Show solution
class Counter extends Notifier<int> { @override int build()=>0; void inc()=>state++; }
final counterProvider = NotifierProvider<Counter,int>(Counter.new);
// Text('${ref.watch(counterProvider)}'); onPressed: ()=>ref.read(counterProvider.notifier).inc();

Exercise 5 — Filtered list. A Notifier-based filter + a derived Provider that filters items.

Show solution
class FilterN extends Notifier<String> { @override String build()=>''; void set(String s)=>state=s; }
final filterProvider = NotifierProvider<FilterN,String>(FilterN.new);
final visibleProvider = Provider((ref) {
  final q = ref.watch(filterProvider);
  return ref.watch(itemsProvider).where((i) => i.contains(q)).toList();
});

Exercise 6 — Async CRUD add. An AsyncNotifier<List<Todo>> with add using the guard idiom.

Show solution
class TodoC extends AsyncNotifier<List<Todo>> {
  @override FutureOr<List<Todo>> build() => ref.watch(repo).fetchAll();
  Future<void> add(String t) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async { await ref.read(repo).add(t); return ref.read(repo).fetchAll(); });
  }
}

Exercise 7 — Optimistic delete. Add a delete(id) with rollback to the controller above.

Show solution
Future<void> delete(String id) async {
  final prev = state.valueOrNull ?? [];
  state = AsyncData(prev.where((t) => t.id != id).toList());
  try { await ref.read(repo).delete(id); } catch (_) { state = AsyncData(prev); }
}

Exercise 8 — Chat. A StreamNotifier streaming messages + send.

Show solution
class ChatC extends StreamNotifier<List<Message>> {
  @override Stream<List<Message>> build() => ref.watch(chatRepo).stream();
  Future<void> send(String t) => ref.read(chatRepo).send(t);
}
final chatProvider = StreamNotifierProvider<ChatC, List<Message>>(ChatC.new);

Exercise 9 — Migrate legacy. Rewrite StateNotifierProvider<CartSN, Cart>((ref)=>CartSN()) (with CartSN() : super(Cart.empty()) and addItem) to modern.

Show solution
class Cart extends Notifier<CartData> {
  @override CartData build() => CartData.empty();
  void addItem(Item i) => state = state.copyWith(items: [...state.items, i]);
}
final cartProvider = NotifierProvider<Cart, CartData>(Cart.new);

super(...)build(), CartSN()Cart.new, method preserved.

Exercise 10 — Capstone: a todo app. Build a todo feature: a Repository (DI via Provider), an AsyncNotifier<List<Todo>> that fetches on build() and exposes add/toggle/remove (guard + optimistic), and a ConsumerWidget rendering with .when and wiring the actions. Exercise the whole grid.

Show solution
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class Todo {
  final String id; final String title; final bool done;
  Todo(this.id, this.title, {this.done = false});
  Todo copyWith({bool? done}) => Todo(id, title, done: done ?? this.done);
}

// DI: the repository (swap for a fake in tests via override).
abstract class TodoRepository {
  Future<List<Todo>> fetchAll();
  Future<void> add(String title);
  Future<void> toggle(String id);
  Future<void> remove(String id);
}
final todoRepositoryProvider = Provider<TodoRepository>((ref) => /* real impl */ throw UnimplementedError());

// Async, mutable state controller — the heart of the feature.
class TodoListController extends AsyncNotifier<List<Todo>> {
  TodoRepository get _repo => ref.read(todoRepositoryProvider);

  @override
  FutureOr<List<Todo>> build() => ref.watch(todoRepositoryProvider).fetchAll();

  Future<void> add(String title) async {
    state = const AsyncLoading<List<Todo>>().copyWithPrevious(state); // keep old data
    state = await AsyncValue.guard(() async {
      await _repo.add(title);
      return _repo.fetchAll();
    });
  }

  Future<void> toggle(String id) async {
    final prev = state.valueOrNull ?? [];
    state = AsyncData([                                   // optimistic
      for (final t in prev) t.id == id ? t.copyWith(done: !t.done) : t,
    ]);
    try {
      await _repo.toggle(id);
    } catch (_) {
      state = AsyncData(prev);                            // rollback
    }
  }

  Future<void> remove(String id) async {
    final prev = state.valueOrNull ?? [];
    state = AsyncData(prev.where((t) => t.id != id).toList());
    try { await _repo.remove(id); } catch (_) { state = AsyncData(prev); }
  }
}

final todoListProvider =
    AsyncNotifierProvider<TodoListController, List<Todo>>(TodoListController.new);

class TodoScreen extends ConsumerWidget {
  const TodoScreen({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todosAsync = ref.watch(todoListProvider);       // WATCH async state
    final controller = ref.read(todoListProvider.notifier); // for actions

    return Scaffold(
      appBar: AppBar(title: const Text('Todos')),
      body: todosAsync.when(                               // loading/data/error
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (e, _) => Center(child: Text('Error: $e')),
        data: (todos) => RefreshIndicator(
          onRefresh: () async => ref.invalidate(todoListProvider), // re-fetch
          child: ListView(children: [
            for (final t in todos)
              CheckboxListTile(
                value: t.done,
                title: Text(t.title),
                onChanged: (_) => controller.toggle(t.id),
                secondary: IconButton(
                  icon: const Icon(Icons.delete),
                  onPressed: () => controller.remove(t.id),
                ),
              ),
          ]),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => controller.add('New todo'),
        child: const Icon(Icons.add),
      ),
    );
  }
}

This capstone exercises the whole series: DI via Provider (Part 1); async fetch with the AsyncValue model (Part 2); a mutable AsyncNotifier with the AsyncLoadingAsyncValue.guard idiom, copyWithPrevious, and optimistic updates with rollback (Part 5); reading state via watch and actions via read(.notifier) (Part 4); and invalidate for refresh. Override todoRepositoryProvider with a fake to test it all (Foundation Part 3).


You made it

Eight parts and one hundred questions. You now command every Riverpod provider:

  • The grid — value kind × mutability — and plain Provider (Part 1).
  • FutureProvider and StreamProvider for read-only async/live data (Parts 2–3).
  • NotifierProvider, AsyncNotifierProvider, StreamNotifierProvider for mutable state (Parts 4–6).
  • Legacy providers to recognize and migrate (Part 7).

Next is the third series — Mastering Riverpod: Core Concepts — where we go deep on the cross-cutting features that make these providers powerful: AsyncValue, family, autoDispose, keepAlive, invalidate/refresh, provider dependencies, and ref.onDispose.