Skip to content

How Flutter Works Internally

Every earlier module in this course taught you to use Flutter: build widgets, manage state, navigate, lay out screens, ship a build. None of it required knowing what setState actually does underneath, what a “relayout boundary” is, why Impeller’s PSOs are “precompiled,” or what gen_snapshot actually produces. That gap is exactly where a confident-sounding explanation of Flutter (human- or AI-written) goes wrong in ways flutter analyze alone won’t catch — a plausible-sounding claim about “the UI and raster threads merging” or “each isolate having its own totally separate heap” reads as authoritative right up until you check the actual source or run the actual command.

This module answers five internals questions, each proven against the real, installed Flutter 3.47.5 / Dart 3.13.4 source (revision 6a19cca564) or a real command run on this Mac, never asserted from memory:

  1. What actually happens between a widget rebuilding and a pixel changing? (The Three Trees, From the Source — Widget.canUpdate, Element.updateChild’s real four-case table, a custom RenderObjectWidget proving createRenderObject/updateRenderObject/performLayout/paint are four separately-triggerable hooks, and GlobalKey reparenting proven with identical())
  2. What turns “a widget called setState” into “a frame actually runs,” and in what order? (Build Scheduling and Frames — the real SchedulerPhase enum, WidgetsBinding.drawFrame’s own 8-step doc comment, and a real test proving two synchronous setState calls coalesce into exactly one build)
  3. Where, precisely, does a chain of markNeedsLayout calls stop climbing the tree? (Layout and Paint Internals — the exact relayout-boundary rule quoted from source, proven with a custom RenderBox pair and a real debugDumpRenderTree showing the boundary annotation appear and disappear)
  4. What does Impeller actually replace, and is “UI+raster merged” even true? (Impeller and the GPU — the engine’s own README and Entity/Contents/RenderPass source, and a real correction: the documented 3.29+ thread merge is UI+platform, not UI+raster)
  5. What does flutter build actually run, and what does a Dart isolate’s memory model really look like? (The Dart VM and Isolates — real, verbose flutter build apk/flutter build web output, a real ELF libapp.so extracted and inspected with file(1), and a live, grepped proof that a kDebugMode branch is deleted from a release build)

Every source-level claim below cites a real permalink into github.com/flutter/flutter at this course’s pinned revision 6a19cca564 (Flutter 3.47.5), or github.com/dart-lang/sdk at the matching pinned tag 3.13.4 — the same revision flutter --version reports on this Mac, so every quoted line number matches what’s linked, even after this SDK is eventually upgraded. Every runtime claim is a real flutter test run against this course’s shared probe (real, trimmed output, never fabricated), and every build-pipeline claim (Impeller and the GPU’s CLI --help text, The Dart VM and Isolates’s flutter build apk/flutter build web -v output) is a real command run on this Mac, either through the probe or a throwaway scratch flutter create app for anything the shared probe can’t safely run (a full APK/web build). Where this environment has no device and a claim genuinely can’t be verified locally (DevTools raster-stats screenshots, a real production Impeller frame trace), each lesson says so directly instead of guessing.

flowchart TD
  t["The Three Trees, From the Source\n(Widget/Element/RenderObject)"] --> f["Build Scheduling and Frames\n(setState -> a real frame)"]
  t --> l["Layout and Paint Internals\n(the relayout-boundary rule)"]
  f --> l
  l --> i["Impeller and the GPU\n(what actually runs on the raster thread)"]
  f --> v["The Dart VM and Isolates\n(the real build pipeline)"]
  i --> v
The five internals lessons, in the order they build on each other

Reading order and what each lesson assumes

Section titled “Reading order and what each lesson assumes”
LessonBuilds on (Phase 3)Answers
The Three Trees, From the SourceWidgets and the TreeWhy does same-type-same-key reuse an Element, and what does Widget.canUpdate actually compare?
Build Scheduling and FramesThe Render Pipeline, Async in DartWhat is the real chain from one setState() call to one scheduled frame, and why do redundant setState calls coalesce?
Layout and Paint InternalsConstraints & Layout, The Render PipelineWhere, exactly, does a markNeedsLayout() call stop propagating up the tree?
Impeller and the GPUThe Render PipelineWhat is Impeller’s real architecture, and are the UI and raster threads actually merged?
The Dart VM and IsolatesIsolates & compute, Build & ReleaseWhat does flutter build actually invoke, and what does an isolate’s memory model really look like?

