StatefulWidget in Flutter: Why Your Widget Isn't Updating and How to Fix It

You have a widget that should update, but it just won't. Learn how the StatefulWidget lifecycle works — and why yours sometimes won't rebuild.

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:

  1. Creation
  2. Update
  3. Destruction

Each one has specific lifecycle methods within the State class. Here’s the full flow at a glance:

Flutter StatefulWidget lifecycle diagram

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 State object.
  • Here, there’s no access to context yet.
  • This State persists as long as the widget remains in the same location in the tree.

b. initState()

  • Executes once, right after the State is 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.

Important: Never call setState() here directly, unless it’s inside a future callback (for example, in a Future.delayed).

c. didChangeDependencies()

  • Called right after initState(), and also every time context dependencies change (for example, an InheritedWidget like Theme or Localizations).
  • 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

MethodWhen it executesWhat it’s used for
createState()Once when creating the widgetCreates the State instance
initState()Once when creating the StateInitializes resources (controllers, listeners)
didChangeDependencies()After initState() and when dependencies changeReads data from context (theme, language)
build()Many times (every rebuild)Describes the widget’s UI
didUpdateWidget()When parent widget props changeCompares previous vs new configuration
setState()When you call itMarks the widget as “dirty” for rebuild
deactivate()When temporarily removed from treeRarely needed
dispose()When permanently removedCleans 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:

  1. Change the User ID in the input (for example: 123 → 456 → abc)
  2. Press “Update”
  3. 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()

MethodTriggered when…Depends on
didUpdateWidget()The parent rebuilds and changes the widget’s propsDirect properties
didChangeDependencies()An InheritedWidget that the context depends on changesEnvironment (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.

Language · Idioma

English Español