From 827bb5dfb5ff722af102ad0510d36a5abeaf3e91 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 21 Dec 2023 14:39:36 +0900 Subject: [PATCH] feat: Adds `useEquatable` for creating observables (#971) 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/all_tests.dart | 2 + mobx/test/atom_extensions_test.dart | 44 ++++++++++++++ mobx/test/utils_test.dart | 24 ++++++++ mobx_codegen/CHANGELOG.md | 4 ++ mobx_codegen/lib/src/template/observable.dart | 4 +- mobx_codegen/lib/version.dart | 2 +- mobx_codegen/pubspec.yaml | 4 +- 14 files changed, 159 insertions(+), 11 deletions(-) create mode 100644 mobx/test/utils_test.dart 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 _todos = []; + + @computed + List get todos => _todos; + + @action + void setTodos(List 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 newValue, T oldValue, void Function() setNewValue, - {EqualityComparer? equals}) { - final areEqual = equals ?? equatable; + {EqualityComparer? 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 a, T b) { +bool equatable(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/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 = >[]; + 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 map = {'first': 1}; + + @MakeObservable(useDeepEquality: true) + List 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 get iterable { + _$iterableAtom.reportRead(); + return super.iterable; + } + + @override + set iterable(List 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); + }); + }); +} 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