Skip to content

Commit

Permalink
Allow a custom equals parameter for observable collections
Browse files Browse the repository at this point in the history
  • Loading branch information
amondnet committed Dec 15, 2023
1 parent d14a27e commit d13d88d
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 53 deletions.
4 changes: 4 additions & 0 deletions mobx/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.2.4

- Allow a custom equals parameter for observable collections( ObservableList, ObservableMap, ObservableSet ) - [@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
2 changes: 2 additions & 0 deletions mobx/lib/src/api/observable_collections.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import 'dart:collection';
import 'dart:math';

import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:mobx/mobx.dart';
import 'package:mobx/src/utils.dart';

part 'observable_collections/observable_list.dart';
part 'observable_collections/observable_map.dart';
Expand Down
43 changes: 31 additions & 12 deletions mobx/lib/src/api/observable_collections/observable_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,23 @@ class ObservableList<T>
ListMixin<T>
implements
Listenable<ListChange<T>> {
ObservableList({ReactiveContext? context, String? name})
: this._wrap(context, _observableListAtom<T>(context, name), []);
ObservableList(
{ReactiveContext? context, String? name, EqualityComparer<T>? equals})
: this._wrap(context, _observableListAtom<T>(context, name), [], equals);

ObservableList.of(Iterable<T> elements,
{ReactiveContext? context, String? name})
{ReactiveContext? context, String? name, EqualityComparer<T>? equals})
: this._wrap(context, _observableListAtom<T>(context, name),
List<T>.of(elements, growable: true));
List<T>.of(elements, growable: true), equals);

ObservableList._wrap(ReactiveContext? context, this._atom, this._list)
ObservableList._wrap(
ReactiveContext? context, this._atom, this._list, this._equals)
: _context = context ?? mainContext;

final ReactiveContext _context;
final Atom _atom;
final List<T> _list;
final EqualityComparer<T>? _equals;

List<T> get nonObservableInner => _list;

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

if (oldValue != value) {
if (!_areEquals(oldValue, value)) {
_list[index] = value;
_notifyElementUpdate(index, value, oldValue);
}
Expand Down Expand Up @@ -167,10 +170,18 @@ class ObservableList<T>
}

@override
Map<int, T> asMap() => ObservableMap._wrap(_context, _list.asMap(), _atom);
Map<int, T> asMap() =>
ObservableMap._wrap(_context, _list.asMap(), _atom, _equals);

@override
List<R> cast<R>() => ObservableList._wrap(_context, _atom, _list.cast<R>());
List<R> cast<R>([EqualityComparer<R>? equals]) => ObservableList._wrap(
_context,
_atom,
_list.cast<R>(),
equals ??
(_equals != null
? (R? a, R? b) => _equals!(a as T?, b as T?)

Check warning on line 183 in mobx/lib/src/api/observable_collections/observable_list.dart

View check run for this annotation

Codecov / codecov/patch

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

Added line #L183 was not covered by tests
: null));

