ObservableList / ObservableMap not rebuild ui after mutate property
ChatItem is class without the Store , I attempt to mutate propery in ObservableList and ObservableMap but tracking changes not working. I tried with chatStore.chats.toList() and without toList(), and add observer inside itemBuilder, but nothing happend. UI updating only if i change the route or reassing whole chatsMap / chats. Please can u help me with this simple task.
I also extend ChatItem class with equatable package to override hashCode and operator == I use .g generator
mobx: ^2.4.0 flutter_mobx: ^2.2.1+1 mobx_codegen: ^2.6.2
class ChatsStore = _ChatsStore with _$ChatsStore;
abstract class _ChatsStore with Store {
@MakeObservable(useDeepEquality: true)
ObservableList<ChatItem> chats = ObservableList.of([]);
@MakeObservable(useDeepEquality: true)
ObservableMap<String, ChatItem> chatsMap = ObservableMap<String, ChatItem>.of({});
@computed
ObservableList<ChatItem> get computedChatList => ObservableList.of(chatsMap.value);
@action
Future<void> getChats() async {
List<ChatItem>? chatList = await getChatsList();
if (chatList == null) return;
// try List
chats.clear();
chats.addAll(chatList);
// try Map
chatsMap.clear();
for (var chat in chatList) {
chatsMap[chat.id] = chat;
}
}
/// I tried to update property inside **ChatItem**
@action
updateUnread(String id) {
print('hash before: ${ chatsMap[id].hashCode}');
chatsMap[id].unreadCount = 99;
print('hash after: ${ chatsMap[id].hashCode}'); // hash changed
var found = chats.firstWhere((el) => el.id == id);
found.unreadCount = 98;
}
}
Widget
Column(children: [
Expanded(
child: Observer(builder: (_) {
var chatList = chatStore.chats.toList();
print('REBUILD');
return RefreshIndicator(
onRefresh: handleRefresh,
color: Colors.white,
backgroundColor: Colors.blue,
child: ListView.separated(
separatorBuilder: (_, int index) => const DididerWmKeyVal(),
itemCount: chatList.length,
itemBuilder: (_, int index) {
var element = chatList[index];
return InkWell(
child: SizedBox(
height: 70,
child: Row(
children: [
Container(
padding: EdgeInsets.only(top: 10),
alignment: Alignment.topCenter,
width: 60,
child: Stack(
children: [
AvatarWidget(wmid: element.id),
BadgeWidget(count: element.unreadCount)
],
),
),
Expanded(
child: Padding(
padding: EdgeInsets.only(top: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(element.name),
Text('${element.hint}'.trim()),
],
),
),
),
ListItemTime(time: element.time),
],
),
),
onTap: () {
chatStore.updateUnread("78000000");
},
));
}),
),
]),
I've been having the same issue while using ObservableList on the versions below:
mobx: 2.5.0 flutter_mobx: 2.3.0 mobx_codegen: 2.7.0
@Poloten @CaLouro The issue you're experiencing is that MobX doesn't automatically track changes to properties inside objects within ObservableList or ObservableMap. When you modify ChatItem properties, MobX doesn't detect these deep changes because ChatItem itself isn't observable.
https://mobx.netlify.app/guides/when-does-mobx-react
Solution 1: Make ChatItem Observable
Convert your ChatItem class to a MobX store:
import 'package:mobx/mobx.dart';
import 'package:equatable/equatable.dart';
part 'chat_item.g.dart';
class ChatItem = _ChatItem with _$ChatItem;
abstract class _ChatItem with Store implements EquatableMixin {
_ChatItem({
required this.id,
required this.name,
required this.hint,
required this.time,
this.unreadCount = 0,
});
final String id;
final String name;
final String hint;
final String time;
@observable
int unreadCount;
// Equatable implementation
@override
List<Object?> get props => [id, name, hint, time, unreadCount];
@override
bool get stringify => true;
}
Solution 2: Make ChatItem Immutable
Immutable ChatItem with Replacement Strategy
Benefits of Immutable Approach:
- Predictable State: No accidental mutations
- Better Testing: Easier to test and reason about
- MobX Compatibility: MobX detects changes when you replace objects
- Debugging: Easier to track state changes
import 'package:equatable/equatable.dart';
class ChatItem extends Equatable {
const ChatItem({
required this.id,
required this.name,
required this.hint,
required this.time,
this.unreadCount = 0,
});
final String id;
final String name;
final String hint;
final String time;
final int unreadCount;
// copyWith method for creating modified copies
ChatItem copyWith({
String? id,
String? name,
String? hint,
String? time,
int? unreadCount,
}) {
return ChatItem(
id: id ?? this.id,
name: name ?? this.name,
hint: hint ?? this.hint,
time: time ?? this.time,
unreadCount: unreadCount ?? this.unreadCount,
);
}
@override
List<Object?> get props => [id, name, hint, time, unreadCount];
@override
bool get stringify => true;
}
import 'package:mobx/mobx.dart';
part 'chats_store.g.dart';
class ChatsStore = _ChatsStore with _$ChatsStore;
abstract class _ChatsStore with Store {
@observable
ObservableList<ChatItem> chats = ObservableList.of([]);
@observable
ObservableMap<String, ChatItem> chatsMap = ObservableMap<String, ChatItem>.of({});
@computed
ObservableList<ChatItem> get computedChatList =>
ObservableList.of(chatsMap.values);
@action
Future<void> getChats() async {
List<ChatItem>? chatList = await getChatsList();
if (chatList == null) return;
chats.clear();
chats.addAll(chatList);
chatsMap.clear();
for (var chat in chatList) {
chatsMap[chat.id] = chat;
}
}
@action
void updateUnread(String id, int newCount) {
// Update in map
final currentChat = chatsMap[id];
if (currentChat != null) {
final updatedChat = currentChat.copyWith(unreadCount: newCount);
chatsMap[id] = updatedChat;
}
// Update in list
final index = chats.indexWhere((chat) => chat.id == id);
if (index != -1) {
final updatedChat = chats[index].copyWith(unreadCount: newCount);
chats[index] = updatedChat;
}
}
}
The reason MobX behaves this way is due to its reactivity model and how it tracks observability. Here's why:
1. Shallow Observation by Default
MobX observes references to objects in collections, not the internal properties of those objects. When you have:
@observable
ObservableList<ChatItem> chats = ObservableList.of([]);
MobX tracks:
- ✅ Adding/removing items from the list
- ✅ Replacing items in the list
- ❌ Changes to properties inside the ChatItem objects
2. Reference-Based Change Detection
MobX uses reference equality to detect changes. When you mutate a property inside an object:
chatsMap[id].unreadCount = 99; // Same object reference
The object reference stays the same, so MobX doesn't detect any change. But when you replace the entire object:
chatsMap[id] = newChatItem; // Different object reference
MobX detects this as a change because the reference changed.
3. Performance Optimization
This design is intentional for performance reasons:
- Deep observation would require MobX to recursively watch every property of every object
- This would create many more observers and significantly impact performance
- It would also create complex dependency graphs
4. Explicit vs Implicit Reactivity
MobX follows the principle of explicit reactivity:
- You must explicitly mark what should be observable
- If you want object properties to be reactive, you need to make the object itself observable
- This prevents accidental performance issues from over-observation
5. Memory and Garbage Collection
Deep observation would create strong references to all nested objects, potentially preventing garbage collection and causing memory leaks.
6. Architectural Philosophy
This behavior encourages better architectural patterns:
- Immutable data structures (which you're now using)
- Single source of truth for state
- Predictable state changes through actions
The Two Solutions:
- Make objects observable:
ChatItembecomes a MobX store - Use immutable objects: Replace entire objects when changes occur
Both solutions work because they align with MobX's reference-based change detection system. The immutable approach is often preferred because it's more predictable and prevents accidental mutations.
@amondnet thanks for the very informational reply.
I will double-check my code to see what exactly I was doing. But I as far as I remember I was not changing only deep objects properties and was in fact changing the list itself. But then again, I will double-check and come back with that info.
Is the ObservableList behaving normally everywhere else for you?
@Poloten @CaLouro
You'll also need to modify the Observer-wrapped Widgets.
I haven't run the code, but it looks like the current code only rebuilds the entire ListView when the entire list changes(var chatList = chatStore.chats.toList()), or when the length of the list changes(chatStore.chats.length).
ListView.builder(
itemCount: chatStore.chats.length,
itemBuilder: (context, index) {
return Observer( // Wrap each item with Observer
builder: (_) {
final chatItem = chatStore.chats[index];
return ListTile(
title: Text(chatItem.title),
subtitle: Text(chatItem.content),
// ... other properties
);
},
);
},
)
https://mobx.netlify.app/api/observers#observer-widget
Immediate context for
builderThe key thing to note is that the
builderfunction will only track the observables in its immediate execution context. If an observable is being used in a nested function, it will not be tracked. Make sure to dereference (a.k.a. read) an observable in the immediate execution context. If no observables are found when running thebuilderfunction, it will warn you on the console.This is one of the most common gotchas when using MobX. Just because you have nested functions inside a
builder, which are reading an observable, it does not actually track the observable. It can appear deceiving but the fact is, the observable was not in the immediate execution context. Be watchful for this scenario, especially when yourObserver-wrappedWidgetsare not updating properly.
https://mobx.netlify.app/api/observable#observablelist
https://github.com/mobxjs/mobx.dart/issues/402#issuecomment-576685334 https://github.com/mobxjs/mobx.dart/blob/a550bbf25567abdbfc6b57b9f9a92565e86933f6/mobx_examples/lib/todos/todo_widgets.dart#L56 https://github.com/mobxjs/mobx.dart/issues/527