← Back to blog
Flutter State Management · Part 7 of 7
September 19, 202627 min read

100 Questions to Master Flutter State Management

FlutterDartState Management

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

YessetState 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

watchdependOnInheritedWidgetOfExactType (subscribe); readgetInheritedWidgetOfExactType (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

ProviderScoperunApp(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 complexitysetState 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 NotifierProvider for the shared todo list — Part 4: compile-safe, context-free shared state, built on the InheritedWidget container of Part 2.
  • setState for the local expand animation — Part 1: correct for ephemeral local UI.
  • select for the badge — Part 3/Part 4: rebuild only on the slice, the discipline from Part 6.
  • Plain Provider for the ApiClient — 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. 🚀