Summary: in this tutorial, you’ll explore the flutter stateful widget lifecycle and what you can do in each phase.
A StatefulWidget is a Flutter widget that can change its UI over time because it remembers some data called state.
When that state changes (for example, a counter number), Flutter automatically rebuilds the UI to show the new value.
You use a StatefulWidget when your screen needs to update in response to user actions or events.
Lifecycle flow (simplified) #
createState()
↓
initState() ← called once
↓
didChangeDependencies()
↓
build() ← called many times
↓
(during updates)
didUpdateWidget()
↓
build()
↓
(disposal)
dispose()initState() #
What it is #
Called once when the State object is first created.
@override
void initState() {
super.initState();
// one-time initialization
}Code language: JavaScript (javascript)When it’s called #
- ✅ Automatically
- ✅ Exactly one time
- ✅ Before the first
build()
What it’s for #
Use initState for one-time setup:
✅ Good uses:
- Logging an error once
- Initializing controllers
- Starting timers
- Initial API calls
- Adding listeners
- Reading initial widget values
❌ Bad uses:
- Calling
BuildContextAPIs that depend on ancestors (Theme, MediaQuery) - Calling
setState()
Example #
@override
void initState() {
super.initState();
analytics.logPageView();
}Code language: CSS (css)didChangeDependencies() #
What it is #
Called when inherited dependencies change
@override
void didChangeDependencies() {
super.didChangeDependencies();
}Code language: CSS (css)When it’s called #
- ✅ Right after
initState - ✅ Again when an
InheritedWidgetchanges:ThemeMediaQueryLocalizationsProvider,Riverpod, etc.
What it’s for #
Use this if you depend on context-based data:
✅ Good uses:
- Reading
Theme.of(context) - Reading
MediaQuery.of(context) - Subscribing to providers
- Localizations
❌ Bad uses:
- One-time logic unrelated to context
- Heavy work (can be called multiple times)
Example #
@override
void didChangeDependencies() {
super.didChangeDependencies();
final theme = Theme.of(context);
}Code language: JavaScript (javascript)build(BuildContext context) #
What it is #
Builds the UI.
@override
Widget build(BuildContext context) {
return …
}Code language: CSS (css)When it’s called #
- ✅ Very often
- ✅ After
initState - ✅ After
setState - ✅ When parent rebuilds
- ✅ When dependencies change
- ✅ During animations, rotations, etc.
Golden rule #
build()must be pure and side-effect‑free
✅ What to do #
- Create widgets
- Read state
- Read immutable data
- Choose layouts
❌ What NOT to do #
- Logging
- API calls
- Timers
- Controllers init
- State mutations
- Analytics
Bad example: #
@override
Widget build(BuildContext context) {
Logger.e(error); // ❌ runs many times
…
}Code language: JavaScript (javascript)didUpdateWidget() #
What it is #
Called when the parent rebuilds with a new widget configuration, but State is reused.
@override
void didUpdateWidget(covariant MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
}Code language: CSS (css)When it’s called #
- ✅ Parent rebuilds
- ✅ Widget constructor args change
- ✅ BEFORE
build()
This is VERY important #
Widgets change → State does NOT reset
So Flutter gives you this hook to respond.
What it’s for #
Use didUpdateWidget to:
- Compare old vs new values
- React to external changes
- Update controllers
- Re-log errors when they change
Example (logging safely) #
@override
void didUpdateWidget(covariant PrettyError oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.error != oldWidget.error) {
Logger.e('${widget.message}: ${widget.error}');
}
}Code language: JavaScript (javascript)✅ Only logs when the error actually changes
setState() #
What it is #
Marks the widget dirty so Flutter knows to rebuild.
setState(() {
counter++;
});Rules #
- ✅ Call when state changes
- ❌ Never in
build() - ❌ Not in
initState()without delay - ✅ Safe in async callbacks
dispose() #
What it is #
Called when the State is removed permanently.
@override
void dispose() {
controller.dispose();
super.dispose();
}Code language: CSS (css)When it’s called #
- ✅ Widget is removed from tree
- ✅ Navigation pop
- ✅ List item destroyed
What it’s for #
- Dispose controllers
- Cancel timers
- Remove listeners
- Close streams
Quick cheat sheet #
| Method | Called | Use for | Avoid |
|---|---|---|---|
initState | Once | Setup, logging, controllers | Context, setState |
didChangeDependencies | Many | Theme, MediaQuery, providers | Heavy logic |
build | Very often | UI only | Side effects |
didUpdateWidget | When props change | React to changes | Ignoring oldWidget |
dispose | Once | Cleanup | Forgetting super.dispose() |