Stateless vs Stateful
Stateless: pure description
Section titled “Stateless: pure description”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.
Stateful: state that outlives the widget
Section titled “Stateful: state that outlives the widget”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.
Why the split? So state survives rebuilds
Section titled “Why the split? So state survives rebuilds”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
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.)