Explain the Flutter widget lifecycle
4 min read
Flutter Basics
Theory — the flow
createState() ← Flutter creates the State once
↓
initState() ← one-time setup (controllers, subs)
↓
didChangeDependencies() ← context-aware setup (Theme, MediaQuery)
↓
build() ←──────────┐
↓ │ triggered by setState() / didUpdateWidget() / didChangeDependencies()
[widget active] │
↓ │
didUpdateWidget() │ (parent rebuilt with new config)
│ │
└─────────────┘
↓
deactivate() ← removed from tree (might be reinserted)
↓
dispose() ← gone for good — release resources
What to do in each method
| Method | Use it for | Avoid |
|---|---|---|
initState | controllers, subscriptions, one-time work | Theme.of(context), Navigator.of(context) — context isn't fully wired |
didChangeDependencies | reading InheritedWidgets after init or when they change | heavy work each rebuild |
didUpdateWidget | react when parent passes new params | calling setState unconditionally (causes loop) |
build | construct the widget tree | side effects, network calls, expensive computations |
dispose | cancel subscriptions, dispose() controllers | calling setState (widget is gone) |
Code in action
class _Profile extends State<Profile> {
late final StreamSubscription _sub;
late final AnimationController _anim;
@override
void initState() {
super.initState();
_sub = api.userStream(widget.userId).listen(_onUser);
_anim = AnimationController(vsync: this, duration: kThemeAnimationDuration);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Safe to read inherited widgets here
final locale = Localizations.localeOf(context);
}
@override
void didUpdateWidget(covariant Profile old) {
super.didUpdateWidget(old);
// React when parent passes a different userId
if (widget.userId != old.userId) {
_sub.cancel();
_sub = api.userStream(widget.userId).listen(_onUser);
}
}
@override
Widget build(BuildContext context) => ...;
@override
void dispose() {
_sub.cancel();
_anim.dispose();
super.dispose();
}
}
When does each fire? (Quick reference)
| Trigger | What fires |
|---|---|
| Widget mounted first time | createState → initState → didChangeDependencies → build |
setState(() {}) called | build |
| Parent rebuilds with new props | didUpdateWidget → build |
| InheritedWidget changes (e.g. theme) | didChangeDependencies → build |
| Widget removed from tree | deactivate → dispose |
Moved within tree (GlobalKey) | deactivate → reinserted → didChangeDependencies → build |
Common mistakes to avoid
// ❌ Reading inherited widgets in initState — partially wired context
@override
void initState() {
super.initState();
final theme = Theme.of(context); // ⚠️ unreliable
}
// ✅ Use didChangeDependencies, or schedule for after first frame:
WidgetsBinding.instance.addPostFrameCallback((_) {
final theme = Theme.of(context);
});
// ❌ Forgetting to dispose
late final _ctrl = TextEditingController();
// dispose() override missing → leak on every screen open
// ❌ Calling setState in build()
@override
Widget build(BuildContext context) {
setState(() => _count++); // 💥 infinite rebuild
}
// ❌ Calling setState after the widget is unmounted
fetchData().then((d) => setState(() => _data = d)); // crashes if user navigated away
// ✅ Guard with `mounted`
if (mounted) setState(() => _data = d);
// ❌ Doing heavy work in build()
@override
Widget build(BuildContext context) {
final sorted = items.toList()..sort(); // recomputed every rebuild
}
// ✅ Cache in didChangeDependencies / didUpdateWidget
Interview follow-ups
-
Why is
didChangeDependenciesseparate frominitState? IninitState, the State isn't fully attached to the tree yet — calls likeTheme.of(context)can be unsafe and won't subscribe to inherited changes.didChangeDependenciesruns after that wiring is complete and re-runs whenever anInheritedWidgetyou depend on changes. -
What's the difference between
disposeanddeactivate?deactivateruns when the State is removed from the tree but might be reinserted (e.g. viaGlobalKey).disposeruns when it's gone for good. Cleanup that must always happen (cancel subs, dispose controllers) goes indispose. -
When does
didUpdateWidgetfire vssetState?setStateis fired from inside the State when its own data changes.didUpdateWidgetfires when the parent rebuilds with new constructor params — you typically comparewidget.foovsoldWidget.fooand react. -
Why the
mountedcheck beforesetState? Async work might complete after the widget is removed. CallingsetStateon an unmounted State throws.if (!mounted) return;after everyawaitis the safe pattern.
How helpful was this content?
Please sign in to rate this article.