Flutter Internals
Three trees, RenderObject, Element tree, BuildOwner, rendering pipeline
- 01
Explain Flutter's three trees: Widget, Element, and RenderObject.
HardFlutter keeps three parallel trees so that cheap descriptions can be thrown away and rebuilt constantly while the expensive layout objects survive.
// Understanding the three trees: // What you write (WIDGET TREE): Scaffold( body: Center(child: Text('Hello')), ) … - 02
How does Flutter's rendering pipeline work? Explain the frame lifecycle.
HardEvery frame runs the same fixed sequence, and the whole sequence has to fit inside one display refresh — roughly 16 ms at 60 Hz, and only 8 ms on a 120 Hz ProMotion screen.
// ── Layout protocol: constraints down, sizes up ───── // Custom RenderObject example class RenderCenteredBox extends RenderBox { RenderBox? _child; @override … - 03
What are slivers in Flutter and how do you build custom scroll effects?
HardSlivers are scroll-aware render objects — they know their scroll offset and can change size/appearance as the user scrolls.
// ── Full CustomScrollView example ────────────────── CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ // Collapsing + parallax app bar SliverAppBar( … - 04
What is BuildContext and what does it actually hold?
MediumBuildContext is a handle to a location in the Element tree.
// BuildContext is just an Element under the hood @override Widget build(BuildContext context) { // ✅ Use context only here, in this synchronous frame final theme = Theme.of(context); final size = MediaQuery.of(context).size; … - 05
What are BuildOwner and PipelineOwner, and why split them?
HardFlutter splits every frame into two stages with two different owners, because reconciling widgets and laying out render objects have completely different cost profiles and ordering rules.
void dumpFrameSnapshot() { final buildOwner = WidgetsBinding.instance.buildOwner!; final pipeline = RendererBinding.instance.pipelineOwner; debugPrint('inside a build phase right now: ${buildOwner.debugBuilding}'); debugPrint('semantics enabled: ${pipeline.semanticsOwner != null}'); } … - 06
How does compositing work? When does Flutter create a new layer?
HardAfter paint, RenderObjects produce a Layer tree — not a flat bitmap.
// ── RepaintBoundary — explicit layer separation ───── Stack( children: [ // Static, expensive background — repainting once is fine RepaintBoundary(child: const BlueprintGrid()), … - 07
How does hit testing work in Flutter?
HardWhen the user taps the screen, Flutter walks the RenderObject tree to figure out who consumes the event.
// ── HitTestBehavior — controls hit-test on transparent areas ── // 1) Transparent SizedBox — no hits unless behavior set GestureDetector( onTap: () => debugPrint('tap'), child: const SizedBox.expand(), // ❌ transparent — no hits ); … - 08
What is the Semantics tree and how does Flutter implement accessibility?
MediumBeside the Widget/Element/RenderObject trees, Flutter maintains a Semantics tree.
// ── Default — adequate for most cases ── ElevatedButton( onPressed: _save, child: const Text('Save'), // label = 'Save', role = button ); … - 09
What is the Flutter engine, and how does the embedder boot a Flutter app on each platform?
HardFlutter ships as three layers — a Dart framework, a C++ engine, and a per-platform embedder that hosts the engine inside an otherwise normal native app.
// A named Dart entry point is what an embedder asks for when it starts a // second engine (add-to-app, background isolates, engine groups). @pragma('vm:entry-point') void secondaryMain() { runApp(const SecondaryApp()); } … - 10
What does WidgetsFlutterBinding.ensureInitialized() actually do, and when must you call it yourself?
MediumIt creates the singleton that wires the Dart framework to the engine, and nothing that talks to the platform works before it exists.
Future<void> main() async { // Must come first: everything below talks to the engine final binding = WidgetsFlutterBinding.ensureInitialized(); binding.deferFirstFrame(); // hold the native splash await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); … - 11
A RenderObject calls markNeedsLayout. How far up the tree does that dirt travel, and what stops it?
HardIt climbs to the nearest relayout boundary and stops there, which is the only reason a size change deep inside a list does not re-lay-out the whole screen.
class RenderStripe extends RenderBox with RenderObjectWithChildMixin<RenderBox> { RenderStripe({required double thickness}) : _thickness = thickness; double _thickness; set thickness(double v) { if (v == _thickness) return; … - 12
What makes Flutter produce a frame at all, and what is the difference between transient, persistent and post-frame callbacks?
MediumAn idle Flutter app renders nothing — a frame exists only because something asked for one.
class MeasuredHeader extends StatefulWidget { const MeasuredHeader({super.key}); @override State<MeasuredHeader> createState() => _MeasuredHeaderState(); } class _MeasuredHeaderState extends State<MeasuredHeader> { … - 13
Wrapping a Column child in Positioned throws "Incorrect use of ParentDataWidget" — what was Positioned actually trying to do?
MediumPositioned paints nothing and sizes nothing — it writes fields into its child render object's
parentData, and that slot only accepts them when the parent that owns it is a Stack.class TrayParentData extends ContainerBoxParentData<RenderBox> { double weight = 1; } class RenderTray extends RenderBox with ContainerRenderObjectMixin<RenderBox, TrayParentData> { … - 14
Your custom RenderObject only changes a colour, yet the timeline shows layout running every frame — what did the setter call?
MediumIt called
markNeedsLayoutwheremarkNeedsPaintwould have done — a RenderObject has one mark per pipeline phase, and you pick the cheapest one the change actually invalidates.class RenderBadge extends RenderBox { RenderBadge({required Color color, required double radius}) : _color = color, _radius = radius; Color _color; … - 15
A widget is removed from the tree — when does its State.dispose actually run, and what is the framework doing in the gap?
MediumNot at removal: the element is parked in an inactive list for the rest of the frame, and only what is still sitting there when the frame is finalized gets unmounted and disposed.
class _ChartState extends State<Chart> { StreamSubscription<Tick>? _sub; Tick? _last; @override void initState() { … - 16
You push an opaque route over a page whose AnimationController is running — does that controller keep ticking underneath?
MediumNo: the Overlay wraps every entry hidden behind an opaque one in
TickerMode(enabled: false), and a muted ticker stops calling its callback.class _PulseState extends State<Pulse> with SingleTickerProviderStateMixin { late final AnimationController _c = AnimationController( vsync: this, // the mixin binds ticker.muted to TickerMode.getNotifier(context) duration: const Duration(seconds: 1), ); … - 17
A parent and one of its descendants both call setState before the same frame — how many times does the descendant build?
HardOnce. The dirty list is sorted by depth, so the parent builds first and the descendant's own dirt is consumed by that same pass.
class _ParentState extends State<Parent> { int _n = 0; void bump() => setState(() => _n++); // marks dirty + schedules a frame @override … - 18
A route transition throws "Duplicate GlobalKeys detected in widget tree" — what is a GlobalKey actually doing under the hood?
HardA GlobalKey is a registry entry — one key maps to exactly one live Element in the BuildOwner — and the error means two subtrees claimed the same entry inside one frame.
class _GalleryState extends State<Gallery> { // Created once, owned by one State — never inside build() final _playerKey = GlobalKey<VideoPlayerState>(); bool _fullscreen = false; … - 19
Why can't LayoutBuilder's builder call setState on an ancestor, and how does it build widgets in the middle of layout at all?
HardBecause the builder does not run in the build phase — it runs inside
performLayout, through a build scope of its own that the framework opens while layout is already in progress.// ❌ the ancestor's build phase is over — layout is running right now LayoutBuilder( builder: (context, constraints) { setState(() => _measured = constraints.maxWidth); // "setState() or markNeedsBuild() called during build." return Content(width: _measured); … - 20
A custom RenderBox renders fine alone but throws "does not implement computeDryLayout" inside a Wrap — what is the parent asking for?
HardIt is asking what size you would be under these constraints without you actually laying out — a dry layout — and the base implementation of
computeDryLayoutthrows rather than guess on your behalf.class RenderStackedPair extends RenderBox with ContainerRenderObjectMixin<RenderBox, BoxParentData>, RenderBoxContainerDefaultsMixin<RenderBox, BoxParentData> { // One sizing rule, two callers: the dry pass measures, the wet pass lays out. Size _computeSize(BoxConstraints constraints, ChildLayouter layoutChild) { …