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 NotifierProvider — Provider 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 FutureProvider → AsyncNotifierProvider?
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 StateNotifier → Todos 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. Dio → ApiClient → PostRepository, 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 AsyncLoading → AsyncValue.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). FutureProviderandStreamProviderfor read-only async/live data (Parts 2–3).NotifierProvider,AsyncNotifierProvider,StreamNotifierProviderfor 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.