100 Questions to Master Flutter Internals
This is Part 7 — the finale of the Flutter Internals series. The previous six parts explained the engine; this is where you prove you understand it deeply enough to explain under interview pressure.
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/render code yourself. dartpad.dev runs Flutter snippets.
- After the 100, there are 10 coding mini-exercises with full solutions, ending in a capstone that traces one tap through every layer.
If you can explain the why on all 100 without hints, you understand Flutter internals at a senior level. Let's go.
Section A — The three trees in depth (Q1–17)
Q1. [Basic] (Theory) Name the three trees and each one's job.
Hint
Config / instance / pixels. See Part 1.
Solution
Widget = immutable config (recreated each build); Element = persistent instance (holds state, is the BuildContext, reconciles); RenderObject = layout + paint.
Q2. [Basic] (Theory) Where does a StatefulWidget's state actually live?
Hint
Not the widget.
Solution
In the State held by the StatefulElement (Element tree). That's why state survives widget rebuilds.
Q3. [Basic] (Theory) Why is rebuilding the whole widget tree cheap?
Hint
Elements/render objects persist.
Solution
Widgets are cheap immutable config (often const, canonicalized); the expensive Elements and RenderObjects are kept and updated in place via reconciliation, so only changed properties are touched.
Q4. [Medium] (Theory) State the canUpdate rule.
Hint
Two fields.
Solution
An Element is reused for a new widget only if runtimeType and key both match. Match → update in place (state kept); mismatch → dispose + rebuild (state lost).
Q5. [Medium] (Coding) Reused or replaced? Text('a') → Container().
Hint
runtimeType.
Solution
Replaced — different runtimeType, so canUpdate is false; the old Element is disposed.
Q6. [Medium] (Coding) Reused or replaced? Box(key: ValueKey(1)) → Box(key: ValueKey(2)).
Hint
Same type, diff key.
Solution
Replaced — same type but different key fails canUpdate; state is lost.
Q7. [Medium] (Theory) Walk through updateChild's four cases.
Hint
null/null/canUpdate/else.
Solution
New widget null → deactivate old child. Old child null → inflate/mount new. canUpdate true → reuse (child.update). canUpdate false → replace (deactivate old, inflate new).
Q8. [Basic] (Theory) What are the two main Element flavors?
Hint
Compose vs render.
Solution
ComponentElement (composes other widgets, e.g. StatelessElement/StatefulElement) and RenderObjectElement (creates/manages a RenderObject).
Q9. [Medium] (Theory) What is the BuildOwner and what does it do per frame?
Hint
Dirty list.
Solution
It tracks dirty elements (from markNeedsBuild) and each frame rebuilds them in tree order (buildScope), running build/updateChild to reconcile, after which changed render objects lay out and paint.
Q10. [Medium] (Coding) Trace setState(() => x++) to a repaint.
Hint
markNeedsBuild → BuildOwner → canUpdate → render.
Solution
Mutate fields → markNeedsBuild() on the Element → BuildOwner dirty list → next frame build() + updateChild/canUpdate reconcile → changed RenderObjects markNeedsPaint/Layout → pipeline paints.
Q11. [Advanced] (Theory) Why doesn't changing a Text's string create a new RenderObject?
Hint
Reused element → update.
Solution
canUpdate matches (same type+key), so the Element is reused and updateRenderObject mutates the existing render object's text + marks needs-paint. New render objects only appear on element replacement.
Q12. [Basic] (Theory) Which tree is the BuildContext?
Hint
Middle.
Solution
The Element — Element implements BuildContext.
Q13. [Medium] (Theory) Why separate Widget from Element at all?
Hint
Disposable vs persistent.
Solution
So widgets can be recreated cheaply every build while a persistent Element holds state and decides what actually changed — keeping expensive render objects alive across rebuilds.
Q14. [Advanced] (Coding) A const child in a rebuilt parent — what happens to it?
Hint
Identical → skip.
Solution
A const widget is canonicalized to one instance; on rebuild the new child is identical() to the old, so canUpdate reuses the Element and Flutter can skip that subtree entirely. (const)
Q15. [Advanced] (Theory) How does markNeedsBuild connect setState to the framework?
Hint
State Mgmt Part 1.
Solution
setState calls markNeedsBuild() on the State's Element, enrolling it in the BuildOwner's dirty list so it rebuilds next frame. It's the bridge from a field change to reconciliation. (State Mgmt Part 1)
Q16. [Medium] (Coding) Same type, same key, new data: Text('a', key: k) → Text('b', key: k). Outcome?
Hint
canUpdate true.
Solution
Reused — same type+key, so the Element/render object are kept and just updated to show 'b'.
Q17. [Advanced] (Theory) Why is reconciliation O(n) over the children, not O(n²)?
Hint
Position-paired (+keys).
Solution
Children are matched by position (with keys helping match by identity within a list), so each old/new child pair is compared once via canUpdate — a single linear walk, not all-pairs matching.
Section B — Keys (Q18–34)
Q18. [Basic] (Theory) What does a key control?
Hint
The key half of canUpdate.
Solution
The key in canUpdate — it makes reconciliation match an Element by identity instead of position, deciding which Element/state is reused.
Q19. [Basic] (Theory) Do most widgets need keys?
Hint
No.
Solution
No — only same-type, stateful widgets in a collection that can reorder or change membership generally need them.
Q20. [Medium] (Theory) Explain the "reorder resets state" bug.
Hint
Matched by slot.
Solution
Without keys, widgets match existing Elements by position, so State stays glued to the slot. After a reorder/removal the wrong rows keep state. Keys make matching follow item identity.
Q21. [Medium] (Coding) Choose a key: items have a unique id int.
Hint
Value.
Solution
ValueKey(item.id).
Q22. [Medium] (Coding) Choose a key: items have no id but stable object identity.
Hint
Object.
Solution
ObjectKey(item).
Q23. [Medium] (Coding) You want a widget to fully reset every build. Which key, and the risk?
Hint
Unique.
Solution
UniqueKey() — a new identity each build forces a fresh Element. Risk: in a list it destroys reuse and resets all state every frame.
Q24. [Medium] (Theory) LocalKey vs GlobalKey scope?
Hint
Siblings vs app.
Solution
LocalKey is unique among siblings; GlobalKey is unique across the whole app (and grants state/context access + re-parenting).
Q25. [Medium] (Coding) How do you call validate() on a Form from elsewhere?
Hint
GlobalKey currentState.
Solution
final k = GlobalKey<FormState>();
Form(key: k, ...);
k.currentState!.validate();
Q26. [Advanced] (Theory) How does a GlobalKey move a widget across the tree without losing state?
Hint
Re-parent the element.
Solution
It identifies an Element globally, so when the keyed widget reappears elsewhere, Flutter re-parents the existing Element (with its State/RenderObject) instead of rebuilding — preserving state.
Q27. [Advanced] (Theory) Why is GlobalKey more expensive than LocalKey?
Hint
Global registry.
Solution
The framework maintains a global registry mapping each GlobalKey to its Element (for lookups/re-parenting), and it must stay unique app-wide — more bookkeeping than a sibling-scoped LocalKey.
Q28. [Medium] (Coding) Where must a list-item key be placed?
Hint
Sibling level.
Solution
On the item widget that's a direct child of the collection's children (the sibling level the list reconciles) — not buried deeper inside the item.
Q29. [Medium] (Theory) When do you NOT need keys?
Hint
Stable / stateless / distinct types.
Solution
When the structure is stable and never reorders, the widgets are stateless, or siblings are different types (so runtimeType already distinguishes them).
Q30. [Advanced] (Coding) Two TextFields swap positions and the text seems stuck. Fix and explain.
Hint
Keys make them follow.
Solution
Add distinct keys (ValueKey('a'), ValueKey('b')). Without them both are TextField at positions 0/1, so swapping reuses Elements by slot. Keys make Flutter match by identity so the fields truly swap.
Q31. [Basic] (Theory) Name the three LocalKey subtypes.
Hint
Value/Object/Unique.
Solution
ValueKey, ObjectKey, UniqueKey.
Q32. [Advanced] (Theory) Why does UniqueKey() in a ListView.builder hurt performance?
Hint
New identity each build.
Solution
Every build yields new keys → canUpdate always fails → every item's Element/state rebuilt from scratch each frame, destroying reuse and resetting state.
Q33. [Medium] (Coding) Will adding ValueKeys to a never-reordered stateless list change behavior?
Hint
Mostly harmless.
Solution
Functionally it's harmless (and can aid diffing), but it's unnecessary — with stable order and no state there's nothing for keys to fix.
Q34. [Advanced] (Theory) What happens if the same GlobalKey is used in two places in the tree simultaneously?
Hint
Must be unique.
Solution
It throws — a GlobalKey must identify one Element at a time. Duplicate simultaneous use violates uniqueness and crashes.
Section C — BuildContext (Q35–50)
Q35. [Basic] (Theory) What is a BuildContext concretely?
Hint
The element.
Solution
The Element for a widget — a handle to its location in the Element tree.
Q36. [Basic] (Theory) Which direction does of(context) search?
Hint
Up.
Solution
Upward from context to the nearest matching ancestor (usually an InheritedWidget).
Q37. [Medium] (Coding) Fix "Scaffold.of() ... does not contain a Scaffold" inside a page returning a Scaffold.
Hint
Builder.
Solution
Wrap the child in a Builder so its context is below the Scaffold:
Scaffold(body: Builder(builder: (context) =>
...ScaffoldMessenger.of(context)...));
Q38. [Medium] (Theory) Why does the page's own context fail to find the Scaffold it returns?
Hint
Above it.
Solution
The page's context sits above the Scaffold, and of searches upward — it can't find a Scaffold that's below the starting context.
Q39. [Medium] (Coding) Guard a context used after await.
Hint
mounted.
Solution
await something();
if (!context.mounted) return;
Navigator.of(context).pop();
Q40. [Medium] (Theory) What does context.mounted tell you?
Hint
Still in tree?
Solution
Whether the Element backing the context is still mounted in the tree. Check it after async gaps before using the context.
Q41. [Advanced] (Theory) Why is reading inherited values in initState problematic?
Hint
Doesn't react.
Solution
The dependency isn't set up to react to later changes and some lookups aren't valid that early; the value can go stale. Use didChangeDependencies or build.
Q42. [Basic] (Theory) What widget introduces a context one level deeper?
Hint
Builder.
Solution
Builder — its builder callback receives a context below the Builder.
Q43. [Medium] (Theory) Why can two contexts on the same screen return different Themes?
Hint
Different start points.
Solution
Theme.of searches upward from each context; if they start at different locations (one below an inner Theme override), their nearest Theme ancestor differs.
Q44. [Advanced] (Theory) Why is storing a context for later dangerous?
Hint
May deactivate.
Solution
A context is valid only while its Element is mounted/in place; after rebuilds/navigation/disposal it can be deactivated, so using a stored one throws "deactivated widget's ancestor." Capture concrete values synchronously instead.
Q45. [Medium] (Coding) State.mounted vs context.mounted — which to use where?
Hint
State vs bare context.
Solution
In a State, use mounted (State.mounted). With a bare BuildContext (stateless callback), use context.mounted. Both mean "element still in the tree."
Q46. [Medium] (Theory) What two things can a context do that justify passing it everywhere?
Hint
Lookups + anchor.
Solution
Find ancestors (of(context), inherited dependencies) and act as the anchor for Navigator/Theme/MediaQuery/overlays — all relative to its tree location.
Q47. [Advanced] (Theory) Why does dependOnInheritedWidgetOfExactType need a context, mechanically?
Hint
Start node + register.
Solution
The context (Element) is the starting node for the upward type lookup and the node registered as a dependent, so the framework knows which element to rebuild when the inherited widget changes. (State Mgmt Part 2)
Q48. [Basic] (Coding) Which lint flags using a context after an await?
Hint
use_build_context...
Solution
use_build_context_synchronously.
Q49. [Medium] (Theory) Can a context see its descendants?
Hint
No (directly).
Solution
Not directly via of-style lookups — those go upward. (You can reach a render object via findRenderObject, but normal inherited lookups only see ancestors.)
Q50. [Advanced] (Theory) Summarize the one rule that explains all context bugs.
Hint
Location + upward.
Solution
A context is a location in the tree and lookups search upward from it; using a context from the wrong location (too high, deactivated, or another subtree) makes lookups fail or find the wrong ancestor.
Section D — Widget lifecycle (Q51–67)
Q51. [Basic] (Theory) Which hooks fire exactly once?
Hint
Start/end.
Solution
initState (start) and dispose (end). didChangeDependencies is once-then-on-change; build/didUpdateWidget fire many times.
Q52. [Basic] (Coding) Put in order: build, initState, createState, didChangeDependencies.
Hint
Birth → prep → deps → perform.
Solution
createState → initState → didChangeDependencies → build.
Q53. [Medium] (Theory) What belongs in initState?
Hint
One-time setup.
Solution
One-time setup — create controllers/subscriptions, initial values. Call super.initState() first; don't do change-tracking inherited lookups or setState here.
Q54. [Medium] (Theory) didChangeDependencies vs didUpdateWidget — when does each fire?
Hint
Inherited vs own props.
Solution
didChangeDependencies when an inherited dependency (via of(context)) changes; didUpdateWidget(old) when the parent rebuilds with new widget config (same State reused).
Q55. [Medium] (Coding) A subscription set up in initState from widget.stream goes stale when the parent passes a new stream. Fix.
Hint
didUpdateWidget.
Solution
@override
void didUpdateWidget(W old) {
super.didUpdateWidget(old);
if (old.stream != widget.stream) { _sub.cancel(); _sub = widget.stream.listen(_on); }
}
Q56. [Medium] (Theory) Why doesn't initState re-run when a parent passes new props?
Hint
Same State reused.
Solution
canUpdate keeps the same State and just hands it a new widget — the Element isn't recreated, so initState doesn't run again. Reconcile prop-dependent setup in didUpdateWidget.
Q57. [Basic] (Coding) What's the super call order for initState and dispose?
Hint
First / last.
Solution
super.initState() first, super.dispose() last.
Q58. [Medium] (Theory) What must dispose do?
Hint
Release resources.
Solution
Release everything acquired in setup — dispose() controllers, cancel() subscriptions/timers, remove listeners — to avoid leaks. super.dispose() last.
Q59. [Advanced] (Theory) Why does forgetting dispose cause a memory leak?
Hint
Reachable State.
Solution
Undisposed controllers/subscriptions keep the State (and its subtree) reachable, so the GC can't reclaim it — a retained-reference leak. (GC)
Q60. [Medium] (Coding) Where to build a DateFormat derived from inherited Locale so it updates?
Hint
didChangeDependencies.
Solution
In didChangeDependencies — it re-runs when the inherited Locale changes.
Q61. [Medium] (Theory) What is deactivate and how does it differ from dispose?
Hint
Might return.
Solution
deactivate = removed from the tree but might be reinserted (e.g. GlobalKey re-parent). dispose = gone for good; release resources. deactivate is rarely overridden.
Q62. [Basic] (Theory) Does StatelessWidget have lifecycle hooks?
Hint
Just build.
Solution
No — only build. The lifecycle hooks belong to State.
Q63. [Medium] (Theory) After dispose, what is mounted and why does it matter?
Hint
false → guard async.
Solution
mounted is false. Async callbacks must check it before setState/using context, or they throw "setState after dispose."
Q64. [Advanced] (Coding) A TickerCard subscribes per widget.symbol. Write the hooks that keep it correct and leak-free.
Hint
initState + didUpdateWidget + dispose.
Solution
void initState() { super.initState(); _sub = stream(widget.symbol).listen(_on); }
void didUpdateWidget(o) { super.didUpdateWidget(o);
if (o.symbol != widget.symbol) { _sub.cancel(); _sub = stream(widget.symbol).listen(_on); } }
void dispose() { _sub.cancel(); super.dispose(); }
Q65. [Medium] (Theory) Which hook fires on hot reload only?
Hint
reassemble.
Solution
reassemble() — dev-only (hot reload); never in production.
Q66. [Advanced] (Theory) Why is build required to be side-effect-free?
Hint
Runs often, unpredictably.
Solution
It can run many times per second (rebuilds, dependency changes) and must be safe to re-run. Side-effects (starting requests, mutating external state, creating controllers) there cause duplication, leaks, and bugs — those belong in initState/didUpdateWidget/callbacks.
Q67. [Advanced] (Theory) Tell the difference between "inherited changed" and "props changed" and the hook for each.
Hint
deps vs widget.
Solution
Inherited value (read via of(context)) changed → didChangeDependencies. Your own widget.* props changed (parent rebuild) → didUpdateWidget. They're commonly confused; the source of the change tells you the hook.
Section E — Slivers & CustomScrollView (Q68–83)
Q68. [Basic] (Theory) What is a sliver?
Hint
Scrollable segment.
Solution
A scrollable segment of a viewport laid out with a scroll-aware protocol — a piece of the "belt" that a CustomScrollView arranges with others. (Part 5)
Q69. [Basic] (Theory) How does CustomScrollView differ from ListView?
Hint
Many slivers.
Solution
ListView is a viewport with one sliver; CustomScrollView places multiple slivers in one viewport so they scroll together.
Q70. [Medium] (Theory) How does sliver layout differ from box layout?
Hint
SliverConstraints → SliverGeometry.
Solution
Boxes: BoxConstraints → Size (2D). Slivers: SliverConstraints → SliverGeometry (1D, scroll-aware — scroll extent, paintExtent/visible amount, where the next sliver starts).
Q71. [Medium] (Coding) Put a single Card banner between two slivers. Which widget?
Hint
Adapter.
Solution
SliverToBoxAdapter(child: Card(...)).
Q72. [Medium] (Coding) Keep a SliverList lazy for 100k rows.
Hint
Builder delegate.
Solution
SliverList(delegate: SliverChildBuilderDelegate(
(c, i) => Tile(data[i]), childCount: data.length));
Q73. [Medium] (Theory) Why do sliver lists scale to huge datasets?
Hint
Lazy build.
Solution
SliverChildBuilderDelegate builds children lazily (only visible ones + a small cache) and disposes off-screen ones, keeping live Elements/RenderObjects roughly constant.
Q74. [Advanced] (Theory) Explain a collapsing SliverAppBar in protocol terms.
Hint
paintExtent shrinks.
Solution
As scrollOffset grows, its paintExtent (visible size) shrinks from expandedHeight toward the toolbar height. pinned clamps paintExtent at the toolbar height; floating lets it grow again on upward scroll.
Q75. [Medium] (Coding) Add padding around a sliver. Which widget, and why not Padding?
Hint
SliverPadding.
Solution
SliverPadding — Padding is a box widget and can't sit in slivers: or wrap a sliver.
Q76. [Basic] (Theory) What does pinned: true do on a SliverAppBar?
Hint
Stays at top.
Solution
Keeps the toolbar visible (pinned) at the top when scrolled, instead of scrolling fully away.
Q77. [Medium] (Theory) Why is nesting scrollables to combine regions an anti-pattern?
Hint
Unbounded + uncoordinated.
Solution
It causes unbounded-height errors, conflicting physics, and double scrollbars, and the regions don't coordinate. Use one CustomScrollView with multiple slivers instead.
Q78. [Medium] (Coding) Fill leftover viewport space with a footer. Which sliver?
Hint
FillRemaining.
Solution
SliverFillRemaining(child: Footer()).
Q79. [Advanced] (Theory) What does SliverGeometry.paintExtent represent vs scrollExtent?
Hint
Visible vs total.
Solution
scrollExtent = the sliver's total length along the scroll axis; paintExtent = how much of it is currently visible in the viewport.
Q80. [Medium] (Theory) Name three common slivers and their roles.
Hint
AppBar/List/Grid.
Solution
SliverAppBar (collapsing header), SliverList (lazy list), SliverGrid (lazy grid). Also SliverToBoxAdapter, SliverPadding, SliverFillRemaining, SliverPersistentHeader.
Q81. [Advanced] (Coding) A SliverList with children: [...] is janky on a big list. Why and fix?
Hint
Fixed list builds all.
Solution
A fixed children: list builds everything up front (no laziness). Switch to a SliverChildBuilderDelegate so only visible rows build.
Q82. [Medium] (Theory) What gives you full control over a pinning/floating custom header?
Hint
Persistent header.
Solution
SliverPersistentHeader (with a SliverPersistentHeaderDelegate) — custom header that can pin/float with explicit min/max extents.
Q83. [Advanced] (Theory) Why can a collapsing header + list + grid scroll as one with slivers but not with nested ListViews?
Hint
One viewport.
Solution
Slivers share one viewport (one scroll position/belt), so their geometry coordinates. Nested ListViews are separate viewports with independent scroll, so they can't collapse/scroll together.
Section F — RenderObject (Q84–100)
Q84. [Basic] (Theory) What three jobs does a RenderObject do?
Hint
Layout/paint/hit-test.
Solution
Layout (size + place children), paint (draw), hit testing (point-inside). RenderBox is the common 2D base. (Part 6)
Q85. [Basic] (Theory) State the layout protocol.
Hint
Down/up/parent.
Solution
Constraints go down, sizes go up, the parent sets position. Parent passes BoxConstraints; child picks a Size; parent sets the child's offset.
Q86. [Medium] (Theory) Who chooses size and who chooses position?
Hint
Child vs parent.
Solution
The child chooses its size (within constraints); the parent chooses the child's position.
Q87. [Medium] (Theory) markNeedsLayout vs markNeedsPaint — when each?
Hint
Size vs appearance.
Solution
markNeedsLayout when size/position changes; markNeedsPaint when only appearance changes (e.g. color). Repaint-only is cheaper.
Q88. [Medium] (Coding) In a custom RenderBox, a color setter should call which invalidation?
Hint
Paint only.
Solution
markNeedsPaint() — color doesn't affect size/position, so no relayout.
Q89. [Medium] (Theory) What methods does a RenderObjectWidget implement, and when are they called?
Hint
create / update.
Solution
createRenderObject (once, on mount) and updateRenderObject (on rebuild when the Element is reused) — the latter mutates the render object in place.
Q90. [Advanced] (Theory) How does updateRenderObject explain cheap rebuilds?
Hint
Mutate in place.
Solution
When canUpdate reuses the Element, updateRenderObject updates the existing render object with new values instead of allocating a new one — so a full build() translates to minimal real work.
Q91. [Basic] (Theory) Name the three RenderObjectWidget base classes by child count.
Hint
Leaf/Single/Multi.
Solution
LeafRenderObjectWidget (0), SingleChildRenderObjectWidget (1), MultiChildRenderObjectWidget (many).
Q92. [Medium] (Theory) What is parentData used for?
Hint
Position/extra info.
Solution
It stores per-child data the parent owns — most commonly the child's offset (position), and layout extras (e.g. flex). The parent reads/writes it during layout/paint.
Q93. [Medium] (Coding) When should you use CustomPaint instead of a custom RenderBox?
Hint
Drawing, not layout.
Solution
When you need custom drawing with no special layout — a CustomPainter is simpler and sufficient (charts, gauges, signatures).
Q94. [Medium] (Theory) Why does child.layout(..., parentUsesSize: true) matter?
Hint
Relayout propagation.
Solution
It declares that the parent's layout depends on the child's size, so when the child relays out the parent relays out too. Omitting it (when you don't read child size) limits relayout propagation — an optimization.
Q95. [Advanced] (Theory) Why are layout and paint separate phases?
Hint
Independent invalidation.
Solution
Many changes affect only appearance, not geometry. Separate phases let Flutter repaint without relaying out (and vice versa), invalidating each independently to avoid unnecessary work.
Q96. [Medium] (Theory) What does RepaintBoundary do?
Hint
Own layer.
Solution
Isolates a subtree onto its own paint layer, so its repaints don't force neighbors to repaint (and vice versa).
Q97. [Advanced] (Coding) A small spinner forces a heavy static chart to repaint each frame. Fix.
Hint
Wrap it.
Solution
Wrap the spinner in RepaintBoundary so its repaints stay on its own layer and don't dirty the chart's layer.
Q98. [Advanced] (Theory) Why do "unbounded constraints" errors happen, in protocol terms?
Hint
Infinite maxHeight.
Solution
A parent (e.g. Column) passes unbounded (infinity) max constraints down to a child that wants to fill available space (e.g. ListView). The child can't pick a finite size, so layout fails. Give it bounded constraints (Expanded, SizedBox, etc.). (constraints)
Q99. [Medium] (Theory) How does hit testing work at the render layer?
Hint
Walk + point-inside.
Solution
Flutter walks the render tree asking each object (via hitTest) whether the pointer position falls inside it (typically against its size), building the list of objects under the pointer that then receive the event.
Q100. [Advanced] (Theory) Tie the whole series together: trace a Text color change from widget to pixel.
Hint
Every layer.
Solution
setState mutates a field and markNeedsBuilds the Element (Part 1) → BuildOwner rebuilds it → build returns a new Text → canUpdate matches (type+key) so the Element is reused → updateRenderObject sets the new color on the existing RenderParagraph and calls markNeedsPaint (no relayout, since size is unchanged) → the paint phase redraws just that render object's layer. Widget → Element → RenderObject, mutated in place.
Coding Mini-Exercises
Ten larger problems. Try each before opening the solution. Exercise 10 is a capstone.
Exercise 1 — Reorder bug. Show the minimal change that fixes state sticking to the wrong row when a stateful list reorders.
Show solution
Add an identity key to each item widget:
for (final item in items) ItemTile(key: ValueKey(item.id), item: item)
Now reconciliation matches by key, so state follows the item, not the slot (Part 2).
Exercise 2 — canUpdate table. For transitions (a) Text('a')→Text('b'), (b) Text('a')→SizedBox(), (c) Box(key: k1)→Box(key: k2), (d) Box(key: k1)→Box(key: k1), state reuse/replace and whether state survives.
Show solution
(a) reuse, state survives. (b) replace (type), state lost. (c) replace (key differs), state lost. (d) reuse, state survives. Driven by runtimeType + key (Part 1).
Exercise 3 — Builder fix. Rewrite so ScaffoldMessenger.of(context) works inside a page that returns a Scaffold.
Show solution
Scaffold(
body: Builder(builder: (context) => ElevatedButton(
onPressed: () => ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Hi'))),
child: const Text('Show'),
)),
);
The Builder provides a context below the Scaffold (Part 3).
Exercise 4 — Lifecycle correctness. Write initState/didUpdateWidget/dispose for a widget that listens to widget.channel and must follow channel changes leak-free.
Show solution
void initState() { super.initState(); _sub = widget.channel.listen(_on); }
void didUpdateWidget(W o) { super.didUpdateWidget(o);
if (o.channel != widget.channel) { _sub.cancel(); _sub = widget.channel.listen(_on); } }
void dispose() { _sub.cancel(); super.dispose(); }
initState sets up, didUpdateWidget reconciles the changed prop (same State reused), dispose prevents the leak (Part 4).
Exercise 5 — Async guard. Add the correct guard around a post-await navigation in a State.
Show solution
await api.submit();
if (!mounted) return; // State.mounted
Navigator.of(context).pop();
After await the State may be disposed; mounted prevents "deactivated widget" errors (Part 3/Part 4).
Exercise 6 — Coordinated scroll. Build a CustomScrollView with a pinned collapsing header, a lazy list of 1000 items, and a footer filling the rest.
Show solution
CustomScrollView(slivers: [
const SliverAppBar(expandedHeight: 180, pinned: true,
flexibleSpace: FlexibleSpaceBar(title: Text('Feed'))),
SliverList(delegate: SliverChildBuilderDelegate(
(c, i) => ListTile(title: Text('Item $i')), childCount: 1000)),
const SliverFillRemaining(hasScrollBody: false, child: Footer()),
]);
One viewport, lazy list, all scrolling together (Part 5).
Exercise 7 — Custom RenderBox. Write a RenderBox that is a fixed 100×100 square of a settable color, repainting (not relaying out) on color change.
Show solution
class RenderSquare extends RenderBox {
RenderSquare(this._c);
Color _c;
set color(Color v) { if (v == _c) return; _c = v; markNeedsPaint(); }
@override
void performLayout() => size = constraints.constrain(const Size(100, 100));
@override
void paint(PaintingContext context, Offset offset) =>
context.canvas.drawRect(offset & size, Paint()..color = _c);
}
Color setter calls markNeedsPaint only — no relayout (Part 6).
Exercise 8 — Wrap it as a widget. Wire the RenderSquare from Exercise 7 into a LeafRenderObjectWidget.
Show solution
class Square extends LeafRenderObjectWidget {
const Square(this.color, {super.key});
final Color color;
@override
RenderObject createRenderObject(BuildContext c) => RenderSquare(color);
@override
void updateRenderObject(BuildContext c, RenderSquare ro) => ro.color = color;
}
createRenderObject once; updateRenderObject mutates in place on reuse (Part 6).
Exercise 9 — Contain repaints. A CustomScrollView has an animated banner among static cards that repaint every frame in profiling. Apply the fix.
Show solution
Wrap the animating banner (and/or the expensive static cards) in RepaintBoundary so the animation's repaints stay on its own layer and don't dirty the cards (Part 6).
Exercise 10 — Capstone: trace a tap, end to end. A user taps a "like" button in a reorderable feed list, incrementing a count shown on that row. Trace the interaction through every layer of the series, naming the mechanism at each step.
Show solution
- Hit testing (Part 6): the tap location walks the render tree; the like button's render object is found under the pointer and the gesture is dispatched.
- Callback → setState (Part 4/State Mgmt 1): the row's
Stateincrements its count insidesetState, which callsmarkNeedsBuildon its Element. - BuildOwner (Part 1): the dirty Element is rebuilt next frame in tree order.
- Reconciliation /
canUpdate(Part 1): the new widget subtree matches the old byruntimeType+key, so Elements are reused, not rebuilt. Because the list has keys (Part 2), the correct row's Element is matched by identity even though the list can reorder. updateRenderObject(Part 6): the countText's render object has its text updated in place and callsmarkNeedsPaint(size unchanged → no relayout, just repaint).BuildContext(Part 3): anyTheme.of(context)the row uses resolves upward to style the new text.- Layout (if size changed) (Part 6): if the new number is wider, the protocol runs — constraints down, sizes up, parent positions — but for a same-width change only paint runs.
- Slivers (Part 5): because the feed is a lazy
SliverList, only on-screen rows ever existed as Elements/RenderObjects, so this whole process touched a screen's worth of objects, not the whole list. - Lifecycle/leak safety (Part 4): if the like triggered async work, the callback guards with
mountedbefore any furthersetState.
One tap, every layer: hit test → callback → markNeedsBuild → BuildOwner → canUpdate (+keys) → updateRenderObject → markNeedsPaint → composited frame. If you can narrate this, you understand Flutter internals.
You made it
A hundred questions, ten exercises, and an end-to-end capstone. If you worked them honestly, you can now explain Flutter's engine the way interviewers hope candidates can:
- Part 1 — Three trees: Widgets, Elements,
canUpdate, BuildOwner. - Part 2 — Keys: controlling reconciliation identity.
- Part 3 — BuildContext: it's the Element; lookups go up.
- Part 4 — Lifecycle: initState → didUpdateWidget → dispose.
- Part 5 — Slivers: the viewport/sliver scrolling engine.
- Part 6 — RenderObject: layout, paint, and below.
Pair this with Flutter Fundamentals, State Management, and the Dart Internals series, and you understand Flutter top to bottom. Now go read the framework source — it'll finally make sense. 🚀