100 Questions to Master Flutter State Management
This is Part 7 — the finale of the Flutter State Management series. The previous six parts taught the tools; this is where you prove you can choose and wield 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, only then check the solution.
- For (Coding) questions, sketch the widget/code yourself. dartpad.dev runs Flutter snippets.
- After the 100, there are 10 coding mini-exercises with full solutions, ending in a capstone app.
If you can explain the why and pick the right tool on all 100, you understand Flutter state management at a senior level. Let's go.
Section A — setState & the rebuild problem (Q1–16)
Q1. [Basic] (Theory) What does setState actually do?
Hint
It marks something dirty. See Part 1.
Solution
It runs your callback (mutate fields), marks the State's element dirty via markNeedsBuild(), and returns. Flutter rebuilds that State's build() on the next frame. It doesn't paint anything itself.
Q2. [Basic] (Theory) State the core Flutter equation for UI and state.
Hint
UI is a function of...
Solution
UI = f(state) — the screen is a function of state; change state, re-run build(), get new pixels.
Q3. [Basic] (Coding) Why doesn't this update the screen?
void add() => n++;
Hint
No dirty flag.
Solution
It mutates n but never calls setState, so the element is never marked dirty and build() never re-runs. Use setState(() => n++).
Q4. [Basic] (Theory) Where can you legally call setState?
Hint
Which widget type?
Solution
Inside a State object (of a StatefulWidget). A StatelessWidget has no setState.
Q5. [Medium] (Coding) Fix the crash:
Future<void> load() async {
final d = await api.get();
setState(() => _d = d);
}
Hint
Widget might be gone.
Solution
final d = await api.get();
if (!mounted) return;
setState(() => _d = d);
mounted guards against calling setState after dispose().
Q6. [Medium] (Theory) Is setState synchronous or asynchronous in effect?
Hint
When does the rebuild happen?
Solution
The callback runs synchronously, but the rebuild is deferred to the next frame — asynchronous in effect. The new tree doesn't exist on the next line.
Q7. [Medium] (Coding) Why does this throw, and the fix?
@override
Widget build(BuildContext context) {
setState(() {});
return const SizedBox();
}
Hint
setState during build.
Solution
Calling setState during build schedules a rebuild during a build → "setState() called during build" error/loop. Never call it in build; trigger state changes from callbacks or lifecycle methods.
Q8. [Medium] (Theory) Why does setState cause "over-rebuilding"?
Hint
Scope of the rebuild.
Solution
It rebuilds the entire build() of the State, including expensive children that didn't change.
Q9. [Medium] (Coding) A counter tap rebuilds a heavy sibling Chart. Give two fixes.
Hint
Scope down / const.
Solution
(1) Extract the counter into its own small StatefulWidget so setState is scoped to it. (2) Make Chart const/cached so it's skipped on rebuild. (Or adopt a state solution that rebuilds only the counter.)
Q10. [Basic] (Theory) What is mounted?
Hint
A lifecycle flag.
Solution
A State flag: true while the State is in the tree, false after dispose(). Used to guard async setState.
Q11. [Medium] (Theory) Why put the mutation inside the setState callback?
Hint
Intent + ordering.
Solution
It makes intent clear ("these changes cause this rebuild") and ensures the mutation happens before the dirty flag is acted on. Mutating outside and calling setState(() {}) works but is a smell.
Q12. [Advanced] (Theory) Precisely why doesn't setState scale to app-wide shared state?
Hint
It only rebuilds one owner.
Solution
It can only rebuild the widget that owns the state. Sharing across distant widgets forces lifting state up and prop-drilling it through every layer — boilerplate, coupling, over-rebuilding, tangled ownership. It has no way for a far widget to read the state directly.
Q13. [Basic] (Theory) Give two examples where setState is the correct, non-fallback choice.
Hint
Local, ephemeral.
Solution
E.g. a password obscure toggle, an expand/collapse flag, an animation value, a focus state — local UI state owned by one widget.
Q14. [Medium] (Coding) Does calling setState with no actual field change still rebuild?
Hint
It doesn't diff.
Solution
Yes — setState just marks dirty; it doesn't diff your fields. A no-op setState(() {}) still triggers a (wasted) rebuild.
Q15. [Advanced] (Theory) What is "prop drilling" and which problems does it create?
Hint
Threading through ancestors.
Solution
Passing shared state down through every intermediate widget via constructors. It creates boilerplate (params widgets don't use), tight coupling (one change touches many files), over-rebuilding (top-level setState), and unclear ownership.
Q16. [Advanced] (Coding) Sketch the smell: a themeMode needed by app bar + a deep child with only setState. What must you do?
Hint
Lift + thread.
Solution
Lift themeMode (and a toggle callback) to a common ancestor's State, then thread both through every intermediate constructor to each consumer — prop drilling. This motivates InheritedWidget/Provider (Part 2).
Section B — InheritedWidget (Q17–32)
Q17. [Basic] (Theory) What problem does InheritedWidget solve?
Hint
Pull, not push.
Solution
It lets any descendant read shared state directly via of(context) instead of prop-drilling, and rebuilds only the readers when it changes.
Q18. [Basic] (Theory) What does the .of(context) convention signal?
Hint
Reading from above.
Solution
You're reading an InheritedWidget above you in the tree, by type, via BuildContext (e.g. Theme.of, MediaQuery.of).
Q19. [Medium] (Theory) How does a widget subscribe to an InheritedWidget?
Hint
Reading is subscribing.
Solution
By calling context.dependOnInheritedWidgetOfExactType<T>() — the read itself registers the element as a dependent. No separate subscribe call.
Q20. [Medium] (Theory) What does updateShouldNotify control?
Hint
Who rebuilds.
Solution
When the InheritedWidget is rebuilt, it decides whether dependents rebuild: true → all dependents rebuild; false → none. Returning true only on real changes prevents wasted rebuilds.
Q21. [Medium] (Coding) Write static of for an InheritedWidget Settings.
Hint
dependOn + assert.
Solution
static Settings of(BuildContext context) {
final s = context.dependOnInheritedWidgetOfExactType<Settings>();
assert(s != null, 'No Settings found');
return s!;
}
Q22. [Medium] (Coding) With updateShouldNotify(old) => v != old.v, the widget rebuilds but v is unchanged. Do dependents rebuild?
Hint
What does it return?
Solution
No — returns false, so no dependent is notified even though a new instance was created.
Q23. [Medium] (Theory) dependOnInheritedWidgetOfExactType vs getInheritedWidgetOfExactType?
Hint
Subscribe vs not.
Solution
dependOn... subscribes (registers a dependency, rebuilds on change) — use in build. get... reads without subscribing — use in callbacks/initState.
Q24. [Advanced] (Theory) Since InheritedWidget is immutable, how does its value change?
Hint
Something above rebuilds it.
Solution
A StatefulWidget (or InheritedNotifier with a Listenable) above it holds the mutable state and, on change, rebuilds the InheritedWidget with new data; updateShouldNotify then triggers dependent rebuilds.
Q25. [Basic] (Theory) Name three framework features that are InheritedWidgets.
Hint
Theme, MediaQuery...
Solution
Theme.of, MediaQuery.of, Navigator.of, DefaultTextStyle.of — and Provider/Riverpod's scope are built on it.
Q26. [Advanced] (Theory) Why is the inherited lookup O(1), not a slow climb?
Hint
Elements cache by type.
Solution
Each Element keeps a map of in-scope InheritedWidgets by type, so dependOnInheritedWidgetOfExactType<T>() is a map lookup, not a linear ancestor scan.
Q27. [Medium] (Coding) What is InheritedNotifier for?
Hint
Listenable bridge.
Solution
It wraps a Listenable (e.g. ChangeNotifier) and rebuilds dependents whenever the notifier fires — no manual updateShouldNotify diffing. It's basically how ChangeNotifierProvider works internally.
Q28. [Advanced] (Theory) Why is reading subscribing a clever design?
Hint
No bookkeeping.
Solution
It removes a whole class of bugs: you can't read a value and forget to subscribe (or subscribe and forget to read). The dependency is established exactly when and where you use the value.
Q29. [Medium] (Coding) A descendant calls Settings.of(context).darkMode in build. When does it rebuild?
Hint
Dependent + updateShouldNotify.
Solution
Whenever Settings is rebuilt and updateShouldNotify returns true (e.g. darkMode changed). It's a registered dependent because it read via of.
Q30. [Advanced] (Theory) Why is hand-writing InheritedWidget for every shared value impractical?
Hint
Boilerplate per value.
Solution
Each value needs an InheritedWidget, a StatefulWidget wrapper, the of method, updateShouldNotify, and a mutation API. Repeated across many values it's huge boilerplate — which is why Provider/Riverpod exist.
Q31. [Basic] (Theory) What does an InheritedWidget require in its constructor besides data?
Hint
A subtree.
Solution
A child (the subtree it wraps and exposes data to), passed via super.child.
Q32. [Advanced] (Coding) Show the StatefulWidget + InheritedWidget pattern that makes a counter changeable.
Hint
setState rebuilds the inherited widget.
Solution
class _CounterScopeState extends State<CounterScope> {
int _n = 0;
void inc() => setState(() => _n++);
@override
Widget build(BuildContext context) =>
_CounterInherited(n: _n, inc: inc, child: widget.child);
}
The State owns mutation; each change rebuilds the inherited widget with a new n.
Section C — Provider (Q33–49)
Q33. [Basic] (Theory) What is Provider in one sentence?
Hint
Wraps Part 2.
Solution
A package that wraps InheritedWidget (plus notifiers) with a clean API, removing the hand-written boilerplate for shared state.
Q34. [Basic] (Coding) A ChangeNotifier mutates but the UI never updates. Why?
Hint
The 🔔.
Solution
Missing notifyListeners() after the mutation. Add it so listeners rebuild.
Q35. [Medium] (Theory) State the watch vs read rule.
Hint
build vs callbacks.
Solution
watch in build() (subscribe, rebuild on change); read in callbacks/initState (one-off, no subscription).
Q36. [Medium] (Coding) Fix: onPressed: () => context.watch<Cart>().add('x').
Hint
Wrong API in callback.
Solution
onPressed: () => context.read<Cart>().add('x') — callbacks use read, not watch.
Q37. [Medium] (Theory) What does select / Selector add over watch / Consumer?
Hint
One slice.
Solution
They subscribe to a derived slice and rebuild only when that slice changes, not on every notifyListeners() — avoiding over-rebuilding.
Q38. [Medium] (Theory) create: vs .value in ChangeNotifierProvider?
Hint
Who disposes.
Solution
create: lets Provider own and dispose a freshly built object (common case). .value is for an instance owned elsewhere (Provider won't dispose it). Misusing .value with a new object risks leaks.
Q39. [Basic] (Coding) How do you expose two models without nesting providers deeply?
Hint
Multi.
Solution
MultiProvider(providers: [...], child: ...).
Q40. [Medium] (Coding) Why does Consumer's optional child improve performance?
Hint
Built once.
Solution
The child doesn't depend on the model; Consumer builds it once and reuses it across rebuilds, keeping that subtree out of the rebuild path.
Q41. [Medium] (Theory) Which Provider exposes a service with no notifications?
Hint
Plain.
Solution
Provider<T> — for DI of a value/service (e.g. an ApiClient) that doesn't notify.
Q42. [Advanced] (Theory) Why is ProviderNotFoundException a runtime error?
Hint
Type lookup via tree.
Solution
Provider resolves by type through the BuildContext/tree, which the compiler can't check. If no provider of that type is above, it compiles but throws at runtime — no compile-time safety.
Q43. [Advanced] (Theory) Why do two Provider<int> collide?
Hint
Nearest by type.
Solution
Lookups are by type, so the tree can only find "the nearest int" — two providers of the same type are ambiguous.
Q44. [Medium] (Coding) Map watch/read to Part 2's two lookups.
Hint
dependOn vs get.
Solution
watch ↔ dependOnInheritedWidgetOfExactType (subscribe); read ↔ getInheritedWidgetOfExactType (no subscription).
Q45. [Medium] (Theory) What does ProxyProvider do?
Hint
Depends on another.
Solution
Creates a provider whose value depends on another provider, recomputing when the dependency changes.
Q46. [Basic] (Theory) What's the standard "source of truth" object in Provider?
Hint
Notifies.
Solution
A ChangeNotifier that calls notifyListeners() on change.
Q47. [Advanced] (Coding) A big ProfilePage watches UserModel just for the name and rebuilds on every change. Fix it.
Hint
select.
Solution
final name = context.select<UserModel, String>((u) => u.name);
Now only name changes trigger a rebuild.
Q48. [Medium] (Theory) Where does Provider's automatic disposal come from?
Hint
create.
Solution
When you use create:, Provider calls dispose() on the created ChangeNotifier when the provider leaves the tree — no manual cleanup.
Q49. [Advanced] (Theory) Summarize Provider's three structural weaknesses.
Hint
Runtime, type, context.
Solution
Runtime dependency errors (ProviderNotFoundException), type collisions (same-type providers), and BuildContext dependence (can't easily read state outside widgets) — all fixed by Riverpod.
Section D — Riverpod (Q50–66)
Q50. [Basic] (Theory) What single change underlies Riverpod's improvements?
Hint
Out of the tree.
Solution
Providers are top-level objects referenced by name (via ref), not widgets looked up by type via context. Compile-safety, no context, and no type collisions all follow.
Q51. [Basic] (Coding) What replaces context for reading state?
Hint
A small object.
Solution
A ref (WidgetRef in widgets, Ref in providers): ref.watch / ref.read / ref.listen.
Q52. [Medium] (Theory) How does Riverpod turn ProviderNotFound into a compile error?
Hint
Reference by name.
Solution
Providers are top-level variables referenced directly; referencing a non-existent one fails to compile, unlike Provider's runtime type lookup.
Q53. [Medium] (Coding) Translate context.watch<Cart>().count and context.read<Cart>().add('x') to Riverpod.
Hint
.notifier for methods.
Solution
ref.watch(cartProvider).count;
ref.read(cartProvider.notifier).add('x');
Q54. [Medium] (Theory) Why can two Provider<int> coexist in Riverpod?
Hint
Identity, not type.
Solution
Riverpod keys providers by identity (the variable), so type is irrelevant — two int providers are distinct.
Q55. [Medium] (Theory) Map ref.watch / ref.read / ref.listen to earlier parts.
Hint
subscribe / once / side-effect.
Solution
watch = subscribe in build (Part 3 watch, Part 2 dependOn); read = one-off in callbacks (Part 3 read); listen = run side-effects on change.
Q56. [Advanced] (Theory) Why is composing logic outside widgets easier in Riverpod?
Hint
No context.
Solution
A provider can ref.watch other providers without BuildContext, so business logic composes and tests outside the widget tree. Provider needs a context.
Q57. [Medium] (Coding) What does AsyncValue<T> give the UI?
Hint
Exhaustive switch.
Solution
A single value with data/loading/error to pattern-match, forcing the UI to handle all three states and removing manual isLoading booleans. See AsyncValue.
Q58. [Medium] (Coding) Which Riverpod feature parameterizes a provider by id?
Hint
family.
Solution
.family — e.g. userProvider(id). See family.
Q59. [Medium] (Theory) What does .autoDispose do?
Hint
Tear down.
Solution
Disposes the provider's state when nothing is listening, with keepAlive to cache when needed. See autoDispose.
Q60. [Basic] (Coding) What wraps the app at the root?
Hint
Scope.
Solution
ProviderScope — runApp(const ProviderScope(child: MyApp())). See ProviderScope.
Q61. [Medium] (Coding) What base class gives a widget a WidgetRef?
Hint
Consumer.
Solution
ConsumerWidget (or ConsumerStatefulWidget), whose build receives (context, ref).
Q62. [Advanced] (Theory) Riverpod removes context for state but still uses an InheritedWidget. Where?
Hint
The container.
Solution
ProviderScope uses an InheritedWidget to hold the provider container and expose WidgetRef. Your providers are top-level objects; the container lives in the scope.
Q63. [Medium] (Theory) What's the @riverpod codegen style, and what powers it?
Hint
build_runner.
Solution
A code-generation API (Riverpod 3.0) that generates providers from annotated functions/classes, powered by build_runner (metaprogramming). The manual API is what it compiles to.
Q64. [Advanced] (Coding) Write a derived provider for a cart total from cartProvider.
Hint
ref.watch another provider.
Solution
final totalProvider = Provider<int>((ref) {
final cart = ref.watch(cartProvider);
return cart.items.fold(0, (s, i) => s + i.price);
});
Q65. [Medium] (Theory) What's Riverpod's equivalent of Provider's select?
Hint
Same name.
Solution
ref.watch(provider.select((s) => s.slice)) — rebuilds only when the selected slice changes.
Q66. [Advanced] (Theory) Give two concrete reasons to choose Riverpod over Provider for a new app.
Hint
Compile-time + context-free.
Solution
Compile-time safety (no runtime ProviderNotFound) and no BuildContext dependence (read via ref, usable/testable outside widgets), plus AsyncValue/family/autoDispose ergonomics.
Section E — Bloc & Cubit (Q67–83)
Q67. [Basic] (Theory) What is unidirectional data flow?
Hint
One direction.
Solution
UI triggers a change → business logic produces a new immutable state → UI rebuilds. The UI never mutates state directly.
Q68. [Basic] (Theory) Core difference between Cubit and Bloc?
Hint
Methods vs events.
Solution
Cubit triggers changes with methods (emit inside them); Bloc uses events (add(event) → on<Event>), giving a traceable event log.
Q69. [Basic] (Coding) Write a CounterCubit with increment.
Hint
super(0), emit.
Solution
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
Q70. [Medium] (Coding) Why is this Cubit wrong?
void add(String t) { state.add(t); emit(state); }
Hint
Mutates in place.
Solution
It mutates the existing list and emits the same instance; equality-based change detection may skip the rebuild and immutability is violated. Use emit([...state, t]).
Q71. [Medium] (Coding) Convert void increment() => emit(state + 1); to a Bloc event + handler.
Hint
on<Increment>.
Solution
class Increment extends CounterEvent {}
// in constructor:
on<Increment>((e, emit) => emit(state + 1));
// UI: context.read<CounterBloc>().add(Increment());
Q72. [Medium] (Theory) BlocBuilder vs BlocListener vs BlocConsumer?
Hint
rebuild / effect / both.
Solution
BlocBuilder rebuilds UI from state; BlocListener runs side-effects (snackbar/nav) once per transition without rebuilding; BlocConsumer does both.
Q73. [Medium] (Coding) Where should a "show snackbar on error" go, and why?
Hint
Once per transition.
Solution
In BlocListener — side-effects must fire once per state change. BlocBuilder can rebuild repeatedly, causing duplicate snackbars.
Q74. [Medium] (Theory) Why model async state as a sealed class?
Hint
Exhaustiveness.
Solution
The compiler forces the UI's switch to handle every state (loading/loaded/error), so no case is forgotten — unlike nullable-field states that can drift into invalid combinations.
Q75. [Medium] (Coding) What's BlocProvider built on?
Hint
Part 2.
Solution
An InheritedWidget under the hood — the same mechanism Provider uses to expose the Bloc/Cubit to the subtree.
Q76. [Medium] (Theory) What do buildWhen / listenWhen do?
Hint
Gate.
Solution
Predicates that gate whether BlocBuilder rebuilds / BlocListener fires for a given transition — Bloc's version of select-style precision.
Q77. [Basic] (Coding) How does the UI trigger a change in a Bloc vs a Cubit?
Hint
add vs method.
Solution
Bloc: context.read<MyBloc>().add(SomeEvent()). Cubit: context.read<MyCubit>().someMethod().
Q78. [Advanced] (Theory) What does full Bloc's event layer buy over Cubit, and the cost?
Hint
Audit trail vs boilerplate.
Solution
A named, ordered, loggable event trail of user actions (great for complex/auditable flows, debugging, replay). The cost is more boilerplate (event classes + handlers) — overkill for simple state.
Q79. [Medium] (Theory) How does Cubit compare to ChangeNotifier?
Hint
Similar but stricter.
Solution
Both hold state and notify. Cubit's state is immutable and replaced via emit (vs mutate-then-notifyListeners), and it plugs into Bloc tooling/observers.
Q80. [Advanced] (Coding) Write an exhaustive switch UI over WeatherState (Initial/Loading/Loaded/Error).
Hint
Sealed switch.
Solution
switch (state) {
WeatherInitial() => const Text('Search'),
WeatherLoading() => const CircularProgressIndicator(),
WeatherLoaded(:final temp) => Text('$temp°'),
WeatherError(:final message) => Text(message),
}
Q81. [Medium] (Theory) When should you start with Cubit and graduate to Bloc?
Hint
Simple → complex.
Solution
Start with Cubit for simple state; move a feature to Bloc when its event history/traceability becomes valuable (complex flows). Mixing both in one app is normal.
Q82. [Advanced] (Theory) How does Bloc's sealed-state approach compare with AsyncValue?
Hint
DIY vs built-in.
Solution
Both enforce exhaustive multi-state handling. AsyncValue is built-in (data/loading/error); Bloc has you define states yourself — more verbose but fully customizable. See AsyncValue.
Q83. [Advanced] (Theory) Why does emitting a new state object matter for rebuild detection?
Hint
Equality.
Solution
Bloc compares old/new states (by identity/equality). Mutating in place keeps the same instance, so the change may be undetected and the rebuild skipped. A new object (or proper ==/Equatable) ensures the transition registers.
Section F — Choosing the right tool (Q84–100)
Q84. [Basic] (Theory) What's the first question to ask when picking a solution?
Hint
Scope.
Solution
Local to one widget, or shared across widgets? Local → setState; shared → a state-management solution.
Q85. [Basic] (Theory) When is setState the right choice?
Hint
Ephemeral local.
Solution
For local, ephemeral UI state owned by one widget (toggles, focus, animations). A framework there is over-engineering.
Q86. [Medium] (Theory) Why is wrapping a checkbox in a Bloc an anti-pattern?
Hint
Ceremony, no payoff.
Solution
The state is trivial and local, so events/states/handlers add ceremony with no benefit. setState is correct and clearer.
Q87. [Medium] (Theory) Why is prop-drilling app-wide auth via setState an anti-pattern?
Hint
Under-engineering.
Solution
It threads shared state through many widgets (boilerplate, coupling, over-rebuilds). Shared state belongs in a state solution read directly via watch/ref.watch.
Q88. [Medium] (Theory) Two reasons to choose Riverpod over Provider for a new app?
Hint
Compile + context.
Solution
Compile-time safety and no BuildContext dependence (plus AsyncValue ergonomics).
Q89. [Medium] (Theory) When does Bloc's structure pay off?
Hint
Big team, complex flow.
Solution
Large apps/teams with complex, auditable flows needing predictability and an event trail.
Q90. [Medium] (Theory) Which tool for injecting a shared ApiClient with no reactivity?
Hint
DI.
Solution
A plain Provider<T> (Provider package) or a Riverpod Provider — dependency injection of a service.
Q91. [Basic] (Theory) Should a real app use only one state tool?
Hint
Mix.
Solution
No — pick a primary shared-state solution for consistency, but use setState freely for local UI. Mixing by scope is healthy.
Q92. [Advanced] (Theory) How does framework choice relate to rebuild performance?
Hint
Where, not how little.
Solution
The framework moves where state lives, not how little you rebuild. You still scope subscriptions (select/buildWhen), use const, and keep build() light (efficiency).
Q93. [Medium] (Coding) Tool for a fetched list with loading/error states in a new app?
Hint
AsyncValue.
Solution
Riverpod with AsyncValue (FutureProvider/AsyncNotifier) for exhaustive loading/data/error handling.
Q94. [Medium] (Theory) Tool for a legacy app already on Provider?
Hint
Don't rewrite.
Solution
Stay on Provider — it's fine to maintain; don't rewrite working code without a real reason.
Q95. [Advanced] (Theory) Read the four tools as a gradient. What's increasing?
Hint
Structure/ceremony.
Solution
From setState → Provider → Riverpod → Bloc, structure, power, and ceremony increase. More structure pays off as apps/teams grow; it's overhead on small projects.
Q96. [Medium] (Theory) Which tools give an exhaustive async-state pattern, and how?
Hint
AsyncValue / sealed.
Solution
Riverpod via built-in AsyncValue; Bloc via self-defined sealed state classes. Both force handling loading/data/error.
Q97. [Advanced] (Theory) Why can a poorly-scoped Riverpod app rebuild more than a well-scoped setState one?
Hint
Discipline.
Solution
If widgets watch whole chunky objects instead of narrow slices, many rebuild on unrelated changes. Performance comes from scoping subscriptions and using const, not from the tool itself.
Q98. [Medium] (Theory) Tool for a multi-step checkout needing an action audit?
Hint
Events.
Solution
Bloc — each step is a named event, yielding an ordered, loggable trail for debugging/replay.
Q99. [Advanced] (Theory) Give a balanced statement on "Riverpod vs Bloc."
Hint
Both good.
Solution
Both are excellent. Riverpod is more concise and compile-safe with less ceremony; Bloc is more prescriptive with one obvious pattern and an event trail. The choice is often team culture, not correctness.
Q100. [Advanced] (Theory) Summarize the whole series' decision philosophy in one sentence.
Hint
Match tool to scope.
Solution
Match the tool to the state's scope and the app's complexity — setState for local UI, a single shared solution (Provider/Riverpod/Bloc) for app-wide state — and stay disciplined about rebuilding only what changed.
Coding Mini-Exercises
Ten larger problems. Try each before opening the solution. Exercise 10 is a capstone.
Exercise 1 — setState counter, done right. Write a StatefulWidget counter with a button, correctly updating the UI, and explain the rebuild.
Show solution
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _n = 0;
@override
Widget build(BuildContext context) => ElevatedButton(
onPressed: () => setState(() => _n++),
child: Text('$_n'),
);
}
setState mutates _n and marks the element dirty; Flutter re-runs build() next frame with the new value (Part 1).
Exercise 2 — Hand-rolled InheritedWidget. Build a CounterScope (StatefulWidget + InheritedWidget) exposing count and increment() to descendants.
Show solution
class CounterScope extends StatefulWidget {
const CounterScope({super.key, required this.child});
final Widget child;
static _Inherited of(BuildContext c) =>
c.dependOnInheritedWidgetOfExactType<_Inherited>()!;
@override
State<CounterScope> createState() => _CounterScopeState();
}
class _CounterScopeState extends State<CounterScope> {
int _n = 0;
@override
Widget build(BuildContext context) => _Inherited(
count: _n,
increment: () => setState(() => _n++),
child: widget.child,
);
}
class _Inherited extends InheritedWidget {
const _Inherited({required this.count, required this.increment, required super.child});
final int count;
final VoidCallback increment;
@override
bool updateShouldNotify(_Inherited old) => count != old.count;
}
Descendants call CounterScope.of(context).count / .increment() — no prop drilling (Part 2).
Exercise 3 — Provider cart. Build a CartModel (ChangeNotifier), provide it, show the count in an app-bar badge that rebuilds only on count change, and add items from a button.
Show solution
class CartModel extends ChangeNotifier {
final _items = <String>[];
int get count => _items.length;
void add(String i) { _items.add(i); notifyListeners(); }
}
ChangeNotifierProvider(create: (_) => CartModel(), child: const Shop());
// badge:
Selector<CartModel, int>(
selector: (_, c) => c.count,
builder: (_, count, __) => Text('Cart ($count)'),
);
// button:
onPressed: () => context.read<CartModel>().add('Apple');
Selector scopes the rebuild to count; read fires the method (Part 3).
Exercise 4 — Riverpod equivalent. Re-implement Exercise 3's cart in Riverpod with a NotifierProvider.
Show solution
class CartNotifier extends Notifier<List<String>> {
@override
List<String> build() => [];
void add(String i) => state = [...state, i];
}
final cartProvider = NotifierProvider<CartNotifier, List<String>>(CartNotifier.new);
// badge (rebuild only on count):
final count = ref.watch(cartProvider.select((c) => c.length));
// button:
onPressed: () => ref.read(cartProvider.notifier).add('Apple');
Compile-safe, no context; select scopes the rebuild (Part 4).
Exercise 5 — Cubit weather. Write a WeatherCubit with sealed states (Initial/Loading/Loaded/Error) and a fetch method, plus the exhaustive BlocBuilder UI.
Show solution
sealed class WState {}
class WInitial extends WState {}
class WLoading extends WState {}
class WLoaded extends WState { WLoaded(this.t); final double t; }
class WError extends WState { WError(this.m); final String m; }
class WeatherCubit extends Cubit<WState> {
WeatherCubit(this._api) : super(WInitial());
final WeatherApi _api;
Future<void> fetch(String city) async {
emit(WLoading());
try { emit(WLoaded(await _api.getTemp(city))); }
catch (e) { emit(WError('$e')); }
}
}
// UI:
BlocBuilder<WeatherCubit, WState>(builder: (_, s) => switch (s) {
WInitial() => const Text('Search'),
WLoading() => const CircularProgressIndicator(),
WLoaded(:final t) => Text('$t°'),
WError(:final m) => Text(m),
});
(Part 5)
Exercise 6 — Cubit to Bloc. Convert Exercise 5's fetch(city) to an event-driven Bloc.
Show solution
sealed class WEvent {}
class FetchWeather extends WEvent { FetchWeather(this.city); final String city; }
class WeatherBloc extends Bloc<WEvent, WState> {
WeatherBloc(this._api) : super(WInitial()) {
on<FetchWeather>((e, emit) async {
emit(WLoading());
try { emit(WLoaded(await _api.getTemp(e.city))); }
catch (err) { emit(WError('$err')); }
});
}
final WeatherApi _api;
}
// UI: context.read<WeatherBloc>().add(FetchWeather('London'));
Now each fetch is a loggable event (Part 5).
Exercise 7 — Fix over-rebuild. A BlocBuilder<CounterBloc, int> rebuilds an expensive widget on every count, but it should only update on even counts. Fix it.
Show solution
BlocBuilder<CounterBloc, int>(
buildWhen: (prev, curr) => curr.isEven,
builder: (_, count) => ExpensiveWidget(count),
);
buildWhen gates the rebuild (Part 5).
Exercise 8 — Side-effect placement. You navigate to a success screen when state becomes OrderPlaced. Show the correct widget and explain why not BlocBuilder.
Show solution
BlocListener<OrderBloc, OrderState>(
listener: (context, state) {
if (state is OrderPlaced) Navigator.of(context).pushNamed('/success');
},
child: const OrderView(),
);
Navigation is a side-effect that must run once per transition; BlocBuilder can rebuild repeatedly and cause duplicate navigations (Part 5).
Exercise 9 — Choose and justify. For an app with: (a) a theme toggle, (b) a per-card expand animation, (c) auth state, (d) a payment flow needing an audit — assign each a tool with a one-line reason.
Show solution
(a) Riverpod/Provider notifier — shared, reactive theme. (b) setState — local UI animation. (c) Riverpod (new app) — shared, compile-safe session. (d) Bloc — event audit trail for the flow. Match tool to scope/complexity (Part 6).
Exercise 10 — Capstone: a mixed-tool todo app. Design (sketch + prose) a small todo app that uses every tool in the series appropriately, and name which concept each piece exercises.
Show solution
// 1. ROOT: Riverpod for shared app state (todos), compile-safe, no context (Part 4).
final todosProvider = NotifierProvider<TodosNotifier, List<Todo>>(TodosNotifier.new);
class TodosNotifier extends Notifier<List<Todo>> {
@override
List<Todo> build() => [];
void add(Todo t) => state = [...state, t];
void toggle(String id) =>
state = [for (final t in state) if (t.id == id) t.copyToggled() else t];
}
// 2. A list tile uses setState for its LOCAL expand animation (Part 1) —
// purely local UI, no business state involved.
class _TileState extends State<TodoTile> {
bool _expanded = false;
@override
Widget build(BuildContext context) => Column(children: [
ListTile(onTap: () => setState(() => _expanded = !_expanded)),
if (_expanded) const TodoDetails(),
]);
}
// 3. The app bar badge watches only the count via select — scoped rebuild
// (Parts 3/4 select; the rebuild discipline of Part 6 / efficiency).
final count = ref.watch(todosProvider.select((list) => list.length));
// 4. A shared ApiClient injected as a plain Provider — DI, no reactivity (Part 6).
final apiProvider = Provider<ApiClient>((ref) => ApiClient());
// 5. For a complex "sync to server" flow with retries you want auditable,
// a Cubit/Bloc with sealed Syncing/Synced/SyncFailed states (Part 5) —
// each step traceable, exhaustive UI switch.
Concept map:
- Riverpod
NotifierProviderfor the shared todo list — Part 4: compile-safe, context-free shared state, built on theInheritedWidgetcontainer of Part 2. setStatefor the local expand animation — Part 1: correct for ephemeral local UI.selectfor the badge — Part 3/Part 4: rebuild only on the slice, the discipline from Part 6.- Plain
Providerfor theApiClient— dependency injection of a service. - Cubit/Bloc for the auditable sync flow — Part 5: sealed states + (optionally) events for traceability.
If you can justify why each tool fits its piece, you've mastered the series' real lesson: match the tool to the state.
You made it
A hundred questions, ten exercises, and a mixed-tool capstone. If you worked them honestly, you can now choose and wield every major Flutter state solution:
- Part 1 — setState: the rebuild model and where it stops.
- Part 2 — InheritedWidget: the foundation everything wraps.
- Part 3 — Provider: watch/read/select, Consumer/Selector.
- Part 4 — Riverpod: compile-safe, context-free providers.
- Part 5 — Bloc & Cubit: event-driven, unidirectional state.
- Part 6 — Choosing: the decision framework.
Pair this with the Flutter Fundamentals and full Riverpod series, and you can architect state for any Flutter app. Now go build one. 🚀