Repository: mobxjs/mobx.dart

Issue:
ObservableMap not notifying Observer mobx 2.2.3 it was woking fine in 2.2.1
I am using so many ObservableMaps in the app and none of them are updating the UI since I upgraded to 2.2.3

` @observable
  ObservableMap<String, ParticipantExtended> sortedParticipants =
      ObservableMap<String, ParticipantExtended>();`      
      
```
Observer(
                          builder: (_) {
                            return SliverGrid(
                              gridDelegate:
                                  SliverGridDelegateWithFixedCrossAxisCount(
                                crossAxisCount: crossAxisCount,
                                childAspectRatio: tileWidth / tileHeight,
                                crossAxisSpacing: 2,
                                mainAxisSpacing: 2,
                              ),
                              delegate: SliverChildBuilderDelegate(
                                (context, index) {
                                  final participant = videoRoomStore
                                      .sortedParticipants.values
                                      .elementAt(index);
                                 return VisibilityDetector(
                                    key: Key(participant.identity),
                                    onVisibilityChanged: (info) {
                                      final bool isVisible =
                                          info.visibleFraction > 0;
                                      final bool isCompletelyGone =
                                          info.visibleFraction == 0;
                                      final bool isSubscribed = videoRoomStore
                                                  .participantSubscriptions[
                                              participant.identity] ??
                                          false;
                                      final bool shouldSubscribe =
                                          !isSubscribed &&
                                              isVisible &&
                                              !participant.isLocalParticipant;

                                      if (shouldSubscribe) {
                                        videoRoomStore.subscribeToVideoTracks(
                                          participant.participant
                                              as RemoteParticipant,
                                        );
                                        videoRoomStore.visibleParticipants[
                                            participant.identity] = true;
                                      } else if (!participant
                                              .isLocalParticipant &&
                                          isCompletelyGone) {
                                        videoRoomStore.unSubscribeToVideoTracks(
                                          participant.participant
                                              as RemoteParticipant,
                                        );
                                        videoRoomStore.visibleParticipants
                                            .remove(
                                          participant.identity,
                                        );
                                      }
                                    },
                                    child: ParticipantTile(
                                      participant: participant,
                                    ),
                                  );
                                },
                                childCount: videoRoomStore.isAppInForeground
                                    ? videoRoomStore.sortedParticipants.length
                                    : 0,
                              ),
                            );
                          },
                        ),
```

What change I need to make to keep it working as earlier it used to work?                                  


