Has this ever happened to you? You have a widget that should update, but it just won’t. Learn how the StatefulWidget lifecycle works and why, depending on how you implement it, widgets sometimes don’t update their state.
What is a StatefulWidget?
A StatefulWidget is a widget that can “remember” values and update its appearance when those values change.
Unlike a StatelessWidget (which is immutable), a StatefulWidget stores internal information that can be modified over time, making the interface update automatically.
How does it work internally?
Flutter achieves this through a separation between the UI “description” and the “memory” of what happens in it:
- The widget (the class that extends
StatefulWidget) is like the blueprint: it describes what should be displayed. - The State (the class that extends
State<T>) is what lives longer and stores the data that changes.
When you change something inside the State and call setState(), Flutter rebuilds only the visual
part using the new value, but without destroying or recreating the entire State (unless the widget
completely changes its identity in the tree).
But how does Flutter achieve this internal “memory”? Through a well-defined lifecycle that coordinates when and how each widget updates. Let’s look at each stage.
StatefulWidget Lifecycle
A StatefulWidget goes through three main stages:
- Creation
- Update
- Destruction
Each one has specific lifecycle methods within the State class. Here’s the full flow at a glance:

1. Creation (initial mounting)
When Flutter creates the widget for the first time:
a. createState()
- Executes only once, when instantiating the widget.
- Creates the associated
Stateobject. - Here, there’s no access to
contextyet. - This
Statepersists as long as the widget remains in the same location in the tree.
b. initState()
- Executes once, right after the
Stateis created. - Here you already have access to the context (
context) and you can:- Initialize controllers (
AnimationController,ScrollController…) - Subscribe to streams or listeners.
- Call APIs that should only execute once.
- Initialize controllers (
Important: Never call
setState()here directly, unless it’s inside a future callback (for example, in aFuture.delayed).
c. didChangeDependencies()
- Called right after
initState(), and also every time context dependencies change (for example, anInheritedWidgetlikeThemeorLocalizations). - Ideal for reading things from the context that might change later, such as the current language or theme.
d. build()
- This is where you describe how your widget should look.
- Flutter can call it many times (every
setState(), dependency change, or parent update). - It should be pure and fast: no heavy logic, just declarative UI.
2. Update (when something changes)
e. didUpdateWidget(oldWidget)
- Called when the parent widget rebuilds and passes a new widget with the same “key” (
key). - Used to compare the previous configuration with the new one (
oldWidget) and react if something important changed.
When is the widget marked as “dirty”?
- If the parent passes new properties that affect the child → it’s marked dirty to redraw.
- If the parent changes but the child maintains the same configuration → it’s NOT marked dirty.
Practical example: If your widget receives a userId and it changes, here you could reload data.
f. setState()
- Not a formal part of the lifecycle, but it’s the way you trigger a widget rebuild.
- When you call it, Flutter marks the widget as “dirty”.
- This tells the rendering engine that this widget needs to be rebuilt on the next frame.
- Flutter then executes
build()again, “cleaning” the widget (updating the UI with the new data).
3. Destruction (unmounting)
g. deactivate()
- Called when the widget is removed from the tree, but could be reinserted (for example, when moving widgets in the tree).
- You almost never need it, except in advanced optimization cases.
h. dispose()
- Executes only once, right before the widget and its state are completely removed.
- Here you should:
- Cancel stream subscriptions.
- Release controllers or animations.
- Clean up any resources.
Lifecycle Summary
| Method | When it executes | What it’s used for |
|---|---|---|
createState() | Once when creating the widget | Creates the State instance |
initState() | Once when creating the State | Initializes resources (controllers, listeners) |
didChangeDependencies() | After initState() and when dependencies change | Reads data from context (theme, language) |
build() | Many times (every rebuild) | Describes the widget’s UI |
didUpdateWidget() | When parent widget props change | Compares previous vs new configuration |
setState() | When you call it | Marks the widget as “dirty” for rebuild |
deactivate() | When temporarily removed from tree | Rarely needed |
dispose() | When permanently removed | Cleans up resources (cancels listeners) |
Interactive Example: Why Is My Widget NOT Updating?
Want to see this in action? I prepared an interactive example that demonstrates exactly the problem.
Two widgets that receive the same userId from the parent:
- RED Widget (Broken): Does NOT implement
didUpdateWidget()→ Stays with the initial value - GREEN Widget (Fixed): DOES implement
didUpdateWidget()→ Updates correctly
Try the live example on DartPad
What to do in the example:
- Change the User ID in the input (for example: 123 → 456 → abc)
- Press “Update”
- Observe how:
- The red widget shows “OUT OF SYNC” (keeps the old ID internally)
- The green widget shows “IN SYNC” (updates with the new ID)
// Broken: caches userId once in initState, never reacts to updates.
class _BrokenState extends State<UserBadge> {
late String _cachedId;
@override
void initState() {
super.initState();
_cachedId = widget.userId; // set once — never again
}
@override
Widget build(BuildContext context) => Text('User: $_cachedId');
}
// Fixed: didUpdateWidget re-syncs when the parent passes a new userId.
class _FixedState extends State<UserBadge> {
late String _cachedId;
@override
void initState() {
super.initState();
_cachedId = widget.userId;
}
@override
void didUpdateWidget(covariant UserBadge oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.userId != widget.userId) {
setState(() => _cachedId = widget.userId);
}
}
@override
Widget build(BuildContext context) => Text('User: $_cachedId');
}
Key Difference: didUpdateWidget() vs didChangeDependencies()
| Method | Triggered when… | Depends on |
|---|---|---|
didUpdateWidget() | The parent rebuilds and changes the widget’s props | Direct properties |
didChangeDependencies() | An InheritedWidget that the context depends on changes | Environment (theme, language, media query, etc.) |
Next step: Open your Flutter project and add print() statements to each lifecycle method. Seeing
the execution order in real-time is the best way to internalize it.

