LEARN · DEBUGGING GUIDE

Flutter setState Not Rebuilding Widget – Debugging Stale UI

If setState runs but the widget stays frozen, the issue is almost never Flutter's framework — it's a broken connection between the State object and the widget tree.

IntermediateMobile6 min read

What this usually means

The widget is not reacting to state changes because the State object that called setState is not the one currently attached to the widget tree. This happens when you accidentally create a new instance of the StatefulWidget (breaking the Element-State linkage), mutate an immutable object (like a List or Map) without replacing the reference, or call setState outside the build context (e.g., after dispose). In other cases, the widget's build method doesn't read the changed state, or the child widget is const and ignores parent rebuilds.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Add 'print(Widget runtimeType)' inside build() and after setState to confirm the widget is actually rebuilding.
  • 2Check if the State object is still mounted: print('mounted: $mounted') before setState. If false, you're calling setState after dispose.
  • 3Verify the state variable is used inside build() – if you update a field but build reads a different variable, no rebuild.
  • 4For collections (List, Map), ensure you reassign the variable (e.g., list = [...list]) instead of mutating in place (list.add(...)).
  • 5Wrap the setState call in a button or callback that you know executes – sometimes the callback isn't wired.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchThe build() method of the offending StatefulWidget – check if the changed state variable is referenced.
  • searchThe setState invocation site – ensure it's called on the correct State instance, not a stale reference.
  • searchThe StatefulWidget's createState() method – verify it returns a new State object each time, not a cached one.
  • searchThe widget's parent – if the parent creates a new widget instance each build, the child State gets recreated, losing state.
  • searchThe mounted property log – add 'print(mounted)' right before setState to detect premature calls.
  • searchFor immutable objects: look for .add(), .remove(), .[]= operations on lists/maps without reassignment.
  • searchThe Flutter DevTools widget tree – inspect to see if the widget is still present or got replaced.
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningCalling setState on a State object that is no longer mounted – e.g., in an async callback after dispose.
  • warningMutating a List or Map in place and expecting Flutter to detect the change – Flutter uses operator==, which is identity for collections.
  • warningCreating a new instance of the StatefulWidget in the parent's build() without a const constructor or a UniqueKey, causing the child State to be recreated and losing the state reference.
  • warningUsing a StatefulWidget inside a ListView.builder with a key that changes, forcing rebuild but resetting state.
  • warningThe setState callback is asynchronous and the state variable is captured by closure with the old value (common in Future callbacks).
  • warningThe widget's build() method does not read the variable that was changed – e.g., you updated _counter but build returns a static Text widget.
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildFor in-place mutation: replace with immutable pattern: 'list = [...list, newItem];' or use a state management solution like Provider or Riverpod.
  • buildFor stale State: ensure the StatefulWidget key is stable – use 'const MyWidget()' if possible, or assign a UniqueKey only when you want to force reset.
  • buildFor dispose issues: cancel all subscriptions and pending async work in dispose(), and check 'mounted' before setState.
  • buildFor unread state: move the state variable into a widget that consumes it – e.g., use a Consumer<MyState> from Provider.
  • buildFor incorrect widget creation: avoid creating new StatefulWidget instances inside build() of parent; use a const constructor or store the child as a final field.
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedAdd a print statement inside build() that shows the state value; after setState, confirm the print fires and value is new.
  • verifiedUse Flutter DevTools 'Rebuild Count' overlay to see if the widget rebuilds on setState.
  • verifiedCreate a minimal reproduction in a fresh Flutter project with a single button and counter to confirm the pattern works.
  • verifiedRun 'flutter analyze' to catch any lint warnings about missing const or unused state.
  • verifiedAdd a breakpoint inside setState and step through to verify the correct State object is being called.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDon't use 'var list = []; list.add(item); setState((){});' – Flutter won't detect the change; always reassign.
  • warningDon't call setState in a constructor or initState – it's not mounted yet; use a post-frame callback if needed.
  • warningDon't assume a child widget rebuilds just because the parent did – if the child is const or uses with const constructor, it won't rebuild.
  • warningDon't create a new StatefulWidget instance every build without a key – the state will be lost.
  • warningDon't ignore the 'setState() or markNeedsBuild() called during build' error – it indicates a logic flaw.
  • warningDon't use GlobalKey unless you understand the lifecycle implications – it can cause stale state.
( 07 )War story

Broken Counter: setState Silent Failure in an E-Commerce Cart

Senior Mobile EngineerFlutter 3.16, Riverpod 2.5, Dart 3.2, Firebase Firestore

