Flutter: Why Doesn't setState Update My UI?
1. "I Added an Item, But Nothing Happened."
This is the most common mistake I made as a beginner. I added an item to a list and expected the screen to update.
// ❌ WRONG
List<String> items = ['A', 'B'];
void addItem() {
setState(() {
items.add('C'); // Mutating the list in place
});
}
My logic:
- 'C' is added to
items. setStateis called.- During rebuild,
itemsshould have 3 elements. - The ListView should show 'C'.
Reality: Nothing happened. (Or in complex optimization scenarios, it became a ghost bug that updated sporadically.)
2. The Principle: Reference vs. Equality
Modern UI frameworks like Flutter and React constantly check "Did it change?" for efficiency. And their primary method of checking is "Is it the same memory address?" (Reference Equality).
In the code above, the List Object (the room in memory) that items points to DID NOT change.
Only the occupants (data) inside that room changed.
If you are using smart widgets (e.g., Selector, Riverpod, or optimized List widgets), Flutter thinks:
"Huh? The memory address of items is the same as before. It must be unchanged. I'll skip the rebuild."
This is the Trap of Mutable Objects.
3. The Solution: Swap with a New Object (Immutability)
The only way to scream "It Valid!" to Flutter is to Create a completely NEW list and assign it.
// ✅ CORRECT
void addItem() {
setState(() {
// 1. Create a new list by copying (Spread Operator)
items = [...items, 'C'];
});
}
Now items points to a New Memory Address.
Flutter notices: "Oh? The address changed! The content must be different! I must rebuild!"
This is the coding style of respecting Immutability.
4. Deep Dive: Silence in Provider/Riverpod
This is even more critical in State Management libraries.
// Riverpod Example
final listProvider = StateProvider<List<String>>((ref) => []);
// ❌ Bad
ref.read(listProvider).add('New Item');
// Provider never gets a notification that value changed.
Provider/Riverpod triggers notifications to subscribers (Widgets) ONLY when the Assignment Operator (=) runs, like state = something.
Calling internal methods like .add() or .remove() does nothing to trigger updates.
// ✅ Good
final oldList = ref.read(listProvider);
ref.read(listProvider.notifier).state = [...oldList, 'New Item'];
5. ValueNotifier & ChangeNotifier
setState rebuilds the whole widget.
For better performance, use ValueNotifier.
final counter = ValueNotifier<int>(0);
counter.value = 1; // Triggers listeners
But beware: ValueNotifier<List> has the same trap.
list.value.add(1) DOES NOT trigger listeners.
You must do list.value = [...list.value, 1].
6. The const Constructor Trap
If you use const on a widget constructor, Flutter assumes it never changes.
const MyListWidget(items: items); // ❌
Even if items variable changes, const tells Flutter "Don't bother checking me, I'm static."
Remove const if the widget depends on dynamic data.
7. Deep Dive: Surgical Precision with Selector
When using Provider, context.watch<MyModel>() rebuilds the widget when anything in the model changes.
Changed "Name"? The widget showing "Age" and "Address" also rebuilds. Wasteful.
Use Selector to rebuild ONLY when specific data changes.
Selector<UserProvider, String>(
selector: (context, provider) => provider.name, // Watch ONLY "name"
builder: (context, name, child) {
return Text(name); // Rebuilds only when name reference changes
// Changing "age" is ignored.
},
)
This pairs perfectly with Immutability.
Since it checks if the Reference of provider.name changed, you MUST return a new object/string for it to work.
8. Case Study: Cooking with Freezed
"Creating new objects via copy is tedious. Writing copyWith is error-prone."
That's why we use the freezed package.
Before:
class User {
final String name;
final int age;
// Manually implementing copyWith... (Painful)
User copyWith(...) { ... }
}
After:
@freezed
class User with _$User {
factory User(String name, int age) = _User;
}
// Usage
state = state.copyWith(age: 20); // Auto-generated magic!
freezed enforces immutability and overrides == (Equality) operator automatically.
It prevents 99% of "UI Not Updating" bugs at compile time. It's industry standard.
9. Case Study: The "Add to Cart" Bug
I built a Cart using ChangeNotifier.
class Cart extends ChangeNotifier {
final List<Item> _items = [];
void add(Item item) {
_items.add(item);
// I forgot notifyListeners();
}
}
User clicked "Add". Cart icon stayed at "0". User navigated to another tab. Cart icon updated to "1". This is "State Changed, but UI didn't know."
In manual state management (ChangeNotifier), you are responsible for calling notifyListeners().
Modern libraries (Riverpod, Bloc) force you to use Immutability (state = new), effectively preventing this bug.
10. Architecture: State Management Showdown (Provider vs Riverpod vs Bloc)
So setState isn't cutting it anymore. What should you learn next?
- Provider: The classic choice. Recommended by Google, but relies heavily on
contextand can be tricky withGlobalKeys. - Riverpod (Recommended): Created by the author of Provider to fix Provider's flaws. No
contextdependency, compile-time safety, and testable. It's the modern standard. - Bloc: The Enterprise choice. Segregates Events and States strictly. Verbose boilerplate, but unbeatable for long-term maintenance in large teams.
11. FAQ: "Why do you hate GetX?"
GetX is popular for its simplicity (Obx, Get.to), but seasoned developers avoid it.
- Anti-Pattern: It bypasses Flutter's core Widget Tree architecture.
- Vendor Lock-in: It replaces standard Navigation, Dialogs, and State Management with its own non-standard methods.
- Maintenance Risk: It's a massive framework maintained by a single person.
For a side project? Fine. For a career or production app? Learn Riverpod or Bloc.
12. Refactoring Challenge: Mutable to Immutable
Challenge:
You have List<int> numbers = [1, 2, 3];.
Create a NEW List with 4 added, WITHOUT using .add().
Answer:
final newNumbers = [...numbers, 4];
This single habit will increase your salary.
13. Summary
"Screen not updating" means "Flutter doesn't know it changed."
- Don't Mutate.
- Copy & Replace.
- Make Spread Operator (
[...]) your best friend. - Use
notifyListenersif using ChangeNotifier.
Stick to Immutability, and you will escape the swamp of update bugs.