Skip to content

Commit 076d509

Browse files
committed
Allow a custom equals parameter for observable collections
1 parent d14a27e commit 076d509

File tree

6 files changed

+105
-37
lines changed

6 files changed

+105
-37
lines changed

mobx/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.2.4
2+
3+
- Allow a custom equals parameter for observable collections( ObservableList, ObservableMap, ObservableSet ) - [@amondnet](https://github.com/amondnet)
4+
15
## 2.2.3
26

37
- 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)

mobx/lib/src/api/observable_collections.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:math';
33

44
import 'package:meta/meta.dart';
55
import 'package:mobx/mobx.dart';
6+
import 'package:mobx/src/utils.dart';
67

78
part 'observable_collections/observable_list.dart';
89
part 'observable_collections/observable_map.dart';

mobx/lib/src/api/observable_collections/observable_list.dart

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,23 @@ class ObservableList<T>
2525
ListMixin<T>
2626
implements
2727
Listenable<ListChange<T>> {
28-
ObservableList({ReactiveContext? context, String? name})
29-
: this._wrap(context, _observableListAtom<T>(context, name), []);
28+
ObservableList(
29+
{ReactiveContext? context, String? name, EqualityComparer<T>? equals})
30+
: this._wrap(context, _observableListAtom<T>(context, name), [], equals);
3031

3132
ObservableList.of(Iterable<T> elements,
32-
{ReactiveContext? context, String? name})
33+
{ReactiveContext? context, String? name, EqualityComparer<T>? equals})
3334
: this._wrap(context, _observableListAtom<T>(context, name),
34-
List<T>.of(elements, growable: true));
35+
List<T>.of(elements, growable: true), equals);
3536

36-
ObservableList._wrap(ReactiveContext? context, this._atom, this._list)
37+
ObservableList._wrap(
38+
ReactiveContext? context, this._atom, this._list, this._equals)
3739
: _context = context ?? mainContext;
3840

3941
final ReactiveContext _context;
4042
final Atom _atom;
4143
final List<T> _list;
44+
final EqualityComparer<T>? _equals;
4245

4346
List<T> get nonObservableInner => _list;
4447