Discussion: @boradparas
The issue may be related to this [PR](https://github.com/mobxjs/mobx.dart/pull/951). Since 2.2.3, `mobx` checks for equality of map, iterable with [`DeepCollectionEquality`](https://api.flutter.dev/flutter/package-collection_collection/DeepCollectionEquality-class.html).

Could you try one of the following?

1. use `@alwaysNotify` instead of `@observable`. 
    ```dart
     @alwaysNotify 
     ObservableMap<String, ParticipantExtended> sortedParticipants = ObservableMap<String, ParticipantExtended>();
    ```
2.  override `==` and `hashCode` in `ParticipantExtended`. Avoid unnecessary observable notifications of `Iterable` or `Map` fields.
Same for ObservableList etc.

How can I notify observer now, when changing attrs on objects within list or map? Equality checks only addresses in memory, but not theirs contents...

Is this tool only suitable for simple lists of strings and numbers? Previously, it was possible to work with objects.
@alexggordon 

> How can I notify observer now, when changing attrs on objects within list or map? Equality checks only addresses in memory, but not theirs contents...

Can you please provide reproducible code? Then I can tell you how to do it. Alternatively, see the [documentation](https://mobx.netlify.app/api/observable#observablelist).

Could you try one of the following?

1. use `@alwaysNotify`.
2. override `==` and `hashCode` in Object
3. use immutable data class


https://github.com/mobxjs/mobx.dart/issues/851

https://github.com/mobxjs/mobx.dart/pull/970
@amondnet 

> Could you try one of the following?
> 
> 1. use `@alwaysNotify`.

I tried it. Without changes. Perhaps I didn’t fully understand how to apply it correctly.

> 2. override `==` and `hashCode` in Object
> 3. use immutable data class

Looks like a suitable solution. I'll try this approach.
Thank you very much!


Fix patch:
diff --git a/docs/docs/api/observable.mdx b/docs/docs/api/observable.mdx
index 78f00361..9ff43e05 100644
--- a/docs/docs/api/observable.mdx
+++ b/docs/docs/api/observable.mdx
@@ -162,6 +162,65 @@ though, you're allowed to use of computed getters the same way you do with
 > just doesn't make sense otherwise. But don't worry, if by any chance you
 > happens to forget, we warn you with friendly errors at code generation time.
 
+## Use deep equality on collections
+
+By default, MobX uses the `==` to compare the previous value. This is fine for
+primitives, but for collections, you may want to use a [DeepCollectionEquality]
+(https://api.flutter.dev/flutter/package-collection_collection/DeepCollectionEquality-class.html). When
+using deep equal, no reaction will occur if all elements are equal.
+
+```dart
+import 'package:mobx/mobx.dart';
+
+part 'todo.g.dart';
+
+class Todo = _Todo with _$Todo;
+
+abstract class _Todo with Store {
+  _Todo(this.description);
+
+  @observable
+  String description = '';
+
+  @observable
+  bool done = false;
+
+  @action
+  void markDone(bool flag) {
+    done = flag;
+  }
+
+  @override
+  int get hashCode => description.hashCode ^ done.hashCode;
+
+  @override
+  operator ==(Object other) =>
+      identical(this, other) ||
+      other is Todo &&
+          runtimeType == other.runtimeType &&
+          description == other.description &&
+          done == other.done;
+}
+
+class Todos = _Todos with _$Todos;
+
+abstract class _Todos with Store {
+  _Todos();
+
+  @MakeObservable(useDeepEquals: true)
+  List<Todo> _todos = [];
+
+  @computed
+  List get todos => _todos;
+
+  @action
+  void setTodos(List<Todo> todos) {
+    _todos = todos;
+  }
+}
+```
+
+
 ## Computed
 
 #### `Computed(T Function() fn, {String name, ReactiveContext context})`
diff --git a/mobx/CHANGELOG.md b/mobx/CHANGELOG.md
index 1a2639f0..d6e1d0d0 100644
--- a/mobx/CHANGELOG.md
+++ b/mobx/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 2.2.3+1
+
+Make the change in 2.2.3 optional. If you want the use this behavior , modify `@observable` to 
+`@MakeObservable(useDeepEquality: true)`.
+
+- Adds `useDeepEquality` for creating observables by [@amondnet](https://github.com/amondnet)
+
 ## 2.2.3
 
 - Avoid unnecessary observable notifications of `@observable` `Iterable` or `Map` fields of Stores by [@amondnet](https://github.com/amondnet)  in [#951](https://github.com/mobxjs/mobx.dart/pull/951)
diff --git a/mobx/lib/src/api/annotations.dart b/mobx/lib/src/api/annotations.dart
index 602ce7a3..4d8d017d 100644
--- a/mobx/lib/src/api/annotations.dart
+++ b/mobx/lib/src/api/annotations.dart
@@ -25,7 +25,7 @@ class StoreConfig {
 /// String withEquals = 'world';
 /// ```
 class MakeObservable {
-  const MakeObservable({this.readOnly = false, this.equals});
+  const MakeObservable({this.readOnly = false, this.equals, this.useDeepEquality = true});
 
   final bool readOnly;
   /// A [Function] to use check whether the value of an observable has changed.
@@ -38,6 +38,11 @@ class MakeObservable {
   /// If no function is provided, the default behavior is to only trigger if
   /// : `oldValue != newValue`.
   final Function? equals;
+
+  /// By default, MobX uses the `==` to compare the previous value. This is fine for
+  /// primitives, but for Iterable and Map, you may want to use a deep equality on collections. When
+  /// using deep equal, no reaction will occur if all elements are equal.
+  final bool useDeepEquality;
 }
 
 bool observableAlwaysNotEqual(_, __) => false;
diff --git a/mobx/lib/src/core/atom_extensions.dart b/mobx/lib/src/core/atom_extensions.dart
index 3dfb5aee..50756717 100644
--- a/mobx/lib/src/core/atom_extensions.dart
+++ b/mobx/lib/src/core/atom_extensions.dart
@@ -9,8 +9,9 @@ extension AtomSpyReporter on Atom {
   }
 
   void reportWrite<T>(T newValue, T oldValue, void Function() setNewValue,
-      {EqualityComparer<T>? equals}) {
-    final areEqual = equals ?? equatable;
+      {EqualityComparer<T>? equals, bool? useDeepEquality}) {
+    final areEqual = equals ??
+        (a, b) => equatable(a, b, useDeepEquality: useDeepEquality ?? false);
 
     // Avoid unnecessary observable notifications of @observable fields of Stores
     if (areEqual(newValue, oldValue)) {
diff --git a/mobx/lib/src/utils.dart b/mobx/lib/src/utils.dart
index 80c30b82..41fc1e3e 100644
--- a/mobx/lib/src/utils.dart
+++ b/mobx/lib/src/utils.dart
@@ -24,9 +24,9 @@ mixin DebugCreationStack {
 }
 
 /// Determines whether [a] and [b] are equal.
-bool equatable<T>(T a, T b) {
+bool equatable<T>(T a, T b, {bool useDeepEquality = false}) {
   if (identical(a, b)) return true;
-  if (a is Iterable || a is Map) {
+  if (useDeepEquality && (a is Iterable || a is Map)) {
     if (!_equality.equals(a, b)) return false;
   } else if (a.runtimeType != b.runtimeType) {
     return false;
diff --git a/mobx/lib/version.dart b/mobx/lib/version.dart
index 2a69223b..d3f0725f 100644
--- a/mobx/lib/version.dart
+++ b/mobx/lib/version.dart
@@ -1,4 +1,4 @@
 // Generated via set_version.dart. !!!DO NOT MODIFY BY HAND!!!
 
 /// The current version as per `pubspec.yaml`.
-const version = '2.2.3';
+const version = '2.2.3+1';
diff --git a/mobx/pubspec.yaml b/mobx/pubspec.yaml
index 4c59a2ea..ea2d898d 100644
--- a/mobx/pubspec.yaml
+++ b/mobx/pubspec.yaml
@@ -1,5 +1,5 @@
 name: mobx
-version: 2.2.3
+version: 2.2.3+1
 description: "MobX is a library for reactively managing the state of your applications. Use the power of observables, actions, and reactions to supercharge your Dart and Flutter apps."
 
 homepage: https://github.com/mobxjs/mobx.dart
diff --git a/mobx_codegen/CHANGELOG.md b/mobx_codegen/CHANGELOG.md
index 6f58bd96..e9ed8735 100644
--- a/mobx_codegen/CHANGELOG.md
+++ b/mobx_codegen/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.4.1
+
+- Adds `useDeepEquality` for creating observables by [@amondnet](https://github.com/amondnet)
+
 ## 2.4.0
 
 - Require `analyzer: ^5.12.0`
diff --git a/mobx_codegen/lib/src/template/observable.dart b/mobx_codegen/lib/src/template/observable.dart
index bb89577d..c536c69a 100644
--- a/mobx_codegen/lib/src/template/observable.dart
+++ b/mobx_codegen/lib/src/template/observable.dart
@@ -12,6 +12,7 @@ class ObservableTemplate {
     this.isReadOnly = false,
     this.isPrivate = false,
     this.equals,
+    this.useDeepEquality,
   });
 
   final StoreTemplate storeTemplate;
@@ -21,6 +22,7 @@ class ObservableTemplate {
   final bool isPrivate;
   final bool isReadOnly;
   final ExecutableElement? equals;
+  final bool? useDeepEquality;
 
   /// Formats the `name` from `_foo_bar` to `foo_bar`
   /// such that the getter gets public
@@ -61,6 +63,6 @@ ${_buildGetters()}
   set $name($type value) {
     $atomName.reportWrite(value, super.$name, () {
       super.$name = value;
-    }${equals != null ? ', equals: ${equals!.name}' : ''});
+    }${equals != null ? ', equals: ${equals!.name}' : ''}${useDeepEquality != null ? ', useDeepEquality: $useDeepEquality' : ''});
   }""";
 }
diff --git a/mobx_codegen/lib/version.dart b/mobx_codegen/lib/version.dart
index 5c81252f..d2bed9e4 100644
--- a/mobx_codegen/lib/version.dart
+++ b/mobx_codegen/lib/version.dart
@@ -1,4 +1,4 @@
 // Generated via set_version.dart. !!!DO NOT MODIFY BY HAND!!!
 
 /// The current version as per `pubspec.yaml`.
-const version = '2.4.0';
+const version = '2.4.1';
diff --git a/mobx_codegen/pubspec.yaml b/mobx_codegen/pubspec.yaml
index 6e988cdd..b14e3cd8 100644
--- a/mobx_codegen/pubspec.yaml
+++ b/mobx_codegen/pubspec.yaml
@@ -1,6 +1,6 @@
 name: mobx_codegen
 description: Code generator for MobX that adds support for annotating your code with @observable, @computed, @action and also creating Store classes.
-version: 2.4.0
+version: 2.4.1
 
 homepage: https://github.com/mobxjs/mobx.dart
 issue_tracker: https://github.com/mobxjs/mobx.dart/issues
@@ -13,7 +13,7 @@ dependencies:
   build: ^2.2.1
   build_resolvers: ^2.0.6
   meta: ^1.3.0
-  mobx: ^2.2.0
+  mobx: ^2.2.3+1
   path: ^1.8.0
   source_gen: ^1.2.1
 


Test patch:
diff --git a/mobx/test/all_tests.dart b/mobx/test/all_tests.dart
index 985418cc..f979cddc 100644
--- a/mobx/test/all_tests.dart
+++ b/mobx/test/all_tests.dart
@@ -33,6 +33,7 @@ import 'spy_test.dart' as spy_test;
 import 'store_test.dart' as store_test;
 import 'when_test.dart' as when_test;
 import 'atom_test.dart' as atom_test;
+import 'utils_test.dart' as utils_test;
 
 void main() {
   observable_test.main();
@@ -74,4 +75,5 @@ void main() {
   store_test.main();
 
   atom_test.main();
+  utils_test.main();
 }
diff --git a/mobx/test/atom_extensions_test.dart b/mobx/test/atom_extensions_test.dart
index e7df914d..792265c4 100644
--- a/mobx/test/atom_extensions_test.dart
+++ b/mobx/test/atom_extensions_test.dart
@@ -116,6 +116,31 @@ void main() {
           {'first': 1}
         ]);
       });
+
+
+  test(
+      'when write to @MakeObservable(useDeepEquality: true) field with same value, should not trigger notifications for downstream',
+          () {
+        final store = _ExampleStore();
+
+        final autorunResults = <List<int>>[];
+        autorun((_) => autorunResults.add(store.iterable));
+
+        store.iterable = [1];
+        expect(autorunResults, [
+          [1]
+        ]);
+
+        store.iterable = [1];
+        expect(autorunResults, [
+          [1]
+        ]);
+
+        store.iterable = [1];
+        expect(autorunResults, [
+          [1]
+        ]);
+      });
 }
 
 class _ExampleStore = __ExampleStore with _$_ExampleStore;
@@ -137,6 +162,9 @@ abstract class __ExampleStore with Store {
 
   @observable
   Map<String, int> map = {'first': 1};
+
+  @MakeObservable(useDeepEquality: true)
+  List<int> iterable = [1];
 }
 
 // This is what typically a mobx codegen will generate.
@@ -223,4 +251,20 @@ mixin _$_ExampleStore on __ExampleStore, Store {
       super.map = value;
     });
   }
+
+  // ignore: non_constant_identifier_names
+  late final _$iterableAtom = Atom(name: '__ExampleStore.iterable', context: context);
+
+  @override
+  List<int> get iterable {
+    _$iterableAtom.reportRead();
+    return super.iterable;
+  }
+
+  @override
+  set iterable(List<int> value) {
+    _$iterableAtom.reportWrite(value, super.iterable, () {
+      super.iterable = value;
+    }, useDeepEquality: true);
+  }
 }
diff --git a/mobx/test/utils_test.dart b/mobx/test/utils_test.dart
new file mode 100644
index 00000000..53375cfc
--- /dev/null
+++ b/mobx/test/utils_test.dart
@@ -0,0 +1,24 @@
+import 'package:mobx/src/utils.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('Utils', () {
+    test('equatable', () {
+      final a = 1;
+      final b = 1;
+      expect(equatable(a, b), isTrue);
+    });
+
+    test('equatable iterable', () {
+      final a = [1];
+      final b = [1];
+      expect(equatable(a, b, useDeepEquality: false), false);
+    });
+
+    test('equatable with deep equality', () {
+      final a = [1];
+      final b = [1];
+      expect(equatable(a, b, useDeepEquality: true), isTrue);
+    });
+  });
+}