The build Method
This is Part 4 of the Flutter Fundamentals series. You've written build in every example so far. Now we make it precise, because misunderstanding build is behind most Flutter performance complaints ("why is my app janky?") and most "why did my expensive thing run 60 times?" bugs.
Two questions to answer fully:
- What does
buildactually do? - When — and how often — does Flutter call it?
Get these right and you'll write fast, correct widgets by reflex.
What build is: a pure description function
build takes a BuildContext and returns a widget subtree describing what the UI should look like right now, given the current widget config and State:
@override
Widget build(BuildContext context) {
return Text('Count: $_count');
}
The mental model from Part 1: UI = f(state). build is that f. It's a pure function of the current state — feed it the same state, it returns the same widget description.
Crucially, build does not draw anything. It produces lightweight, immutable widget blueprints (Part 2); Flutter then reconciles them against the Element tree and lets RenderObjects do the actual layout and painting. So build is cheap if you keep it cheap — it's just constructing widget objects.
The golden rule:
buildmust be fast and side-effect-free. It can be called many times per second, at any time, in any order. Treat it as "describe the UI" — never "do work."
When does build get called?
This is the part people underestimate. build runs far more often than "once." Here is essentially every trigger:
1. The first time the widget is inserted (mount)
When a widget first enters the tree, Flutter creates its Element and calls build to produce its children.
2. When setState is called
setState marks the Element dirty; Flutter rebuilds it on the next frame (Part 3).
setState(() => _count++); // → build() runs again next frame
3. When an ancestor rebuilds
If a parent rebuilds, its children typically rebuild too. This cascades down — which is why a setState high in the tree can rebuild a large subtree.
4. When an InheritedWidget it depends on changes
If your widget reads Theme.of(context), MediaQuery.of(context), or a Provider, Flutter registers a dependency. When that inherited data changes (rotate the device → MediaQuery changes; toggle dark mode → Theme changes), your build is called again automatically.
5. When the parent gives it a new configuration
If the parent rebuilds and passes new constructor values, this widget rebuilds with them (and didUpdateWidget fires for stateful widgets — Part 3).
6. Other framework triggers
Route transitions, animations (every frame an animation runs, the animated subtree rebuilds), keyboard appearing, hot reload (Part 7), and more.
The upshot: you do not control exactly when
buildruns, and it can run 60+ times a second (e.g. during an animation or a scroll). That single fact dictates every rule below.
Why "side-effect-free" matters: the classic bug
Because build runs constantly, doing work inside it is a disaster. The most common offender — kicking off a network request in build:
// ❌ DISASTER: build can run many times → many duplicate requests.
@override
Widget build(BuildContext context) {
final data = fetchFromNetwork(); // fires on EVERY rebuild!
return Text('$data');
}
Every rebuild (every animation frame, every parent rebuild, every theme change) fires a fresh request. The fix is to do one-time work in initState, not build:
// ✅ Kick off the request ONCE; build only describes the UI.
late Future<Data> _future;
@override
void initState() {
super.initState();
_future = fetchFromNetwork(); // once
}
@override
Widget build(BuildContext context) {
return FutureBuilder<Data>(
future: _future, // reuse the same future
builder: (context, snap) => Text('${snap.data}'),
);
}
The rule restated: build describes; initState (and event handlers) do. Never start requests, timers, subscriptions, or setState from inside build.
BuildContext: your location in the tree
Every build receives a BuildContext. From Part 2 you know the secret: context is the Element — your widget's handle into the live, persistent tree. That's what powers the .of(context) lookups:
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // walk up to nearest Theme
final size = MediaQuery.of(context).size; // walk up to nearest MediaQuery
return Container(
color: theme.colorScheme.primary,
width: size.width * 0.5,
);
}
These work because the Element knows its ancestors. Two practical consequences:
- A
contextis tied to its position in the tree — using the wrong context (e.g. one above theNavigatororThemeyou need) is a common error. - Calling
.of(context)creates a dependency: when that ancestor's data changes, your widget rebuilds (trigger #4 above).
Keeping build fast
Since build runs often, follow these to avoid jank:
1. Mark constant subtrees const
A const widget is created once and skipped on rebuild — Flutter reuses the identical instance instead of rebuilding it.
// const subtrees are not rebuilt — free performance.
const Padding(
padding: EdgeInsets.all(8),
child: Icon(Icons.star),
)
2. Push setState down, not up
A setState rebuilds the calling widget and its descendants. If only a small part changes, extract it into its own small widget and call setState there, so you rebuild a tiny subtree instead of a huge one.
// Instead of setState on a giant screen, isolate the changing bit:
class _Likes extends StatefulWidget { /* ... */ }
// Only _Likes rebuilds on tap; the rest of the screen stays put.
3. Don't build huge lists eagerly
Use ListView.builder (lazy) so only visible items build, not all 10,000 at once.
4. Keep build pure
No I/O, no setState, no allocations you could hoist, no mutation of state. Just construct and return widgets.
A helpful instinct: if you're tempted to do something in
build, stop — that work belongs ininitState, an event handler, or aFutureBuilder/StreamBuilder.buildonly describes.
Practice Challenges
Challenge 1 — How many times? A widget is on screen during a 1-second animation running at 60fps, and nothing else changes. Roughly how many times does its (animated) subtree's build run? Why?
Show solution
Roughly 60 times — once per frame for the duration of the animation. An animation rebuilds the animated subtree every frame so the UI reflects the new animation value. This is exactly why build must be cheap and side-effect-free.
Challenge 2 — Fix the duplicate requests. This fires a network call on every rebuild. Fix it.
@override
Widget build(BuildContext context) {
final user = api.getUser(); // returns a Future
return FutureBuilder(future: user, builder: ...);
}
Show solution
Create the future once in initState and reuse it:
late final Future _user;
@override
void initState() { super.initState(); _user = api.getUser(); }
@override
Widget build(BuildContext context) =>
FutureBuilder(future: _user, builder: ...);
Creating the future inside build re-fires the request on every rebuild.
Challenge 3 — List the triggers. Name four distinct things that can cause build to run.
Show solution
Any four of: first mount; setState; an ancestor rebuilding; an InheritedWidget it depends on changing (Theme/MediaQuery/Provider); the parent passing a new configuration; an animation frame; a route transition; hot reload.
Challenge 4 — Shrink the rebuild. A whole screen rebuilds every time a like-count changes. How do you limit the rebuild to just the counter?
Show solution
Extract the changing part into its own small StatefulWidget and call setState there. Since setState only rebuilds the calling widget and its descendants, isolating the like-count means only that tiny widget rebuilds — the rest of the screen is untouched. Also mark static surrounding widgets const so they're skipped.
Questions to test yourself
Q1 (basic). What is the build method's job — and what is it not allowed to do?
Show answer
Its job is to return a widget subtree describing the UI for the current state — UI = f(state). It must not have side effects: no network/file I/O, no setState, no starting timers/subscriptions, no mutating state. It only describes; it doesn't do.
Q2 (basic). Does build paint pixels to the screen?
Show answer
No. build constructs lightweight, immutable widget blueprints. Flutter then reconciles them against the Element tree, and the RenderObjects do the actual layout and painting. build is just object construction — cheap if you keep it so.
Q3 (intermediate). Give three different triggers that cause build to be called.
Show answer
For example: (1) setState marks the widget dirty; (2) an ancestor rebuilds, cascading down to children; (3) an InheritedWidget the widget depends on changes (e.g. MediaQuery on rotation, Theme on dark-mode toggle, a Provider value). Also: first mount, new parent config, animation frames, route changes, hot reload.
Q4 (intermediate). Why is calling fetchData() inside build a bug, and where should it go?
Show answer
Because build can run many times per second (animations, ancestor rebuilds, inherited changes), so the fetch fires repeatedly — duplicate requests, wasted work, possible flicker. One-time work belongs in initState (store the Future), then build reuses it via a FutureBuilder. build describes; initState/handlers do.
Q5 (intermediate). What is BuildContext, and how does Theme.of(context) use it?
Show answer
BuildContext is the widget's Element — its position in the live tree. Theme.of(context) uses it to walk up the tree to the nearest Theme ancestor and read its data. Doing so also registers a dependency, so the widget rebuilds automatically when that Theme changes.
Q6 (advanced). A setState near the top of your tree causes a large, janky rebuild. Explain why and give two ways to reduce the cost.
Show answer
setState rebuilds the calling widget and all its descendants, so calling it high in the tree rebuilds a big subtree every time. Reduce it by (1) pushing the state down — extract the small changing part into its own widget and call setState there, so only that subtree rebuilds; and (2) marking unchanging subtrees const so Flutter skips rebuilding them. (Also use lazy builders like ListView.builder for long lists.)
Wrapping up
build is the heartbeat of a Flutter app — understand its timing and you control performance:
buildis a pure function: current state in, widget description out (UI = f(state)). It doesn't paint.- It can be called constantly —
setState, ancestor rebuilds, inherited-data changes, animations, new config, hot reload — and you don't control exactly when. - Therefore
buildmust be fast and side-effect-free. Do one-time work ininitState, notbuild. BuildContextis the Element;.of(context)lookups walk the tree and create rebuild dependencies.- Keep it fast:
constsubtrees, pushsetStatedown, lazy lists, no work inbuild.
We've talked a lot about widget trees and rebuilds in the abstract. Time to get visual. The next two parts are about arranging widgets on screen. Part 5 starts with the bread-and-butter of every layout: Row, Column, Expanded & Flexible.