Each Phase 3 lesson linked above already showed you the outside view (a widget test counting rebuilds, a real build’s reported size, a real error message). This module shows the mechanism behind that view — it does not re-teach the outside view, it links to it and goes one level deeper.

Every lesson in this module is the same idea applied to a different layer: Flutter’s internals are not a black box — they are a fixed set of source files and real, runnable commands that will always show you the actual mechanism directly. “Rebuilding is expensive” is not a slogan; it’s Widget.canUpdate’s exact two-field test, provable with a spy. “A relayout boundary stops the climb” is not a metaphor; it’s one literal line, _isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent == null, provable with a counter and a real debugDumpRenderTree. “Impeller removed shader jank” is not marketing; it’s ContentContextOptions’ own doc comment bounding PSO variants at “hundreds, not 10,000s.” Once you know which source file or which command proves which claim, “trust me, that’s how Flutter works under the hood” stops being necessary — for your own claims, and for judging an AI’s.

  • “A widget rebuilding always means a new RenderObject.” The Three Trees proves the opposite with a real counter: createRenderObject runs once; same-key rebuilds only ever pay for updateRenderObject.
  • “Calling setState twice always triggers two rebuilds.” Build Scheduling and Frames proves Element.markNeedsBuild’s own if (dirty) return; guard coalesces two synchronous calls into exactly one.
  • “Marking a widget as needing layout always relays out everything above it.” Layout and Paint Internals proves a relayout boundary genuinely absorbs the dirty flag — a real debugDumpRenderTree confirms it from a second, independent angle.
  • “Impeller merged the UI thread and the raster thread.” Impeller and the GPU proves, with real docs.flutter.dev quotes, that the actual 3.29+ merge is UI+platform on iOS/Android — the raster thread, where Impeller/Skia actually talk to the GPU, was never merged with the UI thread.
  • “Each Dart isolate has its own completely separate heap.” The Dart VM and Isolates proves, from dart-lang/sdk’s own glossary, that isolates in the same group share a heap — and then proves, fresh, that compute()’s message passing still copies rather than aliases regardless.

A preview of the pattern every lesson in this module proves at least once: a plausible-sounding claim about Flutter internals that a real source citation or a real test disproves.

🐛 Spot the bug

// A comment an AI assistant added above a custom RenderBox
// "sizedByParent means the framework never needs to re-check this
// widget's layout when its internal state changes -- just call
// markNeedsLayout() like any other render object."
class _RenderFixedHeightBox extends RenderBox {
@override
bool get sizedByParent => true;
double _height = 40;
set targetHeight(double v) {
_height = v;
markNeedsLayout();
}
@override
Size computeDryLayout(BoxConstraints constraints) =>
constraints.constrain(Size(constraints.maxWidth, _height));
}
Show the answer

Proven wrong in Layout and Paint Internals, with a real, run test: a sizedByParent: true object is unconditionally its own relayout boundary (proven from the exact source rule that lesson quotes), which means plain markNeedsLayout() never notifies the parent when this object’s own size changes — the parent silently keeps a stale size. sizedByParent’s own doc comment requires markNeedsLayoutForSizedByParentChange() instead for exactly this case, and that lesson’s test proves the difference directly: with the plain call, the parent’s observed child width stayed stuck at the old value; with the correct call, it updated.

This module cites permalinks into github.com/flutter/flutter and github.com/dart-lang/sdk. What revision/tag are they pinned to, and why does that matter?
Proven in The Three Trees with a real render-object-level spy, what happens to createRenderObject when the SAME widget type and key rebuild with only a paint-affecting property changed?
Proven in Build Scheduling and Frames with a real build counter, what is the actual scheduling guard behind "redundant setState calls are cheap"?
Proven in Layout and Paint Internals, quoted verbatim from RenderObject.layout, what is the exact relayout-boundary rule?
Proven in Impeller and the GPU with real docs.flutter.dev quotes, what is the REAL thread merge that happened as of Flutter 3.29?
Proven in The Dart VM and Isolates by extracting a real built APK, what did running the standard Unix file(1) command on libapp.so show?