← Back to blog
Flutter Fundamentals · Part 4 of 9
July 18, 20269 min read

The build Method in Flutter: What It Does and When It Gets Called

FlutterDart

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:

  1. What does build actually do?
  2. 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: build must 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 build runs, 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 context is tied to its position in the tree — using the wrong context (e.g. one above the Navigator or Theme you 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 in initState, an event handler, or a FutureBuilder/StreamBuilder. build only 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:

  • build is a pure function: current state in, widget description out (UI = f(state)). It doesn't paint.
  • It can be called constantlysetState, ancestor rebuilds, inherited-data changes, animations, new config, hot reload — and you don't control exactly when.
  • Therefore build must be fast and side-effect-free. Do one-time work in initState, not build.
  • BuildContext is the Element; .of(context) lookups walk the tree and create rebuild dependencies.
  • Keep it fast: const subtrees, push setState down, lazy lists, no work in build.

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.