skip to content

In Flutter, why is a StatefulWidget's build method defined on its State class rather than on the widget itself?

level: middleimportance: nice to knowfreq 20%

answer

  1. closures capture this
  2. the widget instance goes stale
  3. State persists while widgets are replaced
  4. AnimatedWidget declares its own build
  5. State stays private to subclassers

basics

~20 s

Closures 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 s

The 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 lines
dart
import '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

for a junior

Recall that StatefulWidget puts build on its State and that you read configuration there through widget.

for a middle

Explain the closure-capture reason: this in State.build is the persistent State, whose widget getter the framework keeps current.

for a senior

Connect the design to real bugs: configuration captured outside build, in initState or long-lived listeners, still goes stale and needs explicit handling.

for a principal

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