Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adds useDeepEquality for creating observables #971

Merged
merged 1 commit into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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