From 2faae454018100a5fce2c08336a33bb7511895c9 Mon Sep 17 00:00:00 2001
From: Minsu Lee <amond@amond.net>
Date: Mon, 18 Dec 2023 16:49:05 +0900
Subject: [PATCH] feat: Adds `useEquatable` for creating observables

Make the change in 2.2.3 optional. If you want the use this behavior , modify `@observable` to `@MakeObservable(useEquatable: true)`.
---
 docs/docs/api/observable.mdx                  | 59 +++++++++++++++++++
 mobx/CHANGELOG.md                             |  7 +++
 mobx/lib/src/api/annotations.dart             |  7 ++-
 mobx/lib/src/core/atom_extensions.dart        |  5 +-
 mobx/lib/src/utils.dart                       |  4 +-
 mobx/lib/version.dart                         |  2 +-
 mobx/pubspec.yaml                             |  2 +-
 mobx/test/atom_extensions_test.dart           | 44 ++++++++++++++
 mobx_codegen/CHANGELOG.md                     |  4 ++
 mobx_codegen/lib/src/template/observable.dart |  4 +-
 mobx_codegen/lib/version.dart                 |  2 +-
 mobx_codegen/pubspec.yaml                     |  4 +-
 12 files changed, 133 insertions(+), 11 deletions(-)

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/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_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