Widgets and the Tree
Composition, not inheritance
Section titled “Composition, not inheritance”Flutter builds UI by nesting widgets, not subclassing them. You do not extend a Button to add padding — you wrap it in a Padding. A screen is a deep tree of small, single-purpose widgets composed together.
Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ const Text('Hello'), ElevatedButton(onPressed: () {}, child: const Text('Tap')), ], ), ), );}This is composition over inheritance taken to the extreme: many tiny widgets, each doing one thing, nested to form the whole. Center, Padding, Column are not visual chrome — they are the layout.
The three trees
Section titled “The three trees”Here is the model that explains almost everything about Flutter’s behavior and performance. There is not one tree — there are three, kept in sync:
flowchart LR
subgraph W["Widget tree"]
w1["Padding"] --> w2["Text"]
end
subgraph E["Element tree"]
e1["PaddingElement
(persists)"] --> e2["TextElement
(holds state)"]
end
subgraph R["Render tree"]
r1["RenderPadding
(layout)"] --> r2["RenderParagraph
(paint)"]
end
W -.creates.-> E
E -.creates.-> R - Widget — the immutable description you write. Rebuilt constantly and thrown away. Cheap.
- Element — the living instance of a widget at a position in the tree. It persists across rebuilds, holds the
Statefor stateful widgets, and decides whether a new widget can update it or must replace it. - RenderObject — does the actual layout, painting, and hit-testing. This is the expensive layer, and Flutter works hard to reuse it.
Why rebuilds stay cheap
Section titled “Why rebuilds stay cheap”When you rebuild, Flutter walks the new widget tree and compares each new widget to the element already there. If the new widget has the same type and key as the old one, the element is updated in place — the render object is reused, only changed properties applied. Only when the type or key differs does Flutter tear down the element and its render object and build fresh.
// Rebuild produces a new Text widget every time...Text(counter.toString())// ...but the TextElement and RenderParagraph underneath are REUSED.// Only the changed string is pushed down. That's why setState is cheap.This reconciliation — new immutable widgets diffed against persistent elements — is the engine room of Flutter. Keys (a later lesson) exist to control it when the default type-matching gets it wrong.