Irbisa · Cheatsheet

Flutter Internals

Three trees, RenderObject, Element tree, BuildOwner, rendering pipeline

Senior Developer · 22 questions

  1. 01

    What are Flutter's bindings (WidgetsFlutterBinding, SchedulerBinding, RendererBinding, GestureBinding), and what does each one own?

    Bindings are singletons that connect the engine to the framework, one per area, and WidgetsFlutterBinding mixes them all into one class.

  2. 02

    Explain Flutter's three trees: Widget, Element, RenderObject. Why three trees instead of one?

    Widgets are cheap immutable descriptions rebuilt all the time, Elements are the long-lived instances that hold state and position, and RenderObjects do the expensive layout and painting.

  3. 03

    How does Flutter draw its UI without native OEM widgets, and what do the framework, engine and embedder each do?

    Flutter never asks the platform for a button or a text view: it paints every pixel itself with its own renderer into a surface the platform provides.

  4. 04

    Toggling the switch below resets Counter to zero every time. Why does it lose its State, and how do you keep it?

    @overrideWidget build(BuildContext context) {  const counter = Counter(); // StatefulWidget that keeps its own count  return Column(    children: [      Switch(        value: _padded,        onChanged: (v) => setState(() => _padded = v),      ),      _padded          ? const Padding(padding: EdgeInsets.all(16), child: counter)          : counter,    ],  );}

    Because the widget at that slot changes type.

  5. 05

    What is an Element's lifecycle (mount, update, deactivate, activate, unmount), and when does State.dispose actually run?

    An Element is mounted when its widget first appears, updated in place while the widget keeps the same type and key, deactivated when it leaves the tree, and unmounted at the end of that frame unless a GlobalKey claimed it back.

  6. 06

    How does Flutter's rendering pipeline work, and what happens between a setState and pixels on screen?

    setState only marks an element dirty and requests a frame.

    void main() {  WidgetsFlutterBinding.ensureInitialized();  // Real frame timings from the engine, also in release builds  SchedulerBinding.instance.addTimingsCallback((timings) {    for (final t in timings) {      if (t.totalSpan > const Duration(milliseconds: 16)) {…
  7. 07

    What is the difference between transient, persistent and post-frame callbacks, and when do you use addPostFrameCallback?

    When vsync arrives, SchedulerBinding runs three kinds of callback in a fixed order: transient ones first, then persistent ones that build the frame, then post-frame ones once it is finished.

    class _HeaderState extends State<Header> {  final _key = GlobalKey();  double? _height;   @override  void initState() {…
  8. 08

    Wrapping a Column child in Positioned throws "Incorrect use of ParentDataWidget". What was Positioned actually trying to do?

    Positioned draws nothing.

    class TrayParentData extends ContainerBoxParentData<RenderBox> {  double weight = 1;} class Weighted extends ParentDataWidget<TrayParentData> {  const Weighted({super.key, required this.weight, required super.child});…
  9. 09

    This setter runs on every rebuild through updateRenderObject, and the timeline shows layout every frame. What is wrong with it?

    class RenderBadge extends RenderBox {  RenderBadge({required Color color}) : _color = color;   Color _color;  set color(Color value) {    _color = value;    markNeedsLayout();  }   @override  void performLayout() => size = constraints.constrain(const Size.square(24));   @override  void paint(PaintingContext context, Offset offset) => context.canvas      .drawCircle(offset + size.center(Offset.zero), 12, Paint()..color = _color);}

    Two things: it calls markNeedsLayout where markNeedsPaint is enough, and it has no equality guard.

  10. 10

    Find the bug: StatusDot keeps its first color even after its parent rebuilds it with a new one.

    class StatusDot extends LeafRenderObjectWidget {  const StatusDot({super.key, required this.color});  final Color color;  @override  RenderStatusDot createRenderObject(BuildContext context) => RenderStatusDot(color);}class RenderStatusDot extends RenderBox {  RenderStatusDot(this._color);  Color _color;  set color(Color v) { if (v == _color) return; _color = v; markNeedsPaint(); }  @override void performLayout() => size = constraints.constrain(const Size.square(12));  @override void paint(PaintingContext context, Offset offset) => context.canvas.drawCircle(offset + size.center(Offset.zero), 6, Paint()..color = _color);}

    StatusDot never overrides updateRenderObject, so a new color never reaches the render object.

  11. 11

    How does hit testing work in Flutter, and why is a child that overflows its parent not tappable?

    On pointer down Flutter walks the render tree from the root and asks each RenderBox whether the point is inside it; everything that says yes is collected into a HitTestResult, deepest first, and the event is dispatched along that path.

    // A round button: hits in the square corners fall through to whatever is behindclass RenderRoundButton extends RenderProxyBox {  @override  bool hitTest(BoxHitTestResult result, {required Offset position}) {    final center = size.center(Offset.zero);    if ((position - center).distance > size.shortestSide / 2) return false;…
  12. 12

    You push an opaque route over a page whose AnimationController is running. Does that controller keep ticking underneath?

    No. The Overlay builds everything hidden behind an opaque route inside TickerMode(enabled: false), and a muted ticker stops calling its callback, so the animation freezes until the route is popped.

    @overridevoid initState() {  super.initState();  // wrong: if an opaque route covers this page first, the future waits  // until the user comes back, because the ticker is muted, not finished  //   _controller.forward().then((_) => _startTimer());…
  13. 13

    What is debugFillProperties for, and how do you make a custom widget show its fields in the Flutter Inspector?

    debugFillProperties is the Diagnosticable hook that lists an object's properties for debugging tools.

    class PriceTag extends StatelessWidget {  const PriceTag({super.key, required this.amount, this.currency = 'USD', this.onSale = false});   final double amount;  final String currency;  final bool onSale;…
  14. 14

    Why can Flutter afford to rebuild thousands of widgets per frame, and when does that stop being true?

    Because rebuilding widgets only creates small immutable objects and compares them; the expensive Elements and RenderObjects are reused and touched only when something actually changed.

    AnimatedBuilder(  animation: _controller,  // The same child instance comes back on every tick, so updateChild  // sees identical(old, new) and skips the whole chart  child: const ExpensiveChart(),  builder: (context, child) => Transform.rotate(…
  15. 15

    A parent and one of its descendants both call setState before the same frame. How many times does the descendant build?

    Once. BuildOwner sorts the dirty list by depth, so the parent builds first, and the descendant's own dirty flag is consumed by that same pass.

  16. 16

    A route transition throws "Duplicate GlobalKeys detected in widget tree". What is a GlobalKey actually doing under the hood?

    A GlobalKey is a registry entry: one key points at exactly one live element.

    class _GalleryState extends State<Gallery> {  // Created once, owned by one State  final _playerKey = GlobalKey<VideoPlayerState>();  bool _fullscreen = false;   @override…
  17. 17

    What are BuildOwner and PipelineOwner, and why is the work split between them?

    They are the two managers of a frame: BuildOwner drives the Element tree through the build phase, PipelineOwner drives the RenderObject tree through layout, compositing bits, paint and semantics.

  18. 18

    A render object calls markNeedsLayout. How far up the tree does that dirt travel, and what stops it?

    It climbs to the nearest relayout boundary and stops there.

    class RenderStripe extends RenderBox with RenderObjectWithChildMixin<RenderBox> {  RenderStripe(this._thickness);   double _thickness;  set thickness(double v) {    if (v == _thickness) return;…
  19. 19

    Find the bug: after the text inside RenderCaption changes, the padded caption keeps its old size or the debug build asserts.

    class RenderCaption extends RenderShiftedBox {  RenderCaption() : super(null);  static const double padding = 8;   @override  void performLayout() {    final child = this.child!;    child.layout(constraints.deflate(const EdgeInsets.all(padding)));    (child.parentData! as BoxParentData).offset = const Offset(padding, padding);    size = constraints.constrain(      Size(child.size.width + padding * 2, child.size.height + padding * 2),    );  }}

    performLayout reads child.size but lays the child out without parentUsesSize: true.

  20. 20

    How does compositing work in Flutter, and when does a render object get its own layer?

    Paint does not produce a bitmap: it records drawing commands into a tree of layers, and compositing turns that layer tree into a scene for the raster thread.

  21. 21

    Why can't LayoutBuilder's builder call setState on an ancestor, and how does it build widgets in the middle of layout at all?

    Because the builder does not run in the build phase.

    // wrong: the ancestor's build is over, layout is running right nowLayoutBuilder(builder: (context, constraints) {  setState(() => _width = constraints.maxWidth); // asserts  return Content(width: _width);});…
  22. 22

    How do you write a custom RenderBox, and when is it the right tool instead of composing widgets or a CustomPainter?

    You write a RenderObjectWidget that creates and updates a RenderBox, and the box implements layout, paint and hit testing itself.

    class Inset extends SingleChildRenderObjectWidget {  const Inset({super.key, required this.gap, super.child});  final double gap;  @override  RenderObject createRenderObject(BuildContext context) => RenderInset(gap);  @override…