@@ -96,7 +99,7 @@ class ObservableList<T>
9699
_context.conditionallyRunInAction(() {
97100
final oldValue = _list[index];
98101

99-
if (oldValue != value) {
102+
if (_areEquals(oldValue, value)) {
100103
_list[index] = value;
101104
_notifyElementUpdate(index, value, oldValue);
102105
}
@@ -167,10 +170,18 @@ class ObservableList<T>
167170
}
168171

169172
@override
170-
Map<int, T> asMap() => ObservableMap._wrap(_context, _list.asMap(), _atom);
173+
Map<int, T> asMap() =>
174+
ObservableMap._wrap(_context, _list.asMap(), _atom, _equals);
171175

172176
@override
173-
List<R> cast<R>() => ObservableList._wrap(_context, _atom, _list.cast<R>());
177+
List<R> cast<R>([EqualityComparer<R>? equals]) => ObservableList._wrap(
178+
_context,
179+
_atom,
180+
_list.cast<R>(),
181+
equals ??
182+
(_equals != null
183+
? (R? a, R? b) => _equals!(a as T?, b as T?)
184+
: null));
174185

175186
@override
176187
List<T> toList({bool growable = true}) {
@@ -184,7 +195,7 @@ class ObservableList<T>
184195
set first(T value) {
185196
_context.conditionallyRunInAction(() {
186197
final oldValue = _list.first;
187-
if (oldValue != value) {
198+
if (_areEquals(oldValue, value)) {
188199
_list.first = value;
189200
_notifyElementUpdate(0, value, oldValue);
190201
}
@@ -376,7 +387,7 @@ class ObservableList<T>
376387
for (var i = 0; i < _list.length; ++i) {
377388
final oldValue = oldList[i];
378389
final newValue = _list[i];
379-
if (newValue != oldValue) {
390+
if (_areEquals(oldValue, newValue)) {
380391
changes.add(ElementChange(
381392
index: i, oldValue: oldValue, newValue: newValue));
382393
}
@@ -398,7 +409,7 @@ class ObservableList<T>
398409
for (var i = 0; i < _list.length; ++i) {
399410
final oldValue = oldList[i];
400411
final newValue = _list[i];
401-
if (newValue != oldValue) {
412+
if (_areEquals(oldValue, newValue)) {
402413
changes.add(ElementChange(
403414
index: i, oldValue: oldValue, newValue: newValue));
404415
}
@@ -456,6 +467,14 @@ class ObservableList<T>
456467

457468
_listeners.notifyListeners(change);
458469
}
470+
471+
bool _areEquals(T? a, T? b) {
472+
if (_equals != null) {
473+
return _equals!(a, b);
474+
} else {
475+
return equatable(a, b);
476+
}
477+
}
459478
}
460479

461480
typedef ListChangeListener<TNotification> = void Function(
@@ -520,4 +539,4 @@ class ListChange<T> {
520539
/// Used during testing for wrapping a regular `List<T>` as an `ObservableList<T>`
521540
@visibleForTesting
522541
ObservableList<T> wrapInObservableList<T>(Atom atom, List<T> list) =>
523-
ObservableList._wrap(mainContext, atom, list);
542+
ObservableList._wrap(mainContext, atom, list, null);

mobx/lib/src/api/observable_collections/observable_map.dart

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,37 +26,45 @@ class ObservableMap<K, V>
2626
MapMixin<K, V>
2727
implements
2828
Listenable<MapChange<K, V>> {
29-
ObservableMap({ReactiveContext? context, String? name})
29+
ObservableMap(
30+
{ReactiveContext? context, String? name, EqualityComparer<V>? equals})
3031
: _context = context ?? mainContext,
3132
_atom = _observableMapAtom<K, V>(context, name),
32-
_map = <K, V>{};
33+
_map = <K, V>{},
34+
_equals = equals;
3335

34-
ObservableMap.of(Map<K, V> other, {ReactiveContext? context, String? name})
36+
ObservableMap.of(Map<K, V> other,
37+
{ReactiveContext? context, String? name, EqualityComparer<V>? equals})
3538
: _context = context ?? mainContext,
3639
_atom = _observableMapAtom<K, V>(context, name),
37-
_map = Map.of(other);
40+
_map = Map.of(other),
41+
_equals = equals;
3842

3943
ObservableMap.linkedHashMapFrom(Map<K, V> other,
40-
{ReactiveContext? context, String? name})
44+
{ReactiveContext? context, String? name, EqualityComparer<V>? equals})
4145
: _context = context ?? mainContext,
4246
_atom = _observableMapAtom<K, V>(context, name),
43-
_map = LinkedHashMap.from(other);
47+
_map = LinkedHashMap.from(other),
48+
_equals = equals;
4449

4550
ObservableMap.splayTreeMapFrom(Map<K, V> other,
4651
{int Function(K, K)? compare,
4752
// ignore: avoid_annotating_with_dynamic
4853
bool Function(dynamic)? isValidKey,
4954
ReactiveContext? context,
50-
String? name})
55+
String? name,
56+
EqualityComparer<V>? equals})
5157
: _context = context ?? mainContext,
5258
_atom = _observableMapAtom<K, V>(context, name),
53-
_map = SplayTreeMap.from(other, compare, isValidKey);
59+
_map = SplayTreeMap.from(other, compare, isValidKey),
60+
_equals = equals;
5461

55-
ObservableMap._wrap(this._context, this._map, this._atom);
62+
ObservableMap._wrap(this._context, this._map, this._atom, this._equals);
5663

5764
final ReactiveContext _context;
5865
final Atom _atom;
5966
final Map<K, V> _map;
67+
final EqualityComparer<V>? _equals;
6068

6169
Map<K, V> get nonObservableInner => _map;
6270

@@ -94,7 +102,7 @@ class ObservableMap<K, V>
94102
}
95103
}
96104

97-
if (!_map.containsKey(key) || value != oldValue) {
105+
if (!_map.containsKey(key) || _areEquals(value, oldValue)) {
98106
_map[key] = value;
99107
if (type == 'update') {
100108
_reportUpdate(key, value, oldValue);
@@ -127,8 +135,15 @@ class ObservableMap<K, V>
127135
Iterable<K> get keys => MapKeysIterable(_map.keys, _atom);
128136

129137
@override
130-
Map<RK, RV> cast<RK, RV>() =>
131-
ObservableMap._wrap(_context, super.cast(), _atom);
138+
Map<RK, RV> cast<RK, RV>([EqualityComparer<RV>? equals]) =>
139+
ObservableMap._wrap(
140+
_context,
141+
super.cast(),
142+
_atom,
143+
equals ??
144+
(_equals == null
145+
? null
146+
: (RV? a, RV? b) => _equals!(a as V?, b as V?)));
132147

133148
@override
134149
V? remove(Object? key) {
@@ -231,13 +246,21 @@ class ObservableMap<K, V>
231246
}
232247
return _listeners.add(listener);
233248
}
249+
250+
bool _areEquals(V? a, V? b) {
251+
if (_equals != null) {
252+
return _equals!(a, b);
253+
} else {
254+
return equatable(a, b);
255+
}
256+
}
234257
}
235258

236259
/// A convenience method to wrap the standard `Map<K,V>` in an `ObservableMap<K,V>`.
237260
/// This is mostly to aid in testing.
238261
@visibleForTesting
239262
ObservableMap<K, V> wrapInObservableMap<K, V>(Atom atom, Map<K, V> map) =>
240-
ObservableMap._wrap(mainContext, map, atom);
263+
ObservableMap._wrap(mainContext, map, atom, null);
241264

242265
typedef MapChangeListener<K, V> = void Function(MapChange<K, V>);
243266

mobx/lib/src/api/observable_collections/observable_set.dart

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,30 +21,36 @@ class ObservableSet<T>
2121
SetMixin<T>
2222
implements
2323
Listenable<SetChange<T>> {
24-
ObservableSet({ReactiveContext? context, String? name})
25-
: this._(context ?? mainContext, <T>{}, name);
24+
ObservableSet(
25+
{ReactiveContext? context, String? name, EqualityComparer<T>? equals})
26+
: this._(context ?? mainContext, <T>{}, name, equals);
2627

27-
ObservableSet.of(Iterable<T> other, {ReactiveContext? context, String? name})
28-
: this._(context ?? mainContext, Set<T>.of(other), name);
28+
ObservableSet.of(Iterable<T> other,
29+
{ReactiveContext? context, String? name, EqualityComparer<T>? equals})
30+
: this._(context ?? mainContext, Set<T>.of(other), name, equals);
2931

3032
ObservableSet.splayTreeSetFrom(Iterable<T> other,
3133
{int Function(T, T)? compare,
3234
// ignore:avoid_annotating_with_dynamic
3335
bool Function(dynamic)? isValidKey,
3436
ReactiveContext? context,
35-
String? name})
37+
String? name,
38+
EqualityComparer<T>? equals})
3639
: this._(context ?? mainContext,
37-
SplayTreeSet.of(other, compare, isValidKey), name);
40+
SplayTreeSet.of(other, compare, isValidKey), name, equals);
3841

39-
ObservableSet._wrap(this._context, this._atom, this._set);
42+
ObservableSet._wrap(this._context, this._atom, this._set, this._equals);
4043

41-
ObservableSet._(this._context, Set<T> wrapped, String? name)
44+
ObservableSet._(
45+
this._context, Set<T> wrapped, String? name, EqualityComparer<T>? equals)
4246
: _atom = _observableSetAtom(_context, name),
43-
_set = wrapped;
47+
_set = wrapped,
48+
_equals = equals;
4449

4550
final ReactiveContext _context;
4651
final Atom _atom;
4752
final Set<T> _set;
53+
final EqualityComparer<T>? _equals;
4854

4955
Set<T> get nonObservableInner => _set;
5056

@@ -138,7 +144,14 @@ class ObservableSet<T>
138144
}
139145

140146
@override
141-
Set<R> cast<R>() => ObservableSet<R>._wrap(_context, _atom, _set.cast<R>());
147+
Set<R> cast<R>([EqualityComparer<R>? equals]) => ObservableSet<R>._wrap(
148+
_context,
149+
_atom,
150+
_set.cast<R>(),
151+
equals ??
152+
(_equals == null
153+
? null
154+
: (R? a, R? b) => _equals!(a as T?, b as T?)));
142155

143156
@override
144157
Set<T> toSet() {
@@ -180,13 +193,21 @@ class ObservableSet<T>
180193
value: value,
181194
));
182195
}
196+
197+
bool _areEquals(T? a, T? b) {
198+
if (_equals != null) {
199+
return _equals!(a, b);
200+
} else {
201+
return equatable(a, b);
202+
}
203+
}
183204
}
184205

185206
/// A convenience method used during unit testing. It creates an [ObservableSet] with a custom instance
186207
/// of an [Atom]
187208
@visibleForTesting
188209
ObservableSet<T> wrapInObservableSet<T>(Atom atom, Set<T> set) =>
189-
ObservableSet._wrap(mainContext, atom, set);
210+
ObservableSet._wrap(mainContext, atom, set, null);
190211

191212
/// An internal iterator used to ensure that every read is tracked as part of the
192213
/// MobX reactivity system.

mobx/lib/version.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// Generated via set_version.dart. !!!DO NOT MODIFY BY HAND!!!
22

33
/// The current version as per `pubspec.yaml`.
4-
const version = '2.2.3';
4+
const version = '2.2.4';

0 commit comments

Comments
 (0)