100 Questions to Master Riverpod Theming
This is Part 6 — the finale of Mastering Riverpod: Theming. The previous five parts built a persistent, reactive, system-aware, scope-overridable theme system; now you prove you own it.
How to use this bank:
- 100 questions, grouped by part, 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 + Flutter code.
- After the 100 there are 10 coding mini-exercises with full solutions, ending in a capstone that wires the whole system together.
If you can explain the why on all 100, you can ship a production theming layer. Let's go.
Section A — ThemeMode as a NotifierProvider (Q1–20)
Q1. [Basic] (Theory) Which provider type holds ThemeMode, and why not a plain Provider?
Hint
Mutable. Part 1.
Solution
A NotifierProvider — the mode is mutable (the user changes it). A plain Provider is read-only and can't be reassigned from the UI.
Q2. [Basic] (Coding) Write a ThemeModeNotifier defaulting to system with setMode.
Hint
build() + state =.
Solution
class ThemeModeNotifier extends Notifier<ThemeMode> {
@override ThemeMode build() => ThemeMode.system;
void setMode(ThemeMode m) => state = m;
}
final themeModeProvider = NotifierProvider<ThemeModeNotifier, ThemeMode>(ThemeModeNotifier.new);
Q3. [Basic] (Theory) What does the notifier's build() return?
Hint
Initial state.
Solution
The initial ThemeMode — typically ThemeMode.system, the respectful default.
Q4. [Basic] (Coding) Wire MaterialApp to the provider.
Hint
watch → themeMode.
Solution
final mode = ref.watch(themeModeProvider);
MaterialApp(theme: light, darkTheme: dark, themeMode: mode, home: const HomePage());
(Root is a ConsumerWidget, wrapped in ProviderScope.)
Q5. [Basic] (Theory) What must wrap the app for the provider to work?
Hint
Root.
Solution
A ProviderScope at the very top (runApp(const ProviderScope(child: App()))).
Q6. [Medium] (Theory) Why must MaterialApp ref.watch (not ref.read) the provider?
Hint
Rebuild on change.
Solution
It must rebuild with the new themeMode whenever the mode changes; watch subscribes it, read would grab a stale one-time value.
Q7. [Medium] (Coding) Toggle the theme from a button.
Hint
read(.notifier).
Solution
onPressed: () => ref.read(themeModeProvider.notifier).toggle(),
Q8. [Medium] (Theory) In a toggle button, which ref method gets the current mode (for the icon) vs calls toggle()?
Hint
watch + read.
Solution
ref.watch for the current mode (so the icon updates); ref.read(provider.notifier).toggle() inside onPressed for the action.
Q9. [Medium] (Coding) A three-way Light/Dark/System selector.
Hint
SegmentedButton + setMode.
Solution
SegmentedButton<ThemeMode>(
segments: const [
ButtonSegment(value: ThemeMode.light, label: Text('Light')),
ButtonSegment(value: ThemeMode.dark, label: Text('Dark')),
ButtonSegment(value: ThemeMode.system, label: Text('System')),
],
selected: {ref.watch(themeModeProvider)},
onSelectionChanged: (s) => ref.read(themeModeProvider.notifier).setMode(s.first),
);
Q10. [Medium] (Theory) How does changing themeMode recolor the whole app with no per-widget wiring?
Hint
Inherited theme.
Solution
MaterialApp rebuilds with the new mode, swapping the active ThemeData; widgets that read theme roles via the inherited Theme rebuild and re-read the new scheme.
Q11. [Medium] (Theory) Why is ref.read(...notifier) correct inside onPressed?
Hint
Act, not subscribe.
Solution
A callback should act (call a method), not subscribe. read(provider.notifier) fetches the notifier once; watch-ing it just to call a method is the anti-pattern.
Q12. [Medium] (Coding) Implement toggle() flipping light/dark (treat system as light).
Hint
Ternary on state.
Solution
void toggle() => state = state == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark;
Q13. [Medium] (Theory) Why can a settings toggle six screens deep change the global theme with no callbacks?
Hint
Talks to the provider.
Solution
It calls ref.read(themeModeProvider.notifier).setMode(...) — talking to the provider, not a parent. MaterialApp separately watches the same provider and rebuilds. No onThemeChanged prop-drilling.
Q14. [Medium] (Theory) What's wrong with a global mutable themeMode variable?
Hint
No reactivity.
Solution
Changing it doesn't rebuild anything — no reactivity. You'd bolt on a ChangeNotifier and reinvent providers.
Q15. [Advanced] (Theory) Compare NotifierProvider to setState-plus-callbacks for theme mode.
Hint
Prop-drilling.
Solution
setState keeps the mode in one widget's State, so any descendant changing it needs an onThemeChanged callback prop-drilled down. NotifierProvider makes the mode globally readable/mutable via ref, with automatic scoped rebuilds — and sets up cleanly for persistence/testing.
Q16. [Advanced] (Coding) Make the root a ConsumerWidget and show the minimal main + App.
Hint
ProviderScope + ConsumerWidget.
Solution
void main() => runApp(const ProviderScope(child: App()));
class App extends ConsumerWidget {
const App({super.key});
@override
Widget build(BuildContext c, WidgetRef ref) => MaterialApp(
theme: light, darkTheme: dark,
themeMode: ref.watch(themeModeProvider), home: const HomePage());
}
Q17. [Advanced] (Theory) Why does this design make testing the theme logic easy?
Hint
Override.
Solution
The mode lives in an overridable provider; tests can override it (or the notifier) in a ProviderScope/ProviderContainer and assert without driving real UI.
Q18. [Advanced] (Theory) Why is raw InheritedWidget a poor choice here?
Hint
Mutate from below.
Solution
It's lots of boilerplate and awkward to mutate from descendants; NotifierProvider gives clean read+mutate from anywhere with ref.
Q19. [Advanced] (Theory) What's the "spine" this part establishes?
Hint
State / watch / change.
Solution
ThemeMode is state (a NotifierProvider), MaterialApp watches it, and any widget can change it.
Q20. [Advanced] (Coding) Override themeModeProvider to force dark in a widget test.
Hint
overrideWith.
Solution
ProviderScope(
overrides: [
themeModeProvider.overrideWith(() => _FakeMode(ThemeMode.dark)),
],
child: const App(),
);
(or override the notifier with a fake that builds ThemeMode.dark).
Section B — Persistence with SharedPreferences (Q21–40)
Q21. [Basic] (Theory) What is SharedPreferences good (and not good) for?
Hint
Small KV. Part 2.
Solution
Good for small primitive key-value settings (theme choice, flags, username); not for large/structured data — use a database/files.
Q22. [Basic] (Coding) Serialize a ThemeMode to a string and back.
Hint
mode.name.
Solution
Write mode.name ('light'/'dark'/'system'); read the string and map back, defaulting to ThemeMode.system.
Q23. [Basic] (Theory) Why does the notifier become an AsyncNotifier now?
Hint
await on build.
Solution
Initialization depends on storage; getInstance() is async, so build() is async and state is modeled as AsyncValue (the "if build() awaits, use AsyncNotifier" rule).
Q24. [Basic] (Coding) Write ThemeRepository.read() with a switch.
Hint
getString → enum.
Solution
ThemeMode read() => switch (_prefs.getString('themeMode')) {
'light' => ThemeMode.light,
'dark' => ThemeMode.dark,
_ => ThemeMode.system,
};
Q25. [Basic] (Theory) What type is the state after persistence is added?
Hint
Async.
Solution
AsyncValue<ThemeMode> (loading / data / error).
Q26. [Medium] (Coding) Provide SharedPreferences via a provider that throws until overridden.
Hint
UnimplementedError.
Solution
final sharedPrefsProvider = Provider<SharedPreferences>(
(ref) => throw UnimplementedError('Override in ProviderScope'));
Q27. [Medium] (Coding) Override the prefs provider in main.
Hint
await getInstance.
Solution
final prefs = await SharedPreferences.getInstance();
ProviderScope(overrides: [sharedPrefsProvider.overrideWithValue(prefs)], child: const App());
(plus WidgetsFlutterBinding.ensureInitialized().)
Q28. [Medium] (Theory) Why pre-load prefs in main instead of inside the notifier?
Hint
Sync downstream.
Solution
Doing the one unavoidable async load once makes every downstream read synchronous and keeps the store injectable/overridable (testable), avoiding repeated async calls.
Q29. [Medium] (Coding) Implement setMode with an optimistic update then persist.
Hint
AsyncData first.
Solution
Future<void> setMode(ThemeMode m) async {
state = AsyncData(m); // optimistic
await ref.read(themeRepositoryProvider).write(m); // persist
}
Q30. [Medium] (Theory) Why set state = AsyncData(mode) before awaiting the write?
Hint
Instant UI.
Solution
So the UI flips immediately instead of waiting on the disk write (a few ms); the write persists a moment later. No perceived latency.
Q31. [Medium] (Coding) Read the saved value in build() via the repository.
Hint
watch repo.read().
Solution
@override
Future<ThemeMode> build() async => ref.watch(themeRepositoryProvider).read();
Q32. [Medium] (Theory) Why route persistence through a ThemeRepository?
Hint
Seam.
Solution
It isolates storage behind a testable seam — swap stores or inject a fake repository in tests without touching notifier logic.
Q33. [Medium] (Coding) Consume the async state in the root without a spinner.
Hint
valueOrNull ?? system.
Solution
final mode = ref.watch(themeModeProvider).valueOrNull ?? ThemeMode.system;
Q34. [Medium] (Theory) Why fall back to system instead of showing a loading screen?
Hint
Non-blocking.
Solution
A theme is non-blocking; rendering with system and snapping to the saved value (usually the same frame, since prefs are pre-loaded) is smoother than gating the app behind a spinner for one setting.
Q35. [Medium] (Coding) Implement toggle() on the AsyncNotifier.
Hint
valueOrNull.
Solution
Future<void> toggle() async {
final cur = state.valueOrNull ?? ThemeMode.system;
await setMode(cur == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark);
}
Q36. [Advanced] (Theory) Sketch handling a write that fails while keeping the UI responsive.
Hint
try/catch around write.
Solution
Keep the optimistic state = AsyncData(mode), wrap await write(mode) in try/catch. On failure, either surface a transient snackbar (keep the new look) or revert and set state = AsyncError(e, st) — for a theme, a non-fatal snackbar while keeping the change is usually best.
Q37. [Advanced] (Theory) What does this load-on-build / save-on-change / inject-the-store pattern generalize to?
Hint
Other settings.
Solution
Any persisted setting — locale, onboarding flags, auth tokens, feature toggles. The pattern is universal.
Q38. [Advanced] (Theory) What does Riverpod 3.0 offer that automates this?
Hint
Offline persistence.
Solution
Built-in offline persistence — providers can cache/restore their state locally automatically. Doing it manually once teaches what it's doing under the hood.
Q39. [Advanced] (Coding) Test the repository with a fake prefs/in-memory store.
Hint
Override repo.
Solution
final c = ProviderContainer.test(overrides: [
themeRepositoryProvider.overrideWithValue(FakeThemeRepository(ThemeMode.dark)),
]);
expect(await c.read(themeModeProvider.future), ThemeMode.dark);
Q40. [Advanced] (Theory) Why is mode.name a good serialization choice over the enum index?
Hint
Stable across reorders.
Solution
name is a stable string ('dark') that survives reordering or inserting enum values; an index would silently change meaning if the enum's order changed, corrupting saved preferences.
Section C — Runtime switching (Q41–60)
Q41. [Basic] (Theory) Why does a Flutter theme change need no app restart?
Hint
Just a rebuild. Part 3.
Solution
It's a rebuild: changing the active ThemeData updates the inherited Theme, and dependent widgets rebuild with new values in a frame. The tree persists; only read values change.
Q42. [Basic] (Theory) What does reading Theme.of(context) do besides return the theme?
Hint
Subscribe.
Solution
It subscribes the widget to the inherited theme, so it rebuilds when the theme changes.
Q43. [Basic] (Coding) Trace the chain from toggle tap to dark screen.
Hint
state → MaterialApp → Theme → widgets.
Solution
toggle() → state changes → MaterialApp (watching) rebuilds with new themeMode → installs dark ThemeData → widgets reading Theme.of(context) rebuild with dark roles → repaint.
Q44. [Basic] (Theory) In a 20-screen app, which widgets should ref.watch(themeModeProvider)?
Hint
Root + control.
Solution
Essentially just the root MaterialApp (to set themeMode) plus any control showing the current mode. Leaves read Theme.of(context).
Q45. [Medium] (Coding) Narrow a subscription to just "is dark."
Hint
select.
Solution
final isDark = ref.watch(themeModeProvider.select((m) => m == ThemeMode.dark));
Q46. [Medium] (Theory) How do const widgets and select each keep theme-switch rebuilds cheap?
Hint
Skip + narrow.
Solution
const subtrees are canonicalized and skipped during rebuilds; select narrows a subscription to a derived slice so the widget rebuilds only when that slice changes.
Q47. [Medium] (Coding) Cross-fade theme changes over 250ms for a subtree.
Hint
AnimatedTheme.
Solution
AnimatedTheme(
data: Theme.of(context),
duration: const Duration(milliseconds: 250),
child: const Body(),
)
Q48. [Medium] (Theory) Does MaterialApp already animate top-level theme changes?
Hint
Wraps in AnimatedTheme.
Solution
Yes — it wraps the app in an AnimatedTheme, so top-level changes cross-fade by default; you can wrap a subtree for a custom duration/curve.
Q49. [Medium] (Theory) Why shouldn't every screen watch themeModeProvider?
Hint
Double-subscribe.
Solution
The inherited theme already rebuilds theme-reading widgets; watching the provider too is redundant double-subscription. Read the theme, not the mode provider, in leaves.
Q50. [Medium] (Coding) Tween a single surface color on theme change.
Hint
TweenAnimationBuilder + ColorTween.
Solution
TweenAnimationBuilder<Color?>(
tween: ColorTween(end: Theme.of(context).colorScheme.surface),
duration: const Duration(milliseconds: 300),
builder: (c, color, child) => Container(color: color, child: child),
child: const Content(),
)
Q51. [Medium] (Theory) Why keep theme-transition durations short (~200–300ms)?
Hint
Sluggish vs polished.
Solution
A long fade on a theme change feels sluggish, not smooth; short transitions read as "polished," not "showy."
Q52. [Medium] (Coding) Show that only theme-reading widgets rebuild.
Hint
debugPrint in build.
Solution
Widget build(BuildContext c, WidgetRef ref) {
debugPrint('rebuilt');
final s = Theme.of(c).colorScheme; // subscribes → prints on each toggle
return Container(color: s.surfaceContainerHigh);
}
A pure const Text('Hi') won't be forced to rebuild.
Q53. [Advanced] (Theory) A widget reads the theme in initState and reuses it; it won't update. Why, and fix?
Hint
Subscribe in build.
Solution
The theme subscription is established by reading Theme.of(context) during build (via dependOnInheritedWidgetOfExactType). Reading in initState captures a one-time value (and is discouraged). Move the read into build so it re-subscribes and re-reads on each change.
Q54. [Advanced] (Theory) Toggle does nothing for one custom badge. Two likely causes.
Hint
Hardcoded or captured.
Solution
It hardcoded a color (replace with a scheme role), or it captured the color outside build (read Theme.of(context) in build instead).
Q55. [Advanced] (Theory) "Whole app rebuilds heavily" on toggle — cause and fix?
Hint
Big non-const watcher.
Solution
A large non-const subtree watches the theme unnecessarily. const the static parts and read the theme narrowly so only dependent widgets rebuild.
Q56. [Advanced] (Coding) Root reads async mode; show the one line that drives themeMode.
Hint
valueOrNull.
Solution
themeMode: ref.watch(themeModeProvider).valueOrNull ?? ThemeMode.system,
Q57. [Advanced] (Theory) "MaterialApp doesn't update on toggle" — two configuration causes.
Hint
ConsumerWidget / ProviderScope.
Solution
The root isn't a ConsumerWidget (so it doesn't ref.watch), or it isn't under a ProviderScope. Fix both.
Q58. [Advanced] (Theory) Why is the root-only subscription "elegant"?
Hint
Decoupled.
Solution
One provider subscription at the root drives a whole tree of theme-reading widgets; the provider concern and the theme concern stay decoupled, and leaves don't know a provider drives the theme.
Q59. [Advanced] (Coding) Make a leaf widget that reacts to theme without touching the provider.
Hint
Theme.of only.
Solution
class PriceTag extends StatelessWidget {
const PriceTag({super.key});
@override
Widget build(BuildContext c) =>
Text('\$9', style: TextStyle(color: Theme.of(c).colorScheme.primary));
}
Q60. [Advanced] (Theory) Summarize keeping runtime switches cheap in one line.
Hint
const + narrow + select.
Solution
const static subtrees, read the theme narrowly (only the root watches the mode), and use select for derived slices.
Section D — System brightness (Q61–80)
Q61. [Basic] (Theory) What two inputs decide "is the app dark," and which wins?
Hint
OS vs override. Part 4.
Solution
The OS brightness and the user's ThemeMode. A light/dark override wins; the OS only applies in system mode.
Q62. [Basic] (Coding) Read the OS brightness inside a widget.
Hint
platformBrightnessOf.
Solution
final os = MediaQuery.platformBrightnessOf(context); // Brightness.light/dark
Q63. [Basic] (Theory) Is MediaQuery.platformBrightnessOf reactive?
Hint
Rebuild on OS change.
Solution
Yes — widgets reading it rebuild when the user changes the OS setting while the app is open.
Q64. [Basic] (Theory) Why can't a Riverpod provider call platformBrightnessOf directly?
Hint
No context.
Solution
It needs a BuildContext, which providers don't have. Bridge it via a WidgetsBindingObserver notifier (or push from a widget).
Q65. [Medium] (Coding) Sketch the PlatformBrightnessNotifier with an observer.
Hint
addObserver + didChangePlatformBrightness.
Solution
class PlatformBrightnessNotifier extends Notifier<Brightness> with WidgetsBindingObserver {
@override
Brightness build() {
final b = WidgetsBinding.instance;
b.addObserver(this);
ref.onDispose(() => b.removeObserver(this));
return b.platformDispatcher.platformBrightness;
}
@override
void didChangePlatformBrightness() =>
state = WidgetsBinding.instance.platformDispatcher.platformBrightness;
}
Q66. [Medium] (Theory) Which callback fires when the OS dark-mode setting changes?
Hint
didChange…
Solution
WidgetsBindingObserver.didChangePlatformBrightness() — read platformDispatcher.platformBrightness and assign to state.
Q67. [Medium] (Theory) What cleanup must the OS-brightness notifier do, and where?
Hint
removeObserver onDispose.
Solution
Remove its observer when disposed: ref.onDispose(() => WidgetsBinding.instance.removeObserver(this));. Otherwise it leaks past the provider's life.
Q68. [Medium] (Coding) Write effectiveBrightnessProvider.
Hint
switch on mode.
Solution
final effectiveBrightnessProvider = Provider<Brightness>((ref) {
final mode = ref.watch(themeModeProvider).valueOrNull ?? ThemeMode.system;
return switch (mode) {
ThemeMode.light => Brightness.light,
ThemeMode.dark => Brightness.dark,
ThemeMode.system => ref.watch(platformBrightnessProvider),
};
});
Q69. [Medium] (Theory) State the resolution rule effectiveBrightnessProvider encodes.
Hint
Override beats OS.
Solution
If mode is light/dark, that override sets the brightness (OS ignored); if system, effective brightness equals the OS brightness. It recomputes when either input changes.
Q70. [Medium] (Coding) A brightness-aware logo that honors overrides.
Hint
watch effective.
Solution
final isDark = ref.watch(effectiveBrightnessProvider) == Brightness.dark;
return Image.asset(isDark ? 'assets/logo_light.png' : 'assets/logo_dark.png');
Q71. [Medium] (Theory) What bug does reading only the OS brightness cause?
Hint
Ignores override.
Solution
Forced-dark users on a light OS get light-mode assets/colors in your custom widgets (even though MaterialApp is dark), because the in-app override was ignored. Resolve through the effective provider.
Q72. [Medium] (Theory) Inside widgets, when is Theme.of(context).brightness the simplest correct read?
Hint
Rendering logic.
Solution
For rendering logic, almost always — MaterialApp already resolved themeMode, so it reflects the effective theme. Use the providers when non-widget logic needs the answer.
Q73. [Medium] (Coding) Which API for: raw OS setting; applied theme brightness; effective in provider logic?
Hint
Three APIs.
Solution
MediaQuery.platformBrightnessOf(context); Theme.of(context).brightness; ref.watch(effectiveBrightnessProvider).
Q74. [Medium] (Theory) For ThemeMode.system to ever go dark, what must MaterialApp have?
Hint
darkTheme.
Solution
A non-null darkTheme — system detection is pointless without a dark theme to switch to.
Q75. [Advanced] (Theory) Why is a derived Provider (not a Notifier) right for effective brightness?
Hint
Pure function.
Solution
It's a pure function of two reactive inputs with no mutable state of its own; a computed Provider that watches both recomputes whenever either changes. A Notifier would add needless mutable state to keep in sync.
Q76. [Advanced] (Coding) Expose OS brightness only to widgets (no provider). What's the minimal approach?
Hint
MediaQuery.
Solution
Skip the notifier entirely and call MediaQuery.platformBrightnessOf(context) — it's already reactive. The notifier is only needed when other providers depend on OS brightness.
Q77. [Advanced] (Theory) When does effectiveBrightnessProvider recompute?
Hint
Either input.
Solution
When the mode changes (user picks light/dark/system) or, in system mode, when the OS brightness flips — because it watches both inputs.
Q78. [Advanced] (Coding) Use the effective brightness in non-widget logic (e.g. analytics).
Hint
read in a method.
Solution
final b = ref.read(effectiveBrightnessProvider);
analytics.log('theme', {'dark': b == Brightness.dark});
Q79. [Advanced] (Theory) Why does the logo using effectiveBrightnessProvider stay correct when both inputs change?
Hint
Graph propagation.
Solution
The provider derives from both themeModeProvider and platformBrightnessProvider; Riverpod recomputes and re-notifies it whenever either changes, so the watching logo always reflects the true effective brightness.
Q80. [Advanced] (Theory) Summarize the system-brightness architecture in one sentence.
Hint
OS notifier + derived effective.
Solution
Bridge OS brightness into a WidgetsBindingObserver notifier (with onDispose cleanup) and resolve it against the user's ThemeMode in a derived effectiveBrightnessProvider, watched by any logic that needs "is the app effectively dark."
Section E — Per-feature overrides with ProviderScope (Q81–100)
Q81. [Basic] (Theory) What does a nested ProviderScope do to overrides?
Hint
Subtree only. Part 5.
Solution
It applies the overrides only to that subtree; descendants see overridden values, the rest of the app keeps the originals.
Q82. [Basic] (Theory) Why expose the theme/seed as a provider for per-feature overriding?
Hint
Need something to override.
Solution
You can only override something that comes from a provider; sourcing the seed/theme from a provider gives a handle to override per subtree.
Q83. [Basic] (Coding) Expose the seed and a derived light-theme provider.
Hint
fromSeed.
Solution
final seedProvider = Provider<Color>((ref) => const Color(0xFF6750A4));
final lightThemeProvider = Provider<ThemeData>(
(ref) => themeFor(ref.watch(seedProvider), Brightness.light));
Q84. [Basic] (Coding) Override the seed with gold for a feature subtree.
Hint
overrideWithValue.
Solution
ProviderScope(
overrides: [seedProvider.overrideWithValue(const Color(0xFFB8860B))],
child: const PremiumThemed(),
)
Q85. [Medium] (Theory) What two layers make a per-feature theme actually render?
Hint
Scope + Theme.
Solution
The nested ProviderScope override (changes what theme providers compute) and a local Theme widget (changes what Theme.of(context) returns for Material widgets in the subtree).
Q86. [Medium] (Coding) Re-apply the recomputed theme locally inside the scope.
Hint
Theme(data: watch(...)).
Solution
final theme = Theme.of(context).brightness == Brightness.dark
? ref.watch(darkThemeProvider) : ref.watch(lightThemeProvider);
return Theme(data: theme, child: const PremiumBody());
Q87. [Medium] (Theory) When is a plain local Theme enough (no scope)?
Hint
Widgets only.
Solution
When only widgets (via Theme.of(context)) consume the local theme — no provider logic depends on the overridden seed/brand.
Q88. [Medium] (Coding) Force a media-viewer subtree always dark.
Hint
local Theme dark.
Solution
Theme(
data: themeFor(const Color(0xFF6750A4), Brightness.dark),
child: const ViewerBody(),
)
The global themeModeProvider is untouched.
Q89. [Medium] (Theory) Why does the always-dark subtree not affect the global theme?
Hint
Local Theme only.
Solution
It wraps the subtree in a local Theme with dark ThemeData; Theme.of(context) returns dark only inside that subtree — MaterialApp/themeModeProvider are unchanged.
Q90. [Medium] (Theory) Why both the scope and the Theme in the premium example?
Hint
Compute vs return.
Solution
The scope override changes what the theme providers compute (gold seed); the Theme widget changes what Theme.of(context) returns so standard widgets actually render gold. Different layers.
Q91. [Medium] (Coding) Read the overridden seed inside the scoped subtree.
Hint
watch seedProvider.
Solution
final seed = ref.watch(seedProvider); // gold inside the nested scope, purple outside
Q92. [Medium] (Theory) "Global by default, local where needed" — which Riverpod feature enables this?
Hint
Scoping.
Solution
Nested ProviderScope scoping/overrides — the same mechanism Riverpod uses for all scoped state.
Q93. [Advanced] (Theory) A real scenario where the scope override is necessary (local Theme won't do).
Hint
Whitelabel logic.
Solution
A whitelabel partner flow where logic (not just colors) depends on the brand: analytics tags, a logo provider, copy/string providers and the theme all read brandProvider/seedProvider. A nested scope overriding brandProvider makes every provider in that branch compute partner-specific values; a local Theme would only re-skin Material widgets.
Q94. [Advanced] (Coding) Override two providers (seed + brand) for one feature.
Hint
List of overrides.
Solution
ProviderScope(
overrides: [
seedProvider.overrideWithValue(partnerSeed),
brandProvider.overrideWithValue(partnerBrand),
],
child: const PartnerFlow(),
)
Q95. [Advanced] (Theory) Why is over-using nested scopes a smell?
Hint
Most needs are local Theme.
Solution
Most "this screen looks different" needs are just a local Theme; reaching for a ProviderScope override when no provider logic depends on the value adds needless complexity. Reserve it for genuine per-feature provider state.
Q96. [Advanced] (Theory) How does Part 5 compose with Parts 1–4?
Hint
And yet.
Solution
Global mode (P1), persisted (P2), live-switched (P3), OS-resolved (P4) — and yet any subtree can opt out with a local override (P5). Global by default, local where needed.
Q97. [Advanced] (Coding) Source the root MaterialApp themes from providers.
Hint
watch light/dark theme providers.
Solution
MaterialApp(
theme: ref.watch(lightThemeProvider),
darkTheme: ref.watch(darkThemeProvider),
themeMode: ref.watch(themeModeProvider).valueOrNull ?? ThemeMode.system,
home: const HomePage());
Q98. [Advanced] (Theory) If you override seedProvider in a nested scope but forget the local Theme, what happens?
Hint
Providers change, widgets don't.
Solution
The theme providers recompute from the new seed inside the scope, but standard Material widgets still read the ancestor Theme (unchanged), so they don't re-skin. You need the local Theme to feed the recomputed ThemeData into Theme.of(context).
Q99. [Advanced] (Theory) Decision rule: local Theme vs nested ProviderScope override?
Hint
Who consumes it.
Solution
If only widgets consume the local theme (Theme.of(context)), use a plain local Theme. If providers in the subtree must also see the overridden value, use a nested ProviderScope override (plus a local Theme for rendering).
Q100. [Advanced] (Theory) Summarize the entire series' architecture in one sentence.
Hint
State → persist → switch → resolve → scope.
Solution
ThemeMode is a (persisted) NotifierProvider/AsyncNotifier watched by MaterialApp, switched live via the inherited theme, resolved against the OS in a derived effectiveBrightnessProvider, and overridable per subtree with nested ProviderScope — global by default, local where needed.
Coding Mini-Exercises
Ten larger problems assembling the theming system. Build and run each.
Exercise 1 — Mode notifier. A NotifierProvider<ThemeMode> with setMode/toggle, wired into MaterialApp.
Show solution
class ThemeModeNotifier extends Notifier<ThemeMode> {
@override ThemeMode build() => ThemeMode.system;
void setMode(ThemeMode m) => state = m;
void toggle() => state = state == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark;
}
final themeModeProvider = NotifierProvider<ThemeModeNotifier, ThemeMode>(ThemeModeNotifier.new);
// MaterialApp(themeMode: ref.watch(themeModeProvider), ...)
Exercise 2 — Toggle button. An IconButton that shows sun/moon and flips the theme.
Show solution
final isDark = ref.watch(themeModeProvider) == ThemeMode.dark;
IconButton(
icon: Icon(isDark ? Icons.light_mode : Icons.dark_mode),
onPressed: () => ref.read(themeModeProvider.notifier).toggle());
Exercise 3 — Repository. A ThemeRepository reading/writing ThemeMode to prefs.
Show solution
class ThemeRepository {
ThemeRepository(this._p); final SharedPreferences _p;
ThemeMode read() => switch (_p.getString('themeMode')) {
'light' => ThemeMode.light, 'dark' => ThemeMode.dark, _ => ThemeMode.system };
Future<void> write(ThemeMode m) => _p.setString('themeMode', m.name);
}
Exercise 4 — AsyncNotifier persistence. Convert the mode notifier to an AsyncNotifier that loads on build and saves on change.
Show solution
class ThemeModeNotifier extends AsyncNotifier<ThemeMode> {
@override Future<ThemeMode> build() async => ref.watch(themeRepositoryProvider).read();
Future<void> setMode(ThemeMode m) async {
state = AsyncData(m);
await ref.read(themeRepositoryProvider).write(m);
}
}
final themeModeProvider = AsyncNotifierProvider<ThemeModeNotifier, ThemeMode>(ThemeModeNotifier.new);
Exercise 5 — Inject prefs. main() that pre-loads prefs and overrides the provider.
Show solution
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final prefs = await SharedPreferences.getInstance();
runApp(ProviderScope(
overrides: [sharedPrefsProvider.overrideWithValue(prefs)], child: const App()));
}
Exercise 6 — No-spinner root. Root consumes the async mode and falls back to system.
Show solution
final mode = ref.watch(themeModeProvider).valueOrNull ?? ThemeMode.system;
MaterialApp(theme: light, darkTheme: dark, themeMode: mode, home: const HomePage());
Exercise 7 — OS brightness provider. A WidgetsBindingObserver notifier exposing OS brightness with cleanup.
Show solution
class PlatformBrightnessNotifier extends Notifier<Brightness> with WidgetsBindingObserver {
@override Brightness build() {
final b = WidgetsBinding.instance;
b.addObserver(this);
ref.onDispose(() => b.removeObserver(this));
return b.platformDispatcher.platformBrightness;
}
@override void didChangePlatformBrightness() =>
state = WidgetsBinding.instance.platformDispatcher.platformBrightness;
}
final platformBrightnessProvider =
NotifierProvider<PlatformBrightnessNotifier, Brightness>(PlatformBrightnessNotifier.new);
Exercise 8 — Effective brightness. Resolve override-vs-OS in a derived provider, and a logo that uses it.
Show solution
final effectiveBrightnessProvider = Provider<Brightness>((ref) {
final mode = ref.watch(themeModeProvider).valueOrNull ?? ThemeMode.system;
return switch (mode) {
ThemeMode.light => Brightness.light,
ThemeMode.dark => Brightness.dark,
ThemeMode.system => ref.watch(platformBrightnessProvider),
};
});
// final isDark = ref.watch(effectiveBrightnessProvider) == Brightness.dark;
Exercise 9 — Per-feature override. Give a premium subtree a gold theme via nested scope + local Theme.
Show solution
ProviderScope(
overrides: [seedProvider.overrideWithValue(const Color(0xFFB8860B))],
child: Consumer(builder: (c, ref, _) {
final theme = Theme.of(c).brightness == Brightness.dark
? ref.watch(darkThemeProvider) : ref.watch(lightThemeProvider);
return Theme(data: theme, child: const PremiumBody());
}),
)
Exercise 10 — Capstone: the complete theme system. Assemble everything: a persisted AsyncNotifier<ThemeMode>, an OS-brightness notifier, a derived effectiveBrightnessProvider, seed-driven theme providers, a root MaterialApp wired to all of it, a toggle, and one per-feature override.
Show solution
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
// --- Theme factory + seed-driven providers (Part 5) ---
ThemeData themeFor(Color seed, Brightness b) =>
ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: seed, brightness: b));
final seedProvider = Provider<Color>((ref) => const Color(0xFF6750A4));
final lightThemeProvider = Provider<ThemeData>((ref) => themeFor(ref.watch(seedProvider), Brightness.light));
final darkThemeProvider = Provider<ThemeData>((ref) => themeFor(ref.watch(seedProvider), Brightness.dark));
// --- Persistence seam (Part 2) ---
final sharedPrefsProvider = Provider<SharedPreferences>((ref) => throw UnimplementedError());
class ThemeRepository {
ThemeRepository(this._p); final SharedPreferences _p; static const _k = 'themeMode';
ThemeMode read() => switch (_p.getString(_k)) {
'light' => ThemeMode.light, 'dark' => ThemeMode.dark, _ => ThemeMode.system };
Future<void> write(ThemeMode m) => _p.setString(_k, m.name);
}
final themeRepositoryProvider = Provider<ThemeRepository>((ref) => ThemeRepository(ref.watch(sharedPrefsProvider)));
// --- Persisted mode state (Parts 1+2) ---
class ThemeModeNotifier extends AsyncNotifier<ThemeMode> {
@override Future<ThemeMode> build() async => ref.watch(themeRepositoryProvider).read();
Future<void> setMode(ThemeMode m) async {
state = AsyncData(m);
await ref.read(themeRepositoryProvider).write(m);
}
Future<void> toggle() async {
final cur = state.valueOrNull ?? ThemeMode.system;
await setMode(cur == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark);
}
}
final themeModeProvider = AsyncNotifierProvider<ThemeModeNotifier, ThemeMode>(ThemeModeNotifier.new);
// --- OS brightness + effective resolution (Part 4) ---
class PlatformBrightnessNotifier extends Notifier<Brightness> with WidgetsBindingObserver {
@override Brightness build() {
final b = WidgetsBinding.instance;
b.addObserver(this);
ref.onDispose(() => b.removeObserver(this));
return b.platformDispatcher.platformBrightness;
}
@override void didChangePlatformBrightness() =>
state = WidgetsBinding.instance.platformDispatcher.platformBrightness;
}
final platformBrightnessProvider =
NotifierProvider<PlatformBrightnessNotifier, Brightness>(PlatformBrightnessNotifier.new);
final effectiveBrightnessProvider = Provider<Brightness>((ref) {
final mode = ref.watch(themeModeProvider).valueOrNull ?? ThemeMode.system;
return switch (mode) {
ThemeMode.light => Brightness.light,
ThemeMode.dark => Brightness.dark,
ThemeMode.system => ref.watch(platformBrightnessProvider),
};
});
// --- Root (Parts 1+3) ---
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final prefs = await SharedPreferences.getInstance();
runApp(ProviderScope(
overrides: [sharedPrefsProvider.overrideWithValue(prefs)],
child: const App(),
));
}
class App extends ConsumerWidget {
const App({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
theme: ref.watch(lightThemeProvider),
darkTheme: ref.watch(darkThemeProvider),
themeMode: ref.watch(themeModeProvider).valueOrNull ?? ThemeMode.system,
home: const HomePage(),
);
}
}
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isDark = ref.watch(effectiveBrightnessProvider) == Brightness.dark;
return Scaffold(
appBar: AppBar(
title: const Text('Theme demo'),
actions: [
IconButton(
icon: Icon(isDark ? Icons.light_mode : Icons.dark_mode),
onPressed: () => ref.read(themeModeProvider.notifier).toggle(),
),
],
),
// Per-feature override (Part 5): a gold "premium" card.
body: ProviderScope(
overrides: [seedProvider.overrideWithValue(const Color(0xFFB8860B))],
child: Consumer(builder: (c, r, _) {
final theme = Theme.of(c).brightness == Brightness.dark
? r.watch(darkThemeProvider) : r.watch(lightThemeProvider);
return Theme(
data: theme,
child: const Card(child: ListTile(title: Text('Premium (gold)'))),
);
}),
),
);
}
}
This capstone assembles the whole series: a persisted AsyncNotifier<ThemeMode> with a repository seam (Part 1 + Part 2); a root MaterialApp that watches it for a live, restart-free switch (Part 3); an OS-brightness notifier with onDispose cleanup and a derived effectiveBrightnessProvider (Part 4); and a nested ProviderScope seed override + local Theme for a per-feature palette (Part 5). Override sharedPrefsProvider/themeRepositoryProvider with fakes to test it all.
You made it — the theme system is complete
Six parts and one hundred questions. You now own a production-grade Flutter theming layer driven by Riverpod:
- Mode as state — a
NotifierProviderany widget can read and change (Part 1). - Persistence — SharedPreferences + an
AsyncNotifier, repository seam, optimistic updates (Part 2). - Runtime switching — why it needs no restart, kept cheap with
const/select(Part 3). - System brightness — OS detection + a derived effective-brightness provider (Part 4).
- Per-feature overrides — nested
ProviderScopefor local exceptions (Part 5).
Revisit any question you needed a hint for in a week — then theme a real app and watch every screen stay in sync, persist across launches, follow the OS, and still let one feature break the rules.