Skip to content

Commit

Permalink
feat: Adds useEquatable for creating observables (#971)
Browse files Browse the repository at this point in the history
Make the change in 2.2.3 optional. If you want the use this behavior , modify `@observable` to `@MakeObservable(useEquatable: true)`.
  • Loading branch information
amondnet authored Dec 21, 2023
1 parent 52515a1 commit 827bb5d
Show file tree
Hide file tree
Showing 14 changed files with 159 additions and 11 deletions.
59 changes: 59 additions & 0 deletions docs/docs/api/observable.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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})`
Expand Down
7 changes: 7 additions & 0 deletions mobx/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
7 changes: 6 additions & 1 deletion mobx/lib/src/api/annotations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions mobx/lib/src/core/atom_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
4 changes: 2 additions & 2 deletions mobx/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion mobx/lib/version.dart
Original file line number Diff line number Diff line change
@@ -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';
2 changes: 1 addition & 1 deletion mobx/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions mobx/test/all_tests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -74,4 +75,5 @@ void main() {
store_test.main();

atom_test.main();
utils_test.main();
}
44 changes: 44 additions & 0 deletions mobx/test/atom_extensions_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}
}
24 changes: 24 additions & 0 deletions mobx/test/utils_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
});
}
4 changes: 4 additions & 0 deletions mobx_codegen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`
Expand Down
4 changes: 3 additions & 1 deletion mobx_codegen/lib/src/template/observable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class ObservableTemplate {
this.isReadOnly = false,
this.isPrivate = false,
this.equals,
this.useDeepEquality,
});

final StoreTemplate storeTemplate;
Expand All @@ -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
Expand Down Expand Up @@ -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' : ''});
}""";
}
2 changes: 1 addition & 1 deletion mobx_codegen/lib/version.dart
Original file line number Diff line number Diff line change
@@ -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';
4 changes: 2 additions & 2 deletions mobx_codegen/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down

0 comments on commit 827bb5d

Please sign in to comment.