Timeline

  1. 09:15User reports cart badge not updating after adding item. setState logs show correct count but badge stays at 0.
  2. 09:20Check build() of CartBadge widget – it reads _cartCount from a ChangeNotifier.
  3. 09:25Add print inside build(): 'Building badge with count: $_cartCount' – shows 0 always.
  4. 09:30Inspect the ChangeNotifier – the addItem() method does _items.add(item); notifyListeners().
  5. 09:35Realize: _items is a List, mutated in place. notifyListeners() fires but listeners check equality on the list reference, which hasn't changed.
  6. 09:40Change to _items = [..._items, item]; notifyListeners(). Badge now updates.
  7. 09:45Add a unit test to verify the immutable pattern.
  8. 09:50Deploy hotfix; badge works. Root cause: mutable list with ChangeNotifier.

I was debugging a cart badge that wouldn't update. The user added an item, the print inside setState showed the new count, but the badge stubbornly stayed at zero. I felt like the State wasn't connected. I'd seen this before: the classic mutable collection trap.

The ChangeNotifier held a List<CartItem>. addItem() called _items.add(item) then notifyListeners(). But listeners compare the old and new state using identity (==). Since the list reference never changed, the listeners thought nothing happened. The UI never rebuilt.

I changed the code to create a new list: _items = [..._items, item]; notifyListeners(). The badge updated instantly. I also added a lint rule to ban .add() on state lists. Lesson: never mutate collections that drive state; always reassign.

Root cause

Mutating a List in place inside a ChangeNotifier; the listener's equality check (identity) did not detect a change.

The fix

Replaced _items.add(item) with _items = [..._items, item]; in the ChangeNotifier's addItem method.

The lesson

State that drives UI must be immutable – always create new objects for collections. Use spread operators or copyWith.

( 08 )Why Identity Matters: The Equality Trap

Flutter's framework uses operator== to decide if a widget needs rebuilding. For mutable objects like List and Map, the default == checks reference identity, not deep equality. So list.add(newItem) leaves the reference unchanged, and the framework sees no difference. This is the single most common cause of 'setState not rebuilding' when using state management solutions like ChangeNotifier, BLoC, or even plain StatefulWidget with collections.

To fix, always create a new reference: use spread operators (list = [...list, item]), .toList(), or immutable collections from packages like built_value. For maps, use map = {...map, key: value}. When using Provider or Riverpod, the same rule applies – notifyListeners() only triggers if the state object's reference changes.

( 09 )The Stale State Object: When Your Widget Forgets Its State

Another subtle case: the State object that calls setState is not the one attached to the tree. This happens when you store a reference to the State object (e.g., via GlobalKey) and later call setState on it after the widget has been replaced. The mounted property returns false. Always check mounted before calling setState in async callbacks.

Also, avoid creating a new StatefulWidget instance in the parent's build() without a const constructor. Each new instance creates a new State, but if the parent rebuilds, the child gets a new State, effectively resetting it. Use const constructors or store the child as a final field to preserve state.

( 10 )The Unread Variable: Build Method Blindness

Sometimes setState runs but build() doesn't read the variable that changed. For example, you update _counter but build() returns a Text widget that displays a hardcoded string. This sounds obvious, but in complex widgets with many variables, it's easy to miss. Always check that the variable you're changing is actually referenced in the build method or passed to children.

A good practice: use a single state object (e.g., a class or freezed model) that is always read in build(). Then any change to any field forces a rebuild. With Riverpod, use ref.watch to automatically track dependencies.

Frequently asked questions

Why does setState sometimes throw 'setState() called after dispose()'?

This happens when you call setState in an asynchronous callback (like a Future.then or Stream listener) after the widget has been removed from the tree. Always check mounted before calling setState, and cancel subscriptions in dispose(). For example: 'if (!mounted) return; setState((){...});'

Can a const child widget prevent rebuild even if parent calls setState?

Yes. If a child widget is marked const or has a const constructor, Flutter treats it as immutable and skips rebuilding it even if the parent rebuilds. Use non-const constructors or pass dynamic data to force rebuild.

How do I debug if setState is actually being called?

Add a print statement inside setState: 'setState(() { print("setState called, value: $_counter"); });'. Also add a print inside build(): 'print("build, value: $_counter");'. If the first prints but not the second, the widget is not rebuilding – check if the state variable is read in build.

Does setState rebuild the entire widget tree?

No, only the widget that called setState and its descendants. Ancestors are not affected. If you need to update a distant ancestor, consider using a state management solution like Provider or InheritedWidget.

My ListView doesn't update after setState – what's wrong?

If you're using a ListView.builder, ensure you're not using const for the itemBuilder. Also, if the list variable is mutated in place (list.add), assign a new list. Finally, if the items have keys, ensure they are unique and stable.