Stateful Widget Lifecycle

Each of us has used Flutter's Stateful Widgets. And this is one of the must topics I always ask at interviews from candidates to demonstrate deep knowledge on the topic. In most cases, this topic makes clear the degree of the engineer. I mean, if candidate shows off good knowledge on Stateful Widgets lifecyle and how it works internally, this is a good mark and we proceed with other mid-senior topics. So now question for you, ever wondered how Stateful Widgets lifecyle works internally, what each method is used for? Let's dedicate this article on them.
What is a Stateful Widget?
To create efficient and responsive apps, understanding the Stateful Widget lifecycle s crucial. This lifecycle governs how a widget transitions through different states and how Flutter manages these changes internally. A Stateful Widget in Flutter is a widget that has mutable state. Unlike Stateless Widgets, which remain unchanged once built, Stateful Widgets can rebuild themselves based on user interaction or other events.
Examples of when to use Stateful Widgets:
- Updating a counter when a button is pressed
- Managing animations
- Changing the UI based on user input
A Stateful Widget consists of two main classes:
- StatefulWidget: Represents the immutable widget.
- State: Holds the mutable state and logic for updating the UI.
Stateful Widget Lifecycle Methods
Flutter provides several lifecycle methods to help developers manage state transitions. Here’s a breakdown of each method and its role in the lifecycle:
1. constructor()
- Purpose: Called when a Stateful Widget is first instantiated.
- Function: Creates an instance of the
Stateful Widget
class.
class SomeWidget extends StatefulWidget {
const SomeWidget({super.key});
When a Stateful Widget class is instantianed, through updateChild -> inflateWidget Flutter internally creates StatefulElement, to manage this widget's location in the tree, where Stateful Widget's createState()
method is called.
abstract class StatefulWidget extends Widget {
const StatefulWidget({ super.key });
@override
StatefulElement createElement() => StatefulElement(this);
class StatefulElement extends ComponentElement {
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget) {
...
2. createState()
- Purpose: Called before Stateful Widget is inserted into the widget tree.
- Function: Creates an instance of the
State
class for the widget. - Typical Usage: Initializing the State object.
class SomeWidget extends StatefulWidget {
const SomeWidget({super.key});
@override
State<SomeWidget> createState() => _SomeWidgetState();
}
class _SomeWidgetState extends State<SomeWidget> {
...
}
3. State is mounted
After createState()
is called, State object is associated with corresponding StatefulElement (location in Element tree, but not yet inserted into it) and from that moment State object is considered to be mounted. The State object remains mounted until the framework calls dispose.
class StatefulElement extends ComponentElement {
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget) {
...
state._element = this;
...
assert(state._debugLifecycleState == _StateLifecycle.created);
}
To note, in debug, State object has 4 states which it can be in:
enum _StateLifecycle {
/// The [State] object has been created. [State.initState] is called at this
/// time.
created,
/// The [State.initState] method has been called but the [State] object is
/// not yet ready to build. [State.didChangeDependencies] is called at this time.
initialized,
/// The [State] object is ready to build and [State.dispose] has not yet been
/// called.
ready,
/// The [State.dispose] method has been called and the [State] object is
/// no longer able to build.
defunct,
}
and initially it is in _StateLifecycle.created
state.
4. initState()
- Purpose: Called once when the
State
object is created. - Function: Used for one-time initializations like setting up listeners, fetching data, or configuring animations.
- Important: Always call
super.initState()
at the start of this method.
After StatefulElement is created, mount()
method is called on the element.
Element inflateWidget(Widget newWidget, Object? newSlot) {
...
final Element newChild = newWidget.createElement();
...
newChild.mount(this, newSlot);
...
}
where element is inserted into Element Tree, consequently _firstBuild()
method of StatefulElement is called.
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
assert(_child == null);
assert(_lifecycleState == _ElementLifecycle.active);
_firstBuild();
assert(_child != null);
}
with the call of _firstBuild()
, State object's initState()
is called, followed by State object's state gets changed to initialized state in debug mode.
@override
void _firstBuild() {
assert(state._debugLifecycleState == _StateLifecycle.created);
final Object? debugCheckForReturnedFuture = state.initState() as dynamic;
...
assert(() {
state._debugLifecycleState = _StateLifecycle.initialized;
return true;
}());
...
}
@override
void initState() {
super.initState();
print("initState: Widget initialized");
}
5. didChangeDependencies()
- Purpose: Called when the widget's dependencies change.
- Function: Used when the widget depends on an InheritedWidget changes.
- Typical Usage: Reacting to changes in dependencies like localization or theme.
Right after initState()
, State object's didChangeDependencies()
is called, folllowed by State object's state gets changed to ready state in debug mode.
@override
void _firstBuild() {
assert(state._debugLifecycleState == _StateLifecycle.created);
final Object? debugCheckForReturnedFuture = state.initState() as dynamic;
...
assert(() {
state._debugLifecycleState = _StateLifecycle.initialized;
return true;
}());
state.didChangeDependencies();
assert(() {
state._debugLifecycleState = _StateLifecycle.ready;
return true;
}());
super._firstBuild();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("didChangeDependencies: Dependencies updated");
}
6. build()
- Purpose: Called every time the widget is rebuilt.
- Function: Defines the widget's UI and builds the widget tree.
- Typical Usage: Constructing and returning the widget’s visual structure.
As super._firstBuild()
method above is called, consequently rebuild()
and performBuild()
is called. ComponentElement overrides performBuild()
, where build()
method is called for both Stateless and StatefulWidgets.
@override
@pragma('vm:notify-debugger-on-exception')
void performRebuild() {
Widget? built;
try {
...
built = build();
...
}
...
}
When Element is constructing widget hierarchy, framework makes the element dirty, meaning ifsetState()
is already in progress, newsetState()
action is ignored.
After build()
method finishes constructing child hierarchy for ComponentElement, dirty flag is cleared, and constructed hierarchy is saved in _child Element object in ComponentElement.
@override
@pragma('vm:notify-debugger-on-exception')
void performRebuild() {
Widget? built;
try {
...
built = build();
...
} catch {
...
} finally {
super.performRebuild(); // clears the "dirty" flag
}
try {
_child = updateChild(_child, built, slot);
assert(_child != null);
}
...
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text("Hello, Flutter!")),
);
}
7. setState()
- Purpose: Used to update the widget's state and trigger a rebuild.
- Function: Not a lifecycle method itself, but central to Stateful Widgets. When called, Flutter marks the widget as "dirty," prompting a rebuild.
- Best Practices: Use sparingly to optimize performance.
void incrementCounter() {
setState(() {
counter++;
});
}
setState()
is itself big topic to talk about, you can read full steps done in the process in this article.
8. didUpdateWidget()
- Purpose: Called when the widget is rebuilt with new configuration data.
- Function: Used to respond to updates in the widget's properties.
- Typical Usage: Compare the old widget's properties with the new ones.
When configuration passed from parent changes, framework runs updateChild()
which calls StatefulElement's update()
method, in turn, it runs State object's state.didUpdateWidget(oldWidget)
passing the old widget, so that by overriding the didUpdateWidget()
engineer can add some logic reflecting the changes. Consequently, widget is rebuilt with the new data.
@override
void update(StatefulWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
final StatefulWidget oldWidget = state._widget!;
state._widget = widget as StatefulWidget;
final Object? debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget) as dynamic;
...
rebuild(force: true);
}
@override
void didUpdateWidget(covariant MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print("didUpdateWidget: Widget updated");
}
9. deactivate()
- Purpose: Called when the widget is removed from the widget tree temporarily.
- Function: Can be used to clean up resources but is not commonly used directly.
When State object is removed from Element Tree, this method is called.
@override
void deactivate() {
super.deactivate();
print("deactivate: Widget removed from tree, but can be re-inserted");
}
10. activate()
- Purpose: Called when the widget is removed from the widget tree temporarily.
- Function: Can be used to link resources again untied in deactivate method, but is not commonly used directly.
The removed state can be re-inserted into the tree in the other part if GlobalKey is provided to the widget. It helps to improve performance by not re-building whole child hiearchy.
@override
void activate() {
super.activate();
print("activate: Widget is re-inserted");
}
11. dispose()
- Purpose: Called when the widget is permanently removed from the widget tree.
- Function: Used for cleanup tasks, such as canceling timers, removing listeners, or closing streams.
- Important: Always call
super.dispose()
at the end of this method.
@override
void dispose() {
print("dispose: Cleaning up resources");
super.dispose();
}
Visualizing the Stateful Widget Lifecycle
Here’s a simplified flow:
constructor()
callcreateState()
- State is mounted
initState()
didChangeDependencies()
(optional)build()
setState()
(if state changes)didUpdateWidget()
(if updated by parent)deactivate()
(if removed temporarily)activate()
(if re-inserted)dispose()
(if removed permanently)
Common Mistakes to Avoid
- Not calling
super
methods: Always invokesuper
in lifecycle methods likeinitState()
,didChangeDependencies()
, anddispose()
. - Frequent
setState()
calls: OverusingsetState()
can lead to performance issues. Optimize where possible. - Leaking resources: Always clean up listeners, streams, or controllers in the
dispose()
method.
Conclusion
Understanding the Stateful Widget lifecycle is essential for building efficient, maintainable, and dynamic Flutter applications. By leveraging the lifecycle methods effectively, developers can create responsive and well-optimized apps that deliver a seamless user experience.
References: