Flutter's build(): Why It Lives on State, Not the Widget
Flutter's build() method turns state into UI. It's on the State object, not the StatefulWidget, to ensure it always paints with the latest data. The framework calls it on init, after setState(), or when dependencies change.
WHY IT EXISTS The build() method exists to provide a function that the Flutter framework can call to construct the visual representation of a widget. It's the core mechanism that translates a widget's configuration and a State object's internal data into a tree of elements that can be rendered on screen.
THE MENTAL MODEL Think of a StatefulWidget as a recipe card (immutable properties) and its State object as the chef. The build() method is the chef's action of cooking the dish. When you want to change the dish (e.g., make it spicier), you give the same persistent chef a new recipe card. The chef then re-runs the build() method, using the new recipe to create an updated UI. The chef (State) persists, but the recipe (Widget) can be replaced.
HOW IT WORKS The framework calls build() whenever the UI might need to change. This happens after initState, after a call to setState(), when the widget's configuration changes (didUpdateWidget), or when an InheritedWidget it depends on changes. The method must return a Widget. Flutter's engine then efficiently compares this new widget tree to the previous one and updates only what's necessary. For this reason, build() can be called on every frame and must be fast and free of side effects.
WHEN TO USE IT You don't call build() directly. You must implement it for every State object you create. This is where you define your widget's UI by composing other widgets, using data from both the State object itself and the properties of its corresponding widget (accessed via widget.propertyName).
WHEN NOT TO USE IT Never call build() yourself; to request a rebuild, call setState(). Do not perform heavy computations, network requests, or other asynchronous work directly inside build(). It must return a widget synchronously. Offload expensive operations to be triggered by user interactions or in lifecycle methods like initState.
ONE CANONICAL EXAMPLE The most critical design choice is that build() is on State, not StatefulWidget. This prevents a subtle bug. If build() were on the widget, a closure inside it (like an onPressed handler) would capture this—the widget instance. If the parent rebuilt the widget with a new property (e.g., a new color), the old closure would still reference the old widget and its old color. By putting build() on State, the closure captures the persistent State object. The framework updates this State object's widget property to point to the new widget instance. Thus, accessing widget.color inside the closure always gets the latest value.
Read the original → api.flutter.dev
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.