100 Questions to Master Riverpod Core Concepts
This is Part 8 — the finale of Mastering Riverpod: Core Concepts, and of the three-part Riverpod journey. The previous seven parts covered the cross-cutting features; now you prove you own them.
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 capstone.
If you can explain the why on all 100, you've mastered Riverpod's core concepts. Let's go.
Section A — AsyncValue (Q1–16)
Q1. [Basic] (Theory) What are the three states of AsyncValue?
Hint
Loading/data/error. Part 1.
Solution
AsyncData(value), AsyncLoading(), AsyncError(error, stackTrace) — always exactly one.
Q2. [Basic] (Coding) Render an AsyncValue with .when.
Hint
Three callbacks.
Solution
async.when(data: (d)=>View(d), loading: ()=>const Spinner(), error: (e,_)=>Text('$e'));
Q3. [Basic] (Theory) Why does .when require all three callbacks?
Hint
Can't forget.
Solution
So you can't forget loading or error — it enforces complete async UI.
Q4. [Basic] (Coding) Render with a Dart 3 switch.
Hint
Sealed.
Solution
switch (async) {
AsyncData(:final value) => View(value),
AsyncError(:final error) => Text('$error'),
AsyncLoading() => const Spinner(),
}
Q5. [Basic] (Theory) Why can you switch on an AsyncValue?
Hint
Sealed → exhaustive.
Solution
It's a sealed class; switches over it are exhaustive (compiler-checked).
Q6. [Medium] (Coding) Get the value-or-null from an AsyncValue.
Hint
valueOrNull.
Solution
final v = async.valueOrNull; // T?
Q7. [Medium] (Theory) Difference between .value and .requireValue?
Hint
Null vs throw.
Solution
.value/.valueOrNull return T?; .requireValue returns T but throws if no value.
Q8. [Medium] (Coding) Disable a button while loading.
Hint
isLoading.
Solution
onPressed: async.isLoading ? null : _submit,
Q9. [Medium] (Theory) What does skipLoadingOnRefresh: true do?
Hint
No spinner flash.
Solution
Keeps showing existing data during a refresh instead of the loading widget (with preserved previous data).
Q10. [Medium] (Coding) Transform AsyncValue<List<T>> into AsyncValue<int> of its length.
Hint
whenData.
Solution
final count = listAsync.whenData((l) => l.length);
Q11. [Medium] (Theory) What does whenData pass through?
Hint
Loading/error.
Solution
It transforms the data and passes loading/error states through unchanged.
Q12. [Medium] (Coding) Use maybeWhen to get the name or 'Loading…'.
Hint
orElse.
Solution
async.maybeWhen(data: (u) => u.name, orElse: () => 'Loading…');
Q13. [Advanced] (Theory) What does copyWithPrevious enable?
Hint
Stale-while-revalidate.
Solution
It carries previous data through a loading state so the UI shows old data during refresh instead of blanking.
Q14. [Advanced] (Coding) Match premium vs standard data with a guard.
Hint
when in switch.
Solution
switch (async) {
AsyncData(:final value) when value.isPremium => PremiumView(value),
AsyncData(:final value) => StandardView(value),
_ => const Spinner(),
}
Q15. [Advanced] (Theory) What does isRefreshing mean?
Hint
Loading + previous.
Solution
The state is loading but a previous value exists (a refresh in progress over existing data).
Q16. [Advanced] (Theory) Why is AsyncValue called the "lingua franca" of async Riverpod?
Hint
Every async provider.
Solution
Every async provider (Future/Stream/AsyncNotifier/StreamNotifier) exposes one, so a single set of tools handles all async state uniformly.
Section B — family (Q17–30)
Q17. [Basic] (Theory) What does family do?
Hint
Parameterize. Part 2.
Solution
Turns a provider into a parameterized one — each argument gets its own independent, cached state.
Q18. [Basic] (Coding) Declare a FutureProvider.family<User, String>.
Hint
(ref, id).
Solution
final userProvider = FutureProvider.autoDispose.family<User, String>(
(ref, id) => ref.watch(userRepo).fetchById(id));
Q19. [Basic] (Coding) Watch the family for id '123'.
Hint
Call it.
Solution
ref.watch(userProvider('123'));
Q20. [Basic] (Theory) Mental model for a family?
Hint
Map.
Solution
A Map keyed by the argument, with the provider state as the value.
Q21. [Medium] (Theory) Why must family args have stable ==/hashCode?
Hint
Cache key.
Solution
The argument is the cache key; identity-based equality (fresh list/object) causes cache misses → refetch every time.
Q22. [Medium] (Coding) Fix ref.watch(p([1,2,3])) refetching constantly.
Hint
Value equality.
Solution
Pass a value-equality arg — a String/int or a record/freezed class, not a fresh List.
Q23. [Medium] (Coding) Multi-param family via a record.
Hint
Record key.
Solution
final p = FutureProvider.autoDispose.family<Feed, ({String city, int page})>(
(ref, a) => repo.fetch(a.city, a.page));
// ref.watch(p((city:'NYC', page:1)))
Q24. [Medium] (Theory) Why are records perfect family keys?
Hint
Structural ==.
Solution
They have value (structural) equality for free, so composite keys compare by value.
Q25. [Medium] (Theory) Why pair family with autoDispose?
Hint
Per-arg leak.
Solution
Each argument creates a cached instance; without autoDispose they accumulate unbounded — a leak.
Q26. [Medium] (Coding) Sync family: a label per int.
Hint
Provider.family.
Solution
final labelProvider = Provider.family<String, int>((ref, n) => 'Item #$n');
Q27. [Advanced] (Coding) Notifier family via code-gen.
Hint
build(arg).
Solution
@riverpod
class TodoItem extends _$TodoItem {
@override Future<Todo> build(String id) => ref.watch(repo).fetchById(id);
}
Q28. [Advanced] (Theory) What lint rule catches bad family params?
Hint
provider_parameters.
Solution
provider_parameters (in riverpod_lint).
Q29. [Advanced] (Theory) Are userProvider('1') and userProvider('2') independent?
Hint
Per key.
Solution
Yes — each argument is a separate cached instance with its own state/lifecycle.
Q30. [Advanced] (Coding) Override one family instance for a test.
Hint
p(arg).overrideWith.
Solution
userProvider('123').overrideWith((ref) => User(name: 'Test'));
Section C — autoDispose (Q31–44)
Q31. [Basic] (Theory) What does autoDispose do?
Hint
Destroy when unwatched. Part 3.
Solution
Destroys the provider's state when the last listener stops watching it.
Q32. [Basic] (Theory) Default disposal of a manual provider?
Hint
Forever.
Solution
Keep-alive — cached for the app's lifetime (until the ProviderScope is disposed).
Q33. [Basic] (Coding) Make a provider autoDispose (modifier form).
Hint
.autoDispose.
Solution
final p = FutureProvider.autoDispose<User>((ref) => fetch());
Q34. [Basic] (Theory) The trade-off autoDispose makes?
Hint
Cache vs cleanup.
Solution
Caching for cleanup — less memory but recreate/refetch on next use.
Q35. [Medium] (Theory) Walk the autoDispose lifecycle when the last listener leaves.
Hint
onCancel/frame/onDispose.
Solution
onCancel → wait one frame → if still unused, onDispose (destroy); a returning listener triggers onResume (kept).
Q36. [Medium] (Theory) Why the one-frame grace period?
Hint
Navigation.
Solution
So quick rebuilds/navigation (brief zero-listener gaps) don't cause disposal — it only tears down when genuinely unused.
Q37. [Medium] (Coding) autoDispose or keep-alive: auth state, product-by-id, theme, search results?
Hint
App-wide vs transient.
Solution
keep-alive, autoDispose, keep-alive, autoDispose.
Q38. [Medium] (Theory) Why do families need autoDispose?
Hint
Per-arg instances.
Solution
Each argument caches an instance forever otherwise; autoDispose bounds memory by destroying unused ones.
Q39. [Medium] (Theory) Code-gen default: autoDispose or keep-alive?
Hint
Opposite of manual.
Solution
autoDispose by default (@Riverpod(keepAlive: true) to opt out).
Q40. [Medium] (Coding) 3.0 manual isAutoDispose form.
Hint
Named param.
Solution
final p = Provider<String>(isAutoDispose: true, (ref) => 'hi');
Q41. [Advanced] (Theory) Which hook fires when the last listener leaves (before disposal)?
Hint
onCancel.
Solution
ref.onCancel.
Q42. [Advanced] (Theory) What re-engages auto-disposal after keepAlive()?
Hint
link.close.
Solution
link.close() on the KeepAliveLink (or the provider recomputing).
Q43. [Advanced] (Theory) Why are autoDispose providers more leak-prone for resources?
Hint
Destroyed often.
Solution
They're destroyed frequently during normal use, so any resource they create without onDispose leaks each time.
Q44. [Advanced] (Theory) Is autoDispose just on/off?
Hint
keepAlive/onDispose.
Solution
No — it's the base for nuanced policies via keepAlive() (selective keep) and onDispose (cleanup).
Section D — keepAlive (Q45–58)
Q45. [Basic] (Theory) What does ref.keepAlive() do?
Hint
Pin. Part 4.
Solution
Inside an autoDispose provider, prevents auto-disposal and returns a KeepAliveLink.
Q46. [Basic] (Theory) What does link.close() do?
Hint
Unpin.
Solution
Re-enables automatic disposal (unpins the provider).
Q47. [Basic] (Coding) Keep alive only on successful fetch.
Hint
After await.
Solution
final p = FutureProvider.autoDispose<User>((ref) async {
final u = await fetch();
ref.keepAlive(); // only on success
return u;
});
Q48. [Basic] (Theory) Why does keepAlive belong inside autoDispose?
Hint
Nothing to prevent otherwise.
Solution
It overrides auto-disposal; a keep-alive-by-default provider never auto-disposes, so there's nothing to prevent.
Q49. [Medium] (Coding) The cacheFor extension.
Hint
keepAlive + Timer + onDispose.
Solution
extension on Ref {
void cacheFor(Duration d) {
final link = keepAlive();
final t = Timer(d, link.close);
onDispose(t.cancel);
}
}
Q50. [Medium] (Theory) What happens on failure with the keep-on-success pattern?
Hint
keepAlive not reached.
Solution
The await throws, keepAlive() isn't reached, so the failed provider stays auto-dispose and is torn down (retry next time).
Q51. [Medium] (Theory) keepAlive vs plain keep-alive (no autoDispose)?
Hint
Conditional vs always.
Solution
Plain keep-alive caches unconditionally forever; autoDispose + keepAlive caches conditionally (you can pin/unpin based on runtime conditions).
Q52. [Medium] (Coding) Cache a feed for 2 minutes.
Hint
cacheFor.
Solution
final feed = FutureProvider.autoDispose((ref) async {
ref.cacheFor(const Duration(minutes: 2));
return repo.fetch();
});
Q53. [Medium] (Theory) Why cancel the timer in cacheFor's onDispose?
Hint
Destroyed early.
Solution
If the provider is destroyed before the timer fires, the timer would dangle (and call link.close on a dead provider); onDispose(timer.cancel) cleans it up.
Q54. [Medium] (Theory) Pick the approach: session auth state for the whole app.
Hint
Unconditional.
Solution
Plain keep-alive (no autoDispose) — unconditional app-wide state.
Q55. [Advanced] (Coding) Keep alive while logged in, dispose on logout.
Hint
keepAlive + listen.
Solution
final link = ref.keepAlive();
ref.listen(authProvider, (_, next) { if (next == null) link.close(); });
Q56. [Advanced] (Theory) What lint warns about keepAlive misuse?
Hint
inside keep alive.
Solution
A riverpod_lint rule about using keepAlive outside an autoDispose context.
Q57. [Advanced] (Theory) Manual pin/unpin cache-clear pattern?
Hint
Store link.
Solution
Store the KeepAliveLink from keepAlive() and call link.close() from a method when the user clears the cache.
Q58. [Advanced] (Theory) The general keepAlive pattern in one line?
Hint
pin/hold/close.
Solution
keepAlive() to pin, hold the KeepAliveLink, link.close() to unpin when your condition says so.
Section E — invalidate vs refresh (Q59–72)
Q59. [Basic] (Theory) What do both invalidate and refresh do?
Hint
Recompute. Part 5.
Solution
Discard cached state and cause the provider to recompute.
Q60. [Basic] (Theory) What does invalidate return?
Hint
void.
Solution
void — it just marks stale.
Q61. [Basic] (Theory) What does refresh return?
Hint
New value.
Solution
The newly-computed value (it invalidates and reads).
Q62. [Basic] (Coding) Retry a failed provider from a button.
Hint
invalidate.
Solution
onPressed: () => ref.invalidate(dataProvider),
Q63. [Medium] (Coding) Pull-to-refresh awaiting the re-fetch.
Hint
refresh .future.
Solution
onRefresh: () => ref.refresh(postsProvider.future),
Q64. [Medium] (Theory) Express refresh via invalidate + read.
Hint
Two calls.
Solution
ref.invalidate(p); final v = ref.read(p); — refresh = invalidate + read.
Q65. [Medium] (Theory) Why is invalidate "lazy"?
Hint
Recompute on read.
Solution
It only marks stale; if watched it recomputes, else it recomputes on the next read.
Q66. [Medium] (Coding) Invalidate one family instance vs all.
Hint
p(arg) vs p.
Solution
ref.invalidate(userProvider('1')); // one
ref.invalidate(userProvider); // all
Q67. [Medium] (Theory) How does a notifier re-run its own build?
Hint
invalidateSelf.
Solution
ref.invalidateSelf().
Q68. [Medium] (Coding) Reload a list after a mutation in another provider.
Hint
invalidate after write.
Solution
await repo.update(item);
ref.invalidate(itemListProvider);
Q69. [Advanced] (Theory) Default recommendation: invalidate or refresh?
Hint
Use the value?
Solution
Default to invalidate for "just refresh"; use refresh only when you need the returned value.
Q70. [Advanced] (Theory) Why might invalidate not recompute immediately in a test?
Hint
No watcher.
Solution
It's lazy; with no listener, it recomputes only on the next read. Use refresh/read to force it now.
Q71. [Advanced] (Theory) What happens to a non-watched provider when invalidated?
Hint
Disposed.
Solution
It's disposed and recreated on the next read (if watched, it recomputes immediately).
Q72. [Advanced] (Theory) When is refresh(p.future) the right idiom?
Hint
Await.
Solution
Pull-to-refresh — you need the Future to await so the indicator spins until the new data arrives.
Section F — Provider dependencies (Q73–86)
Q73. [Basic] (Theory) How does one provider depend on another?
Hint
watch. Part 6.
Solution
ref.watch(other) inside its create/build — recomputes when other changes.
Q74. [Basic] (Coding) Derive tax = 8% of subtotal.
Hint
watch subtotal.
Solution
final taxProvider = Provider((ref) => ref.watch(subtotalProvider) * 0.08);
Q75. [Basic] (Theory) watch vs read inside a provider?
Hint
Depend vs once.
Solution
watch creates a dependency (recompute on change); read is a one-off with no dependency.
Q76. [Basic] (Theory) Where do you use read inside a Notifier?
Hint
Methods.
Solution
In action methods (one-off reads); watch goes in build().
Q77. [Medium] (Theory) What stops rebuilds from propagating endlessly?
Hint
==.
Solution
Equality filtering — if a recomputed value equals the old (==), propagation stops there.
Q78. [Medium] (Coding) Fix a stale derived provider that uses read on its source.
Hint
watch.
Solution
Change ref.read(source) in build logic to ref.watch(source).
Q79. [Medium] (Coding) React to auth change inside a provider without recomputing.
Hint
listen.
Solution
ref.listen(authProvider, (prev, next) { if (next == null) logOut(); });
Q80. [Medium] (Coding) Depend on an async provider's value.
Hint
.future.
Solution
final user = await ref.watch(userProvider.future);
Q81. [Medium] (Theory) What is a circular dependency?
Hint
A↔B.
Solution
Two providers watching each other (A←B, B←A) — neither can initialize; Riverpod throws.
Q82. [Medium] (Theory) Three ways to break a cycle.
Hint
Merge/third/read.
Solution
Merge into one provider; extract a shared third provider; or use one-directional read in an action (not build).
Q83. [Advanced] (Theory) Why decompose state into many small providers?
Hint
Granular + testable.
Solution
Granular rebuilds (only downstream-of-change recompute, trimmed by ==), easy testing/overriding, and the graph documents data flow.
Q84. [Advanced] (Coding) Combine a Notifier's build (watch user) with a method (read repo).
Hint
watch in build, read in method.
Solution
@override Cart build() => Cart.forUser(ref.watch(userProvider));
void checkout() => ref.read(checkoutRepo).submit(state);
Q85. [Advanced] (Theory) Can you customize the equality filtering?
Hint
updateShouldNotify.
Solution
Yes — override updateShouldNotify on a Notifier to control when listeners are notified.
Q86. [Advanced] (Theory) What shape must the dependency graph be?
Hint
Acyclic.
Solution
A DAG (directed acyclic graph) — no cycles.
Section G — onDispose (Q87–100)
Q87. [Basic] (Theory) What does ref.onDispose do?
Hint
Cleanup. Part 7.
Solution
Registers a callback that runs when the provider's state is destroyed — to clean up resources.
Q88. [Basic] (Coding) Close a StreamController in a provider.
Hint
onDispose close.
Solution
ref.onDispose(controller.close);
Q89. [Basic] (Coding) Cancel a Timer.
Hint
onDispose cancel.
Solution
ref.onDispose(timer.cancel);
Q90. [Basic] (Theory) Widget analogue of onDispose?
Hint
dispose().
Solution
A State's dispose() method.
Q91. [Medium] (Theory) Does onDispose fire only once at end of life?
Hint
Each recompute.
Solution
No — it fires whenever state is destroyed, including on every recompute (invalidate/refresh/dependency change).
Q92. [Medium] (Theory) onCancel vs onDispose?
Hint
Last listener vs destroyed.
Solution
onCancel = last listener left (not destroyed yet); onDispose = state actually destroyed (release resources).
Q93. [Medium] (Theory) What is onResume for?
Hint
New listener.
Solution
Fires when a new listener appears after onCancel — resume paused work.
Q94. [Medium] (Coding) Cancel a manual subscription on dispose.
Hint
sub.cancel.
Solution
final sub = stream.listen(handler);
ref.onDispose(sub.cancel);
Q95. [Medium] (Theory) Which providers auto-cancel their own stream subscription?
Hint
Stream providers.
Solution
StreamProvider/StreamNotifierProvider (for the stream they expose); resources you create by hand are your responsibility.
Q96. [Medium] (Coding) Register multiple cleanups.
Hint
Call twice.
Solution
ref.onDispose(timer.cancel);
ref.onDispose(controller.close); // both run on dispose, in order
Q97. [Advanced] (Theory) Why are autoDispose/family providers leak-prone?
Hint
Destroyed often.
Solution
They're created/destroyed frequently, so any unmanaged resource leaks each time — making onDispose essential.
Q98. [Advanced] (Theory) How does onDispose make cacheFor safe?
Hint
Cancel dangling timer.
Solution
onDispose(timer.cancel) cancels the cache timer if the provider is destroyed before it fires, preventing a leaked timer / callback on a dead provider.
Q99. [Advanced] (Theory) What's the pairing rule for resources in providers?
Hint
Create + onDispose.
Solution
Pair every resource creation with an ref.onDispose for it — like initState/dispose in widgets.
Q100. [Advanced] (Theory) Order the autoDispose lifecycle hooks.
Hint
cancel → frame → dispose.
Solution
onCancel → wait one frame → onDispose (if still unused); onResume if a listener returns within the frame.
Coding Mini-Exercises
Ten larger problems combining the Core Concepts. Build and run each.
Exercise 1 — AsyncValue UI. Render a FutureProvider with .when and a retry button.
Show solution
ref.watch(p).when(
data: (d) => View(d),
loading: () => const CircularProgressIndicator(),
error: (e, _) => Column(children: [
Text('$e'),
ElevatedButton(onPressed: () => ref.invalidate(p), child: const Text('Retry')),
]),
);
Exercise 2 — Family fetch. A FutureProvider.autoDispose.family<User,String> and watching two ids.
Show solution
final userProvider = FutureProvider.autoDispose.family<User, String>(
(ref, id) => ref.watch(repo).fetchById(id));
// ref.watch(userProvider('1')); ref.watch(userProvider('2'));
Exercise 3 — Cache for 5 minutes. Use the cacheFor extension on a dashboard provider.
Show solution
final dashboardProvider = FutureProvider.autoDispose((ref) async {
ref.cacheFor(const Duration(minutes: 5));
return ref.watch(repo).fetch();
});
Exercise 4 — Keep on success. Cache a profile only when the fetch succeeds.
Show solution
final profileProvider = FutureProvider.autoDispose<Profile>((ref) async {
final p = await ref.watch(repo).fetch();
ref.keepAlive();
return p;
});
Exercise 5 — Pull-to-refresh. Wire RefreshIndicator to re-fetch a list.
Show solution
RefreshIndicator(
onRefresh: () => ref.refresh(listProvider.future),
child: /* list */,
);
Exercise 6 — Derived graph. subtotal → tax → total providers.
Show solution
final taxProvider = Provider((ref) => ref.watch(subtotalProvider) * 0.08);
final totalProvider = Provider((ref) => ref.watch(subtotalProvider) + ref.watch(taxProvider));
Exercise 7 — Cleanup. A provider with a periodic timer that's leak-free.
Show solution
final pollProvider = StreamProvider.autoDispose<int>((ref) {
final c = StreamController<int>();
final t = Timer.periodic(const Duration(seconds: 5), (_) => c.add(0));
ref.onDispose(() { t.cancel(); c.close(); });
return c.stream;
});
Exercise 8 — Side effect on dependency. Clear a cache provider when auth becomes null.
Show solution
final cacheProvider = Provider<Cache>((ref) {
final cache = Cache();
ref.listen(authProvider, (_, next) { if (next == null) cache.clear(); });
ref.onDispose(cache.dispose);
return cache;
});
Exercise 9 — Invalidate after write. A mutation that reloads a separate list provider.
Show solution
Future<void> addItem(Item i) async {
await ref.read(repoProvider).add(i);
ref.invalidate(itemListProvider); // list recomputes
}
Exercise 10 — Capstone: a cached, parameterized detail screen. Build a productProvider family that: is autoDispose, fetches a product by id, keeps alive for 5 minutes on success, exposes loading/error/data via AsyncValue, supports retry (invalidate) and pull-to-refresh (refresh), and cleans up. Exercise the whole series.
Show solution
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Reusable caching policy (Part 4 + Part 7).
extension CacheForExtension on Ref {
void cacheFor(Duration duration) {
final link = keepAlive();
final timer = Timer(duration, link.close);
onDispose(timer.cancel);
}
}
final productRepositoryProvider =
Provider<ProductRepository>((ref) => throw UnimplementedError());
// Parameterized, auto-disposing, cached-on-success product provider.
final productProvider =
FutureProvider.autoDispose.family<Product, String>((ref, id) async {
final product = await ref.watch(productRepositoryProvider).fetchById(id); // Part 2/6
ref.cacheFor(const Duration(minutes: 5)); // keepAlive + timer + onDispose (Parts 4,7)
return product; // reached only on success
});
class ProductScreen extends ConsumerWidget {
final String productId;
const ProductScreen({super.key, required this.productId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final productAsync = ref.watch(productProvider(productId)); // family + AsyncValue
return Scaffold(
appBar: AppBar(title: const Text('Product')),
body: RefreshIndicator(
onRefresh: () => ref.refresh(productProvider(productId).future), // Part 5
child: productAsync.when( // Part 1
skipLoadingOnRefresh: true,
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => ListView(children: [
const SizedBox(height: 200),
Center(child: Text('Failed: $e')),
Center(
child: ElevatedButton(
onPressed: () => ref.invalidate(productProvider(productId)), // retry, Part 5
child: const Text('Retry'),
),
),
]),
data: (product) => ListView(children: [
ListTile(title: Text(product.name), subtitle: Text('\$${product.price}')),
]),
),
),
);
}
}
This capstone exercises the whole Core Concepts series: a family keyed by id (Part 2); autoDispose for per-screen memory (Part 3); keepAlive + a timer for a 5-minute cache, cleaned up via onDispose (Parts 4 & 7); the AsyncValue .when with skipLoadingOnRefresh (Part 1); invalidate for retry and refresh for pull-to-refresh (Part 5); and a dependency on the repository (Part 6). Override productRepositoryProvider with a fake to test it all.
You made it — and you've finished the Riverpod journey
Eight parts, one hundred questions, and a capstone — and with it, three complete Riverpod series. You now command:
- Foundations — why Riverpod, setup,
ProviderScope, reading providers (Foundation series). - Provider Types — the full grid from
ProvidertoStreamNotifierProvider(Provider Types series). - Core Concepts —
AsyncValue,family,autoDispose,keepAlive,invalidate/refresh, dependencies, andonDispose(this series).
That's the complete mental model for production Riverpod. Revisit any question you needed a hint for in a week — then go build something real, and watch how naturally the pieces fit together.