Explain the complete lifecycle of a StatefulWidget
TL;DR: The lifecycle goes: createState → initState → didChangeDependencies → build → (didUpdateWidget / setState → build)* → deactivate → dispose.
Full Answer
createState()
Called once when the StatefulWidget is first inserted into the tree. Creates the mutable State object.
initState()
Called once after the State is created. Use for one-time initialization: subscribe to streams, initialize AnimationControllers, register listeners. Never call BuildContext operations that require the tree to be complete here — use didChangeDependencies instead.
didChangeDependencies()
Called immediately after initState and whenever an InheritedWidget dependency changes. Safe to call context.dependOn… here. Useful for fetching data that depends on inherited data like Theme or MediaQuery.
build()
Called after initState, didChangeDependencies, setState, and didUpdateWidget. Must be pure and fast — no side effects, no heavy computation. Returns the widget subtree.
didUpdateWidget(oldWidget)
Called when the parent rebuilds and provides a new widget configuration with the same runtimeType. Use to react to property changes, e.g., cancel and restart an animation if a duration property changed.
deactivate()
Called when the State is temporarily removed from the tree (e.g., during a navigation transition). Rarely overridden.
dispose()
Called when the State is permanently removed. Always dispose: AnimationControllers, TextEditingControllers, FocusNodes, StreamSubscriptions, Timers. Failure to dispose causes memory leaks.
Never call setState() in dispose(). Always call super.dispose() at the end. Check mounted before calling setState() in async callbacks to avoid calling it after dispose.
Code Examples
Console: initState — one-time setup Console: didChangeDependencies — safe to use context fully Console: (on parent rebuild with new title) title changed: Old → New
Common Mistakes
- ✗Calling super.dispose() before cleaning up resources — always clean up first, then call super.
- ✗Using BuildContext in initState synchronously for inherited widgets — use didChangeDependencies.
- ✗Not cancelling stream subscriptions — leads to memory leaks and setState on dead widgets.
Interview Tip
The mounted check pattern: `if (mounted) setState(() { ... })` is essential for any async operation. In Dart 3+, prefer using ref.listen (Riverpod) or BLoC's stream to avoid the need for mounted checks entirely.