In Flutter, why is a StatefulWidget's build method defined on its State class rather than on the widget itself?
answer
- closures capture this
- the widget instance goes stale
- State persists while widgets are replaced
- AnimatedWidget declares its own build
- State stays private to subclassers
basics
~20 sClosures created in build capture this; on State that is the long-lived object whose widget getter always points at the newest configuration, so callbacks never read a stale widget. It also lets subclasses such as AnimatedWidget keep their State private.
solid answer
~40 sThe framework's own design notes give two reasons. First, closures: a handler created inside `build` implicitly captures `this`. If `build` lived on the widget, `this` would be one immutable widget instance, and a closure that outlives it would keep reading its old fields after the parent had rebuilt with new values. On `State`, `this` is the object that persists across rebuilds, and `widget.color` is re-read through the State, whose `widget` the framework keeps updated. Second, subclassing: `AnimatedWidget` extends `StatefulWidget` and declares its own abstract `build(BuildContext)` for its subclasses; if `StatefulWidget` had a `build(context, state)` method, it would have to expose its internal State to them. The same design would even let `StatelessWidget` be written as a `StatefulWidget` subclass.
code
dart · 22 linesimport 'package:flutter/material.dart';
class TipButton extends StatefulWidget {
const TipButton({super.key, required this.percent});
final int percent;
@override
State<TipButton> createState() => _TipButtonState();
}
class _TipButtonState extends State<TipButton> {
@override
Widget build(BuildContext context) {
return TextButton(
// Captures the State; widget.percent is read at tap time,
// so it reflects the newest TipButton the parent built.
onPressed: () => debugPrint('tip ${widget.percent} %'),
child: Text('${widget.percent} %'),
);
}
}go deeper
Recall that StatefulWidget puts build on its State and that you read configuration there through widget.
Explain the closure-capture reason: this in State.build is the persistent State, whose widget getter the framework keeps current.
Connect the design to real bugs: configuration captured outside build, in initState or long-lived listeners, still goes stale and needs explicit handling.
Use this as an example of API design that removes a bug class by construction, and apply the same thinking to your own widget and package APIs.
## The design question A `StatelessWidget` defines `build` on the widget. A `StatefulWidget` does not: it defines `createState`, and `build` lives on the `State<T>` subclass. Interviewers ask why, because the answer shows whether a candidate understands that widgets are replaced while State persists. The framework source answers it in a design-discussion section on `State.build`, giving two reasons. ## Reason 1: closures capture this In Dart, a closure created inside an instance method implicitly captures `this`, so any field it mentions is read through that object when the closure runs. Consider a hypothetical API where `build` lived on the widget: 1. The parent builds `MyButton(color: blue)`, and `build` creates an `onPressed` closure that prints `color`. 2. The parent rebuilds with `MyButton(color: green)`: a **new** widget instance. 3. If the first closure is still referenced somewhere, it captured the **first** widget, so it prints blue although the screen now says green. With `build` on `State`, the closure captures the State instead. The State is the object that survives rebuilds, and on each update the framework assigns the new widget to it, so `widget.color` read through the State returns green. Stale reads of configuration through `this` are ruled out by construction. ## Reason 2: subclassing flexibility `AnimatedWidget` is a `StatefulWidget` subclass that listens to a `Listenable` and asks its own subclasses to implement `Widget build(BuildContext context)`. Its State is a private implementation detail. If `StatefulWidget` had declared `Widget build(BuildContext context, State state)`, every such subclass would be forced to receive, and so know about, a State object it should not see. The source adds that `StatelessWidget` could conceptually be implemented as a `StatefulWidget` subclass the same way, which would be impossible with `build` on the widget. ## What it means for your code - Read configuration through `widget.x` inside `build` and inside callbacks; it is always the newest instance. - A local variable such as `final color = widget.color;` taken inside `build` is fine, because `build` reruns with each update and recreates the closures. - The stale-value risk returns when you store configuration **outside** `build`, for example copying it in `initState` or handing a closure once to a long-lived object; those cases need explicit handling. - `State<T>` is generic, so `widget` is typed as your widget class and its fields are available without casts. ## Summary | If `build` were on the widget | With `build` on `State` | |---|---| | Closures capture one immutable widget instance | Closures capture the persistent State | | A long-lived closure reads old configuration | `widget` always points at the newest instance | | Subclasses like `AnimatedWidget` must expose their State | The State stays private to the subclass | This is a NICE_TO_KNOW design question: few interviews hinge on it, but answering it well demonstrates a clear model of widget replacement versus State persistence.
- Is taking a local copy such as final percent = widget.percent inside build then unsafe?No. `build` runs again after every configuration update and creates new closures, so a local taken inside `build` is fresh for the closures created in that build. The stale-value risk returns only when configuration is stored outside `build`, for example copied in `initState` or captured by a closure registered once with a long-lived object.
saying these in an interview costs you the question
- build is on State because widgets cannot receive a BuildContext
- Putting build on State makes rebuilds faster
- State.widget always refers to the original widget instance
- Closures created in State.build capture the widget instance