Programming with AI
ENRU
11.4

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();
}
MethodWhenThe React equivalent
initStateonce, on appearanceuseEffect(..., [])
buildon every redrawthe component's body
disposeon removalthe 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

Prompt
The project uses provider, version 6. State is held in a
ChangeNotifier. Don't add new state-management libraries.
Prompt
This widget is stateful but has no state of its own —
everything comes through the constructor. Make it stateless.
Prompt
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.

  • initState once, build often, dispose for cleanup.

  • Controllers and subscriptions must be closed in dispose.

  • Choose the state-management library yourself and name it in the request.