blob: f1c91918dbc7168c1aa1875c03b08b1e90a4e2f6 [file] [log] [blame]
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:observable/observable.dart';
/// A [Map] that broadcasts [changes] to subscribers for efficient mutations.
///
/// When client code expects a read heavy/write light workload, it is often more
/// efficient to notify _when_ something has changed, instead of constantly
/// diffing lists to find a single change (like an updated key-value). You may
/// accept an observable map to be notified of mutations:
/// ```
/// set grades(Map<String, int> grades) {
/// buildBook(grades);
/// if (names is ObservableMap<String, int>) {
/// grades.changes.listen(updateBook);
/// }
/// }
/// ```
///
/// *See [MapDiffer] to manually diff two lists instead*
abstract class ObservableMap<K, V> implements Map<K, V>, Observable {
/// An empty observable map that never has changes.
static const ObservableMap EMPTY = const _ObservableUnmodifiableMap(const {});
/// Creates a new observable map.
factory ObservableMap() {
return new _ObservableDelegatingMap(new HashMap<K, V>());
}
/// Like [ObservableMap.from], but creates an empty type.
factory ObservableMap.createFromType(Map<K, V> other) {
ObservableMap<K, V> result;
if (other is LinkedHashMap) {
result = new ObservableMap<K, V>.linked();
} else if (other is SplayTreeMap) {
result = new ObservableMap<K, V>.sorted();
} else {
result = new ObservableMap<K, V>();
}
return result;
}
/// Create a new observable map using [map] as a backing store.
factory ObservableMap.delegate(Map<K, V> map) {
return new _ObservableDelegatingMap<K, V>(map);
}
/// Creates a new observable map that contains all entries in [other].
///
/// It will attempt to use the same backing map type if the other map is
/// either a [LinkedHashMap], [SplayTreeMap], or [HashMap]. Otherwise it will
/// fall back to using a [HashMap].
factory ObservableMap.from(Map<K, V> other) {
return new ObservableMap<K, V>.createFromType(other)..addAll(other);
}
/// Creates a new observable map using a [LinkedHashMap].
factory ObservableMap.linked() {
return new _ObservableDelegatingMap<K, V>(new LinkedHashMap<K, V>());
}
/// Creates a new observable map using a [SplayTreeMap].
factory ObservableMap.sorted() {
return new _ObservableDelegatingMap<K, V>(new SplayTreeMap<K, V>());
}
/// Creates a new observable map wrapping [other].
@Deprecated('Use ObservableMap.delegate for API consistency')
factory ObservableMap.spy(Map<K, V> other) = ObservableMap<K, V>.delegate;
/// Create a new unmodifiable map from [map].
///
/// [ObservableMap.changes] always returns an empty stream, and mutating or
/// adding change records throws an [UnsupportedError].
factory ObservableMap.unmodifiable(Map<K, V> map) {
if (map is! UnmodifiableMapView<K, V>) {
map = new Map<K, V>.unmodifiable(map);
}
return new _ObservableUnmodifiableMap(map);
}
}
class _ObservableDelegatingMap<K, V> extends DelegatingMap<K, V>
implements ObservableMap<K, V> {
final _allChanges = new ChangeNotifier();
_ObservableDelegatingMap(Map<K, V> map) : super(map);
// Observable
@override
Stream<List<ChangeRecord>> get changes => _allChanges.changes;
// ChangeNotifier (deprecated for ObservableMap)
@override
bool deliverChanges() => _allChanges.deliverChanges();
@override
bool get hasObservers => _allChanges.hasObservers;
@override
void notifyChange([ChangeRecord change]) {
if (change is MapChangeRecord && change.oldValue == change.newValue) {
return;
}
_allChanges.notifyChange(change);
}
@override
/*=T*/ notifyPropertyChange/*<T>*/(
Symbol field,
/*=T*/
oldValue,
/*=T*/
newValue,
) {
if (oldValue != newValue) {
_allChanges.notifyChange(
new PropertyChangeRecord/*<T>*/(this, field, oldValue, newValue),
);
}
return newValue;
}
@override
void observed() {}
@override
void unobserved() {}
@override
operator []=(K key, V newValue) {
if (!hasObservers) {
super[key] = newValue;
return;
}
final oldLength = super.length;
V oldValue = super[key];
super[key] = newValue;
if (oldLength != length) {
notifyPropertyChange(#length, oldLength, length);
notifyChange(new MapChangeRecord<K, V>.insert(key, newValue));
} else {
notifyChange(new MapChangeRecord<K, V>(key, oldValue, newValue));
}
}
@override
void addAll(Map<K, V> other) {
if (!hasObservers) {
super.addAll(other);
return;
}
other.forEach((k, v) => this[k] = v);
}
@override
V putIfAbsent(K key, V ifAbsent()) {
final oldLength = length;
final result = super.putIfAbsent(key, ifAbsent);
if (hasObservers && oldLength != length) {
notifyPropertyChange(#length, oldLength, length);
notifyChange(new MapChangeRecord<K, V>.insert(key, result));
}
return result;
}
@override
V remove(Object key) {
final oldLength = length;
final result = super.remove(key);
if (hasObservers && oldLength != length) {
notifyChange(new MapChangeRecord<K, V>.remove(key as K, result));
notifyPropertyChange(#length, oldLength, length);
}
return result;
}
@override
void clear() {
if (!hasObservers || isEmpty) {
super.clear();
return;
}
final oldLength = length;
forEach((k, v) {
notifyChange(new MapChangeRecord<K, V>.remove(k, v));
});
notifyPropertyChange(#length, oldLength, 0);
super.clear();
}
}
class _ObservableUnmodifiableMap<K, V> extends DelegatingMap<K, V>
implements ObservableMap<K, V> {
const _ObservableUnmodifiableMap(Map<K, V> map) : super(map);
@override
Stream<List<ChangeRecord>> get changes => const Stream.empty();
@override
bool deliverChanges() => false;
// TODO: implement hasObservers
@override
final bool hasObservers = false;
@override
void notifyChange([ChangeRecord change]) {
throw new UnsupportedError('Not modifiable');
}
@override
/*=T*/ notifyPropertyChange/*<T>*/(
Symbol field,
/*=T*/
oldValue,
/*=T*/
newValue,
) {
throw new UnsupportedError('Not modifiable');
}
@override
void observed() {}
@override
void unobserved() {}
}