The Widget Tree and the Build Method
How Flutter turns your nested widgets into pixels, and why build methods should stay fast and side-effect free.
3 min de lectura
You've been writing nested widgets for a couple of lessons now — this lesson explains what Flutter actually does with them, because understanding it will save you from some confusing bugs later.
Three trees, not one
When people say "the widget tree," Flutter actually maintains three parallel trees:
- The widget tree — the lightweight, immutable objects you write in
buildmethods. These are cheap to create and thrown away constantly. - The element tree — the long-lived objects that connect widgets to their place in the running app. Elements are what actually persist across rebuilds (this is how
Stateobjects survive, as you saw in the last lesson). - The render tree — the objects that know how to measure, position, and paint pixels.
You'll write widgets almost exclusively; elements and the render tree are managed by Flutter internally. But knowing they exist explains a lot: when you call setState, Flutter doesn't tear down and rebuild the whole app — it rebuilds only the widget subtree that changed, diffs it against the existing element tree, and updates the render tree in the smallest way necessary.
The build method is called often
build can run far more often than you'd expect — any time the widget's configuration changes, any time an ancestor rebuilds and passes new data down, or any time setState is called on that widget's own state. Because of this, build methods need two properties:
@override
Widget build(BuildContext context) {
print('Building HomeScreen'); // fine for learning, remove in real code
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: const Center(child: Text('Welcome')),
);
}- Fast — no expensive computation, no blocking work. If you need to compute something costly, do it once and cache the result, not on every
build. - Free of side effects — never call
setState, start a network request, or mutate external state directly insidebuild. Flutter may callbuildmultiple times for the same visual output, and side effects there can cause infinite rebuild loops or duplicate work.
BuildContext
Every build method receives a BuildContext — a handle to that widget's location in the element tree. You'll use it constantly to look things up from ancestors, such as the current theme or navigator:
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Text('Styled text', style: theme.textTheme.titleLarge);
}Theme.of(context) walks up the element tree from this widget's position to find the nearest Theme widget above it — which is why context has to come from this widget's own build method, not be reused from somewhere else in your code.
What this explains
If you've ever wondered why Flutter feels fast even with deeply nested UIs, this is why: only the widgets that actually need to change get rebuilt, and only the render objects that actually changed get repainted. As long as your build methods stay small and side-effect free, Flutter's diffing does the heavy lifting of keeping the screen in sync with your data — which is exactly the declarative promise from the first lesson of this course.