Any widget that changes in response to the user — a counter, a toggle, a text field — starts here.
A `StatefulWidget` is split into two classes:
- The widget itself, still immutable, holding configuration.
- A `State<T>` object, created by
createState(), which holds mutable data and builds the UI. Flutter keeps the State alive across rebuilds as long as the widget stays at the same place in the tree.
To change the UI, mutate the state inside `setState(() { ... })`. That marks the widget dirty so Flutter calls build again on the next frame. Changing a field without setState updates the value but not the screen.
Lifecycle methods on State:
initState()— runs once when the State is created; set up controllers, subscriptions and animations here (callsuper.initState()first).didUpdateWidget(oldWidget)— the parent rebuilt with new configuration; react to changed fields.dispose()— release controllers, cancel subscriptions and timers (callsuper.dispose()last).
Inside State, the widget's fields are available through widget.someField, and context and mounted are properties. After an await, check if (!mounted) return; before calling setState or using context, because the widget may have been removed meanwhile.
Keep state as low in the tree as possible: only the widget that owns the state rebuilds. When several widgets need the same state, lift it up or use a state-management approach.
Example
import 'package:flutter/material.dart';
class LikeButton extends StatefulWidget {
const LikeButton({super.key, this.initialLikes = 0});
final int initialLikes;
@override
State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
late int _likes;
bool _liked = false;
@override
void initState() { super.initState(); _likes = widget.initialLikes; } // read config once
void _toggle() => setState(() {
_liked = !_liked;
_likes += _liked ? 1 : -1;
});
@override
Widget build(BuildContext context) => TextButton.icon(
onPressed: _toggle,
icon: Icon(_liked ? Icons.favorite : Icons.favorite_border),
label: Text('$_likes'),
);
}Remember
- Mutable data lives in a State object that survives rebuilds
- Change it inside
setStateto trigger a rebuild - Set up in
initState, clean up indispose, checkmountedafter awaits
Watch out for
- Calling
setStateafter anawaitwhen the widget has been disposed — checkmountedfirst.