Explain the Flutter widget lifecycle

High PriorityAsked in ~85% of Flutter interviews

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

MethodUse it forAvoid
initStatecontrollers, subscriptions, one-time workTheme.of(context), Navigator.of(context) — context isn't fully wired
didChangeDependenciesreading InheritedWidgets after init or when they changeheavy work each rebuild
didUpdateWidgetreact when parent passes new paramscalling setState unconditionally (causes loop)
buildconstruct the widget treeside effects, network calls, expensive computations
disposecancel subscriptions, dispose() controllerscalling 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)

TriggerWhat fires
Widget mounted first timecreateStateinitStatedidChangeDependenciesbuild
setState(() {}) calledbuild
Parent rebuilds with new propsdidUpdateWidgetbuild
InheritedWidget changes (e.g. theme)didChangeDependenciesbuild
Widget removed from treedeactivatedispose
Moved within tree (GlobalKey)deactivate → reinserted → didChangeDependenciesbuild

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

  1. Why is didChangeDependencies separate from initState? In initState, the State isn't fully attached to the tree yet — calls like Theme.of(context) can be unsafe and won't subscribe to inherited changes. didChangeDependencies runs after that wiring is complete and re-runs whenever an InheritedWidget you depend on changes.

  2. What's the difference between dispose and deactivate? deactivate runs when the State is removed from the tree but might be reinserted (e.g. via GlobalKey). dispose runs when it's gone for good. Cleanup that must always happen (cancel subs, dispose controllers) goes in dispose.

  3. When does didUpdateWidget fire vs setState? setState is fired from inside the State when its own data changes. didUpdateWidget fires when the parent rebuilds with new constructor params — you typically compare widget.foo vs oldWidget.foo and react.

  4. Why the mounted check before setState? Async work might complete after the widget is removed. Calling setState on an unmounted State throws. if (!mounted) return; after every await is the safe pattern.


How helpful was this content?

Please sign in to rate this article.