State
stateless, stateful, and which to choose
What it is
Widgets come in two kinds: those that simply draw the data handed to them, and those that hold something of their own and redraw when it changes.
The idea is the same as props and state in React (article 10.3), and the rules for deciding are the same. Only the syntax differs — and in Flutter it is wordier.
What it looks like in code
Stateless — no data of its own
class CityCard extends StatelessWidget {
final String name;
final double? temp;
final void Function(String) onRemove;
const CityCard({
super.key,
required this.name,
this.temp,
required this.onRemove,
});
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children: [
Text(name),
Text(temp == null ? 'no data' : '$temp°'),
TextButton(
onPressed: () => onRemove(name),
child: const Text('Remove'),
),
],
),
);
}
}Everything comes from outside through the constructor — the direct analogue
of props. The final on the fields is no accident: they can't be changed,
just like props in React.
super.key is the same key as in React (article 10.2), and it is
there for the same reason: so that when the list changes Flutter knows which
widget is which.
Stateful — it has its own
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: _increment,
child: Text('pressed: $_count'),
);
}
}Two classes instead of one is a Flutter peculiarity you have to get used to: the widget itself and its state live separately.
setState is the only way to announce a change. Assigning _count++
without the setState wrapper changes the value, but the screen doesn't
redraw. That is exactly the same mistake as assignment instead of setX
in React.
A leading underscore in Dart means "this file only". Which is why the
state is usually _CounterState and the fields _count.
The lifecycle
@override
void initState() {
super.initState();
_loadData(); // runs once, when it appears
}
@override
void dispose() {
_controller.dispose(); // cleanup: controllers, subscriptions, timers
super.dispose();
}| Method | When | The React equivalent |
|---|---|---|
initState | once, on appearance | useEffect(..., []) |
build | on every redraw | the component's body |
dispose | on removal | the function returned from useEffect |
dispose is mandatory for everything that has to be closed: text field
controllers, scroll controllers, animations, subscriptions, timers. A
forgotten dispose is a leak, and the analyzer won't always warn about it.
When there is a lot of state
Passing data down five levels of constructors is awkward, and there are
state-management libraries for that: provider, riverpod, bloc and
others.
Choosing between them is an architectural decision, not a matter of taste: it determines how the whole project is arranged. It is worth making yourself rather than leaving to an AI (article 1.1), and you must name your choice in your requests — otherwise you will get code for whichever library had more of the training data.
What to tell the AI
The project uses provider, version 6. State is held in a
ChangeNotifier. Don't add new state-management libraries.This widget is stateful but has no state of its own —
everything comes through the constructor. Make it stateless.Check whether every controller in this file has a dispose.
A list of what isn't being closed.What to look for
Stateful with no state. An extra class and extra code. The sign: the
State has no fields, only a build.
Assignment without setState. The value changes, the screen doesn't.
setState with heavy work inside. Only field changes go inside;
requests and computations go before it.
No dispose. Controllers and subscriptions outlive the widget.
A network request in build. build is called often; the request
belongs in initState or in the data layer. The direct analogue of the
mistake in article 10.4.
The state library chosen for you. bloc appeared in the answer although
the project uses provider — and the project ends up with two systems at
once.
Cheat sheet
Stateless — everything from outside through the constructor, the equivalent of props.
Stateful — two classes: the widget and its state.
Change fields only inside
setState, or the screen won't redraw.initStateonce,buildoften,disposefor cleanup.Controllers and subscriptions must be closed in
dispose.Choose the state-management library yourself and name it in the request.