Skip to content

Stateless vs Stateful

A StatelessWidget has no mutable state of its own. Given its inputs (constructor arguments), its build always returns the same UI. If nothing about its inputs changes, it never needs to change.

class Greeting extends StatelessWidget {
const Greeting({super.key, required this.name});
final String name;
@override
Widget build(BuildContext context) => Text('Hello, $name');
}

Everything it needs is final and passed in. Reach for StatelessWidget whenever a widget just renders its inputs — which is most widgets.

A StatefulWidget is for UI that changes over time on its own — a counter, a toggle, an animation, a form. But here is the subtlety that trips people up: the widget is still immutable. The mutable part lives in a separate State object.

class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0; // mutable state lives HERE, not on the widget
void _increment() {
setState(() => _count++); // tell Flutter to rebuild with new state
}
@override
Widget build(BuildContext context) {
return TextButton(onPressed: _increment, child: Text('$_count'));
}
}

setState does two things: it runs your mutation, then marks the element dirty so Flutter rebuilds. Without setState, you can change _count all day and the screen never updates — the framework doesn’t know anything changed.

The Widget is thrown away and recreated on every rebuild — so it cannot hold state that must persist. The State object is held by the element, which lives on across rebuilds. That is the entire reason for the two-class design.

flowchart TB
  w1["Counter widget
(rebuilt, discarded)"] -.->|createState once| s["_CounterState
(persists on the element,
keeps _count)"]
  w2["Counter widget
(new instance next build)"] -.->|reuses same State| s
The widget is recreated; its State persists on the element

When the parent rebuilds and makes a new Counter widget, Flutter matches it to the existing element and hands it the same _CounterState — so _count is preserved. New widget, same state. (didUpdateWidget lets the State react to the new widget’s data; full lifecycle is in the State Management module.)

Where does the mutable state of a StatefulWidget live?
What does calling setState do?
Why is State a separate object from the widget?
When should you choose a StatelessWidget?