@override
List<T> toList({bool growable = true}) {
Expand All @@ -184,7 +195,7 @@ class ObservableList<T>
set first(T value) {
_context.conditionallyRunInAction(() {
final oldValue = _list.first;
if (oldValue != value) {
if (!_areEquals(oldValue, value)) {
_list.first = value;
_notifyElementUpdate(0, value, oldValue);
}
Expand Down Expand Up @@ -376,7 +387,7 @@ class ObservableList<T>
for (var i = 0; i < _list.length; ++i) {
final oldValue = oldList[i];
final newValue = _list[i];
if (newValue != oldValue) {
if (!_areEquals(oldValue, newValue)) {
changes.add(ElementChange(
index: i, oldValue: oldValue, newValue: newValue));
}
Expand All @@ -398,7 +409,7 @@ class ObservableList<T>
for (var i = 0; i < _list.length; ++i) {
final oldValue = oldList[i];
final newValue = _list[i];
if (newValue != oldValue) {
if (!_areEquals(oldValue, newValue)) {
changes.add(ElementChange(
index: i, oldValue: oldValue, newValue: newValue));
}
Expand Down Expand Up @@ -456,6 +467,14 @@ class ObservableList<T>

_listeners.notifyListeners(change);
}

bool _areEquals(T? a, T? b) {
if (_equals != null) {
return _equals!(a, b);

Check warning on line 473 in mobx/lib/src/api/observable_collections/observable_list.dart

View check run for this annotation

Codecov / codecov/patch

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

Added line #L473 was not covered by tests
} else {
return equatable(a, b);
}
}
}

typedef ListChangeListener<TNotification> = void Function(
Expand Down Expand Up @@ -520,4 +539,4 @@ class ListChange<T> {
/// Used during testing for wrapping a regular `List<T>` as an `ObservableList<T>`
@visibleForTesting
ObservableList<T> wrapInObservableList<T>(Atom atom, List<T> list) =>
ObservableList._wrap(mainContext, atom, list);
ObservableList._wrap(mainContext, atom, list, null);
49 changes: 36 additions & 13 deletions mobx/lib/src/api/observable_collections/observable_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,45 @@ class ObservableMap<K, V>
MapMixin<K, V>
implements
Listenable<MapChange<K, V>> {
ObservableMap({ReactiveContext? context, String? name})
ObservableMap(
{ReactiveContext? context, String? name, EqualityComparer<V>? equals})
: _context = context ?? mainContext,
_atom = _observableMapAtom<K, V>(context, name),
_map = <K, V>{};
_map = <K, V>{},
_equals = equals;

ObservableMap.of(Map<K, V> other, {ReactiveContext? context, String? name})
ObservableMap.of(Map<K, V> other,
{ReactiveContext? context, String? name, EqualityComparer<V>? equals})
: _context = context ?? mainContext,
_atom = _observableMapAtom<K, V>(context, name),
_map = Map.of(other);
_map = Map.of(other),
_equals = equals;

ObservableMap.linkedHashMapFrom(Map<K, V> other,
{ReactiveContext? context, String? name})
{ReactiveContext? context, String? name, EqualityComparer<V>? equals})
: _context = context ?? mainContext,
_atom = _observableMapAtom<K, V>(context, name),
_map = LinkedHashMap.from(other);
_map = LinkedHashMap.from(other),
_equals = equals;

ObservableMap.splayTreeMapFrom(Map<K, V> other,
{int Function(K, K)? compare,
// ignore: avoid_annotating_with_dynamic
bool Function(dynamic)? isValidKey,
ReactiveContext? context,
String? name})
String? name,
EqualityComparer<V>? equals})
: _context = context ?? mainContext,
_atom = _observableMapAtom<K, V>(context, name),
_map = SplayTreeMap.from(other, compare, isValidKey);
_map = SplayTreeMap.from(other, compare, isValidKey),
_equals = equals;

ObservableMap._wrap(this._context, this._map, this._atom);
ObservableMap._wrap(this._context, this._map, this._atom, this._equals);

final ReactiveContext _context;
final Atom _atom;
final Map<K, V> _map;
final EqualityComparer<V>? _equals;

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

Expand Down Expand Up @@ -94,7 +102,7 @@ class ObservableMap<K, V>
}
}

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

@override
Map<RK, RV> cast<RK, RV>() =>
ObservableMap._wrap(_context, super.cast(), _atom);
Map<RK, RV> cast<RK, RV>([EqualityComparer<RV>? equals]) =>
ObservableMap._wrap(
_context,
super.cast(),
_atom,
equals ??
(_equals == null
? null
: (RV? a, RV? b) => _equals!(a as V?, b as V?)));

Check warning on line 146 in mobx/lib/src/api/observable_collections/observable_map.dart

View check run for this annotation

Codecov / codecov/patch

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

Added line #L146 was not covered by tests

@override
V? remove(Object? key) {
Expand Down Expand Up @@ -231,13 +246,21 @@ class ObservableMap<K, V>
}
return _listeners.add(listener);
}

bool _areEquals(V? a, V? b) {
if (_equals != null) {
return _equals!(a, b);

Check warning on line 252 in mobx/lib/src/api/observable_collections/observable_map.dart

View check run for this annotation

Codecov / codecov/patch

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

Added line #L252 was not covered by tests
} else {
return equatable(a, b);
}
}
}

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

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

Expand Down
Loading

0 comments on commit d13d88d

Please sign in to comment.