| import 'dart:async'; |
| |
| import 'package:collection/collection.dart'; |
| import 'package:observable/observable.dart'; |
| import 'package:observable/src/differs.dart'; |
| |
| /// A [List] 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 inserted record). You may |
| /// accept an observable list to be notified of mutations: |
| /// set names(List<String> names) { |
| /// clearAndWrite(names); |
| /// if (names is ObservableList<String>) { |
| /// names.listChanges.listen(smallIncrementalUpdate); |
| /// } |
| /// } |
| /// |
| /// *See [ListDiffer] to manually diff two lists instead* |
| abstract class ObservableList<E> implements List<E>, Observable { |
| /// Applies [changes] to [previous] based on the [current] values. |
| /// |
| /// ## Deprecated |
| /// |
| /// If you need this functionality, copy it into your own library. The only |
| /// known usage is in `package:template_binding` - it will be upgraded before |
| /// removing this method. |
| @Deprecated('Use ListChangeRecord#apply instead') |
| static void applyChangeRecords/*<T>*/( |
| List/*<T>*/ previous, |
| List/*<T>*/ current, |
| List<ListChangeRecord/*<T>*/ > changes, |
| ) { |
| if (identical(previous, current)) { |
| throw new ArgumentError("Can't use same list for previous and current"); |
| } |
| for (final change in changes) { |
| change.apply(previous); |
| } |
| } |
| |
| /// Calculates change records between [previous] and [current]. |
| /// |
| /// ## Deprecated |
| /// |
| /// This was moved into `ListDiffer.diff`. |
| @Deprecated('Use `ListDiffer.diff` instead') |
| static List<ListChangeRecord/*<T>*/ > calculateChangeRecords/*<T>*/( |
| List/*<T>*/ previous, |
| List/*<T>*/ current, |
| ) { |
| return const ListDiffer/*<T>*/().diff(previous, current); |
| } |
| |
| /// Creates an observable list of the given [length]. |
| factory ObservableList([int length]) { |
| final list = length != null ? new List<E>(length) : <E>[]; |
| return new _ObservableDelegatingList(list); |
| } |
| |
| /// Create a new observable list. |
| /// |
| /// Optionally define a [list] to use as a backing store. |
| factory ObservableList.delegate([List<E> list]) { |
| return new _ObservableDelegatingList(list ?? <E>[]); |
| } |
| |
| /// Create a new observable list from [elements]. |
| factory ObservableList.from(Iterable<E> elements) { |
| return new _ObservableDelegatingList(elements.toList()); |
| } |
| |
| /// Creates a new observable list of the given [length]. |
| @Deprecated('Use the default constructor') |
| factory ObservableList.withLength(int length) { |
| return new ObservableList<E>(length); |
| } |
| |
| @Deprecated('No longer supported. Just use deliverChanges') |
| bool deliverListChanges(); |
| |
| @Deprecated('No longer supported') |
| void discardListChanges(); |
| |
| @Deprecated('The `changes` stream emits ListChangeRecord now') |
| bool get hasListObservers; |
| |
| /// A stream of summarized list changes, delivered asynchronously. |
| @Deprecated('' |
| 'The `changes` stream will soon only emit ListChangeRecord; ' |
| 'either continue to use this getter until removed, or use the changes ' |
| 'stream with a `Stream.where` guard') |
| Stream<List<ListChangeRecord<E>>> get listChanges; |
| |
| @Deprecated('Should no longer be used external from ObservableList') |
| void notifyListChange( |
| int index, { |
| List<E> removed: const [], |
| int addedCount: 0, |
| }); |
| } |
| |
| class _ObservableDelegatingList<E> extends DelegatingList<E> |
| implements ObservableList<E> { |
| final _listChanges = new ChangeNotifier<ListChangeRecord<E>>(); |
| final _propChanges = new ChangeNotifier<PropertyChangeRecord>(); |
| |
| StreamController<List<ChangeRecord>> _allChanges; |
| |
| _ObservableDelegatingList(List<E> store) : super(store); |
| |
| // Observable |
| |
| @override |
| Stream<List<ChangeRecord>> get changes { |
| if (_allChanges == null) { |
| StreamSubscription listSub; |
| StreamSubscription propSub; |
| _allChanges = new StreamController<List<ChangeRecord>>.broadcast( |
| sync: true, |
| onListen: () { |
| listSub = listChanges.listen(_allChanges.add); |
| propSub = _propChanges.changes.listen(_allChanges.add); |
| }, |
| onCancel: () { |
| listSub.cancel(); |
| propSub.cancel(); |
| }); |
| } |
| return _allChanges.stream; |
| } |
| |
| // ChangeNotifier (deprecated for ObservableList) |
| |
| @override |
| bool deliverChanges() { |
| final deliveredListChanges = _listChanges.deliverChanges(); |
| final deliveredPropChanges = _propChanges.deliverChanges(); |
| return deliveredListChanges || deliveredPropChanges; |
| } |
| |
| @override |
| void discardListChanges() { |
| // This used to do something, but now we just make it a no-op. |
| } |
| |
| @override |
| bool get hasObservers { |
| return _listChanges.hasObservers || _propChanges.hasObservers; |
| } |
| |
| @override |
| void notifyChange([ChangeRecord change]) { |
| if (change is ListChangeRecord<E>) { |
| _listChanges.notifyChange(change); |
| } else if (change is PropertyChangeRecord) { |
| _propChanges.notifyChange(change); |
| } |
| } |
| |
| @override |
| /*=T*/ notifyPropertyChange/*<T>*/( |
| Symbol field, |
| /*=T*/ |
| oldValue, |
| /*=T*/ |
| newValue, |
| ) { |
| if (oldValue != newValue) { |
| _propChanges.notifyChange( |
| new PropertyChangeRecord/*<T>*/(this, field, oldValue, newValue), |
| ); |
| } |
| return newValue; |
| } |
| |
| @override |
| void observed() {} |
| |
| @override |
| void unobserved() {} |
| |
| // ObservableList (deprecated) |
| |
| @override |
| bool deliverListChanges() => _listChanges.deliverChanges(); |
| |
| @override |
| bool get hasListObservers => _listChanges.hasObservers; |
| |
| @override |
| Stream<List<ListChangeRecord<E>>> get listChanges { |
| return _listChanges.changes.map((r) => projectListSplices(this, r)); |
| } |
| |
| @override |
| void notifyListChange( |
| int index, { |
| List<E> removed: const [], |
| int addedCount: 0, |
| }) { |
| _listChanges.notifyChange(new ListChangeRecord<E>( |
| this, |
| index, |
| removed: removed, |
| addedCount: addedCount, |
| )); |
| } |
| |
| void _notifyChangeLength(int oldLength, int newLength) { |
| notifyPropertyChange(#length, oldLength, newLength); |
| notifyPropertyChange(#isEmpty, oldLength == 0, newLength == 0); |
| notifyPropertyChange(#isNotEmpty, oldLength != 0, newLength != 0); |
| } |
| |
| // List |
| |
| @override |
| operator []=(int index, E newValue) { |
| final oldValue = this[index]; |
| if (hasObservers && oldValue != newValue) { |
| notifyListChange(index, removed: [oldValue], addedCount: 1); |
| } |
| super[index] = newValue; |
| } |
| |
| @override |
| void add(E value) { |
| if (hasObservers) { |
| _notifyChangeLength(length, length + 1); |
| notifyListChange(length, addedCount: 1); |
| } |
| super.add(value); |
| } |
| |
| @override |
| void addAll(Iterable<E> values) { |
| final oldLength = this.length; |
| super.addAll(values); |
| final newLength = this.length; |
| final addedCount = newLength - oldLength; |
| if (hasObservers && addedCount > 0) { |
| notifyListChange(oldLength, addedCount: addedCount); |
| _notifyChangeLength(oldLength, newLength); |
| } |
| } |
| |
| @override |
| void clear() { |
| if (hasObservers) { |
| notifyListChange(0, removed: toList()); |
| _notifyChangeLength(length, 0); |
| } |
| super.clear(); |
| } |
| |
| @override |
| void fillRange(int start, int end, [E value]) { |
| if (hasObservers) { |
| notifyListChange( |
| start, |
| addedCount: end - start, |
| removed: getRange(start, end).toList(), |
| ); |
| } |
| super.fillRange(start, end, value); |
| } |
| |
| @override |
| void insert(int index, E element) { |
| super.insert(index, element); |
| if (hasObservers) { |
| notifyListChange(index, addedCount: 1); |
| _notifyChangeLength(length - 1, length); |
| } |
| } |
| |
| @override |
| void insertAll(int index, Iterable<E> values) { |
| final oldLength = this.length; |
| super.insertAll(index, values); |
| final newLength = this.length; |
| final addedCount = newLength - oldLength; |
| if (hasObservers && addedCount > 0) { |
| notifyListChange(index, addedCount: addedCount); |
| _notifyChangeLength(oldLength, newLength); |
| } |
| } |
| |
| @override |
| set length(int newLength) { |
| final currentLength = this.length; |
| if (currentLength == newLength) { |
| return; |
| } |
| if (hasObservers) { |
| if (newLength < currentLength) { |
| notifyListChange( |
| newLength, |
| removed: getRange(newLength, currentLength).toList(), |
| ); |
| } else { |
| notifyListChange(currentLength, addedCount: newLength - currentLength); |
| } |
| } |
| super.length = newLength; |
| if (hasObservers) { |
| _notifyChangeLength(currentLength, newLength); |
| } |
| } |
| |
| @override |
| bool remove(Object element) { |
| if (!hasObservers) { |
| return super.remove(element); |
| } |
| for (var i = 0; i < this.length; i++) { |
| if (this[i] == element) { |
| removeAt(i); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @override |
| E removeAt(int index) { |
| if (hasObservers) { |
| final element = this[index]; |
| notifyListChange(index, removed: [element]); |
| _notifyChangeLength(length, length - 1); |
| } |
| return super.removeAt(index); |
| } |
| |
| @override |
| E removeLast() { |
| final element = super.removeLast(); |
| if (hasObservers) { |
| notifyListChange(length, removed: [element]); |
| _notifyChangeLength(length + 1, length); |
| } |
| return element; |
| } |
| |
| @override |
| void removeRange(int start, int end) { |
| final rangeLength = end - start; |
| if (hasObservers && rangeLength > 0) { |
| final removed = getRange(start, end).toList(); |
| notifyListChange(start, removed: removed); |
| _notifyChangeLength(length, length - removed.length); |
| } |
| super.removeRange(start, end); |
| } |
| |
| @override |
| void removeWhere(bool test(E element)) { |
| // We have to re-implement this if we have observers. |
| if (!hasObservers) return super.removeWhere(test); |
| |
| // Produce as few change records as possible - if we have multiple removals |
| // in a sequence we want to produce a single record instead of a record for |
| // every element removed. |
| int firstRemovalIndex; |
| |
| for (var i = 0; i < length; i++) { |
| var element = this[i]; |
| if (test(element)) { |
| if (firstRemovalIndex == null) { |
| // This is the first item we've removed for this sequence. |
| firstRemovalIndex = i; |
| } |
| } else if (firstRemovalIndex != null) { |
| // We have a previous sequence of removals, but are not removing more. |
| removeRange(firstRemovalIndex, i--); |
| firstRemovalIndex = null; |
| } |
| } |
| |
| // We have have a pending removal that was never finished. |
| if (firstRemovalIndex != null) { |
| removeRange(firstRemovalIndex, length); |
| } |
| } |
| |
| @override |
| void replaceRange(int start, int end, Iterable<E> newContents) { |
| // This could be optimized not to emit two change records but would require |
| // code duplication with these methods. Since this is not used extremely |
| // often in my experience OK to just defer to these methods. |
| removeRange(start, end); |
| insertAll(start, newContents); |
| } |
| |
| @override |
| void retainWhere(bool test(E element)) { |
| // This should be functionally the opposite of removeWhere. |
| removeWhere((E element) => !test(element)); |
| } |
| |
| @override |
| void setAll(int index, Iterable<E> elements) { |
| if (!hasObservers) { |
| super.setAll(index, elements); |
| return; |
| } |
| // Manual invocation of this method is required to get nicer change events. |
| var i = index; |
| final removed = <E>[]; |
| for (var e in elements) { |
| removed.add(this[i]); |
| super[i++] = e; |
| } |
| if (removed.isNotEmpty) { |
| notifyListChange(index, removed: removed, addedCount: removed.length); |
| } |
| } |
| |
| @override |
| void setRange(int start, int end, Iterable<E> elements, [int skipCount = 0]) { |
| if (!hasObservers) { |
| super.setRange(start, end, elements, skipCount); |
| return; |
| } |
| final iterator = elements.skip(skipCount).iterator..moveNext(); |
| final removed = <E>[]; |
| for (var i = start; i < end; i++) { |
| removed.add(super[i]); |
| super[i] = iterator.current; |
| iterator.moveNext(); |
| } |
| if (removed.isNotEmpty) { |
| notifyListChange(start, removed: removed, addedCount: removed.length); |
| } |
| } |
| } |