Index: pkg/observe/lib/src/observable_list.dart |
diff --git a/pkg/observe/lib/src/observable_list.dart b/pkg/observe/lib/src/observable_list.dart |
index 41379570f35355b203510316eca5004997c42c6c..76cf06bf0ebe8fc4a3122f9848d4f9d5305022de 100644 |
--- a/pkg/observe/lib/src/observable_list.dart |
+++ b/pkg/observe/lib/src/observable_list.dart |
@@ -5,8 +5,9 @@ |
library observe.src.observable_list; |
import 'dart:async'; |
-import 'dart:collection' show ListBase; |
+import 'dart:collection' show ListBase, UnmodifiableListView; |
import 'package:observe/observe.dart'; |
+import 'list_diff.dart' show projectListSplices, calcSplices; |
/** |
* Represents an observable list of model values. If any items are added, |
@@ -16,6 +17,8 @@ import 'package:observe/observe.dart'; |
class ObservableList<E> extends ListBase<E> with ChangeNotifier { |
List<ListChangeRecord> _listRecords; |
+ StreamController _listChanges; |
+ |
/** The inner [List<E>] with the actual storage. */ |
final List<E> _list; |
@@ -38,6 +41,40 @@ class ObservableList<E> extends ListBase<E> with ChangeNotifier { |
factory ObservableList.from(Iterable<E> other) => |
new ObservableList<E>()..addAll(other); |
+ /** |
+ * The stream of summarized list changes, delivered asynchronously. |
+ * |
+ * Each list change record contains information about an individual mutation. |
+ * The records are projected so they can be applied sequentially. For example, |
+ * this set of mutations: |
+ * |
+ * var model = new ObservableList.from(['a', 'b']); |
+ * model.listChanges.listen((records) => records.forEach(print)); |
+ * model.removeAt(1); |
+ * model.insertAll(0, ['c', 'd', 'e']); |
+ * model.removeRange(1, 3); |
+ * model.insert(1, 'f'); |
+ * |
+ * The change records will be summarized so they can be "played back", using |
+ * the final list positions to figure out which item was added: |
+ * |
+ * #<ListChangeRecord index: 0, removed: [], addedCount: 2> |
+ * #<ListChangeRecord index: 3, removed: [b], addedCount: 0> |
+ * |
+ * [deliverChanges] can be called to force synchronous delivery. |
+ */ |
+ Stream<List<ListChangeRecord>> get listChanges { |
+ if (_listChanges == null) { |
+ // TODO(jmesserly): split observed/unobserved notions? |
+ _listChanges = new StreamController.broadcast(sync: true, |
+ onCancel: () { _listChanges = null; }); |
+ } |
+ return _listChanges.stream; |
+ } |
+ |
+ bool get _hasListObservers => |
+ _listChanges != null && _listChanges.hasListener; |
+ |
@reflectable int get length => _list.length; |
@reflectable set length(int value) { |
@@ -45,13 +82,13 @@ class ObservableList<E> extends ListBase<E> with ChangeNotifier { |
if (len == value) return; |
// Produce notifications if needed |
- if (hasObservers) { |
+ notifyPropertyChange(#length, len, value); |
+ if (_hasListObservers) { |
if (value < len) { |
- // Remove items, then adjust length. Note the reverse order. |
- _recordChange(new ListChangeRecord(value, removedCount: len - value)); |
+ _recordChange(new ListChangeRecord(this, value, |
+ removed: _list.getRange(value, len).toList())); |
} else { |
- // Adjust length then add items |
- _recordChange(new ListChangeRecord(len, addedCount: value - len)); |
+ _recordChange(new ListChangeRecord(this, len, addedCount: value - len)); |
} |
} |
@@ -62,9 +99,9 @@ class ObservableList<E> extends ListBase<E> with ChangeNotifier { |
@reflectable void operator []=(int index, E value) { |
var oldValue = _list[index]; |
- if (hasObservers) { |
- _recordChange(new ListChangeRecord(index, addedCount: 1, |
- removedCount: 1)); |
+ if (_hasListObservers) { |
+ _recordChange(new ListChangeRecord(this, index, addedCount: 1, |
+ removed: [oldValue])); |
} |
_list[index] = value; |
} |
@@ -76,17 +113,18 @@ class ObservableList<E> extends ListBase<E> with ChangeNotifier { |
iterable = iterable.toList(); |
} |
var len = iterable.length; |
- _list.setAll(index, iterable); |
- if (hasObservers && len > 0) { |
- _recordChange( |
- new ListChangeRecord(index, addedCount: len, removedCount: len)); |
+ if (_hasListObservers && len > 0) { |
+ _recordChange(new ListChangeRecord(this, index, addedCount: len, |
+ removed: _list.getRange(index, len).toList())); |
} |
+ _list.setAll(index, iterable); |
} |
void add(E value) { |
int len = _list.length; |
- if (hasObservers) { |
- _recordChange(new ListChangeRecord(len, addedCount: 1)); |
+ notifyPropertyChange(#length, len, len + 1); |
+ if (_hasListObservers) { |
+ _recordChange(new ListChangeRecord(this, len, addedCount: 1)); |
} |
_list.add(value); |
@@ -95,9 +133,12 @@ class ObservableList<E> extends ListBase<E> with ChangeNotifier { |
void addAll(Iterable<E> iterable) { |
int len = _list.length; |
_list.addAll(iterable); |
+ |
+ notifyPropertyChange(#length, len, _list.length); |
+ |
int added = _list.length - len; |
- if (hasObservers && added > 0) { |
- _recordChange(new ListChangeRecord(len, addedCount: added)); |
+ if (_hasListObservers && added > 0) { |
+ _recordChange(new ListChangeRecord(this, len, addedCount: added)); |
} |
} |
@@ -113,14 +154,16 @@ class ObservableList<E> extends ListBase<E> with ChangeNotifier { |
void removeRange(int start, int end) { |
_rangeCheck(start, end); |
- int length = end - start; |
- _list.setRange(start, this.length - length, this, end); |
- |
+ int rangeLength = end - start; |
int len = _list.length; |
- _list.length -= length; |
- if (hasObservers && length > 0) { |
- _recordChange(new ListChangeRecord(start, removedCount: length)); |
+ |
+ notifyPropertyChange(#length, len, len - rangeLength); |
+ if (_hasListObservers && rangeLength > 0) { |
+ _recordChange(new ListChangeRecord(this, start, |
+ removed: _list.getRange(start, end).toList())); |
} |
+ |
+ _list.removeRange(start, end); |
} |
void insertAll(int index, Iterable<E> iterable) { |
@@ -141,8 +184,11 @@ class ObservableList<E> extends ListBase<E> with ChangeNotifier { |
_list.setRange(index + insertionLength, this.length, this, index); |
_list.setAll(index, iterable); |
- if (hasObservers && insertionLength > 0) { |
- _recordChange(new ListChangeRecord(index, addedCount: insertionLength)); |
+ notifyPropertyChange(#length, len, _list.length); |
+ |
+ if (_hasListObservers && insertionLength > 0) { |
+ _recordChange(new ListChangeRecord(this, index, |
+ addedCount: insertionLength)); |
} |
} |
@@ -160,8 +206,10 @@ class ObservableList<E> extends ListBase<E> with ChangeNotifier { |
if (index is! int) throw new ArgumentError(index); |
_list.length++; |
_list.setRange(index + 1, length, this, index); |
- if (hasObservers) { |
- _recordChange(new ListChangeRecord(index, addedCount: 1)); |
+ |
+ notifyPropertyChange(#length, _list.length - 1, _list.length); |
+ if (_hasListObservers) { |
+ _recordChange(new ListChangeRecord(this, index, addedCount: 1)); |
} |
_list[index] = element; |
} |
@@ -183,102 +231,42 @@ class ObservableList<E> extends ListBase<E> with ChangeNotifier { |
} |
void _recordChange(ListChangeRecord record) { |
+ if (!_hasListObservers) return; |
+ |
if (_listRecords == null) { |
_listRecords = []; |
- scheduleMicrotask(deliverChanges); |
+ scheduleMicrotask(deliverListChanges); |
} |
_listRecords.add(record); |
} |
- bool deliverChanges() { |
+ bool deliverListChanges() { |
if (_listRecords == null) return false; |
- _summarizeRecords(); |
- return super.deliverChanges(); |
+ var records = projectListSplices(this, _listRecords); |
+ _listRecords = null; |
+ |
+ if (_hasListObservers) { |
+ _listChanges.add(new UnmodifiableListView<ListChangeRecord>(records)); |
+ return true; |
+ } |
+ return false; |
} |
/** |
- * We need to summarize change records. Consumers of these records want to |
- * apply the batch sequentially, and ensure that they can find inserted |
- * items by looking at that position in the list. This property does not |
- * hold in our record-as-you-go records. Consider: |
+ * Calculates the changes to the list, if lacking individual splice mutation |
+ * information. |
* |
- * var model = toObservable(['a', 'b']); |
- * model.removeAt(1); |
- * model.insertAll(0, ['c', 'd', 'e']); |
- * model.removeRange(1, 3); |
- * model.insert(1, 'f'); |
+ * This is not needed for change records produced by [ObservableList] itself, |
+ * but it can be used if the list instance was replaced by another list. |
* |
- * Here, we inserted some records and then removed some of them. |
- * If someone processed these records naively, they would "play back" the |
- * insert incorrectly, because those items will be shifted. |
+ * The minimal set of splices can be synthesized given the previous state and |
+ * final state of a list. The basic approach is to calculate the edit distance |
+ * matrix and choose the shortest path through it. |
* |
- * We summarize changes using a straightforward technique: |
- * Simulate the moves and use the final item positions to synthesize a |
- * new list of changes records. This has the advantage of not depending |
- * on the actual *values*, so we don't need to perform N^2 edit |
+ * Complexity is `O(l * p)` where `l` is the length of the current list and |
+ * `p` is the length of the old list. |
*/ |
- // TODO(jmesserly): there's probably something smarter here, but this |
- // algorithm is pretty simple. It has complexity equivalent to the original |
- // list modifications. |
- // One simple idea: we can simply update the index map as we do the operations |
- // to the list, then produce the records at the end. |
- void _summarizeRecords() { |
- int oldLength = length; |
- for (var r in _listRecords) { |
- oldLength += r.removedCount - r.addedCount; |
- } |
- |
- if (length != oldLength) { |
- notifyPropertyChange(#length, oldLength, length); |
- } |
- |
- if (_listRecords.length == 1) { |
- notifyChange(_listRecords[0]); |
- _listRecords = null; |
- return; |
- } |
- |
- var items = []; |
- for (int i = 0; i < oldLength; i++) items.add(i); |
- for (var r in _listRecords) { |
- items.removeRange(r.index, r.index + r.removedCount); |
- |
- // Represent inserts with -1. |
- items.insertAll(r.index, new List.filled(r.addedCount, -1)); |
- } |
- assert(items.length == length); |
- |
- _listRecords = null; |
- |
- int index = 0; |
- int offset = 0; |
- while (index < items.length) { |
- // Skip unchanged items. |
- while (index < items.length && items[index] == index + offset) { |
- index++; |
- } |
- |
- // Find inserts |
- int startIndex = index; |
- while (index < items.length && items[index] == -1) { |
- index++; |
- } |
- |
- int added = index - startIndex; |
- |
- // Use the delta between our actual and expected position to determine |
- // how much was removed. |
- int actualItem = index < items.length ? items[index] : oldLength; |
- int expectedItem = startIndex + offset; |
- |
- int removed = actualItem - expectedItem; |
- |
- if (added > 0 || removed > 0) { |
- notifyChange(new ListChangeRecord(startIndex, addedCount: added, |
- removedCount: removed)); |
- } |
- |
- offset += removed - added; |
- } |
- } |
+ static List<ListChangeRecord> calculateChangeRecords( |
+ List<Object> oldValue, List<Object> newValue) => |
+ calcSplices(newValue, 0, newValue.length, oldValue, 0, oldValue.length); |
} |