Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(34)

Unified Diff: pkg/observe/lib/src/observable_list.dart

Issue 53743002: introduce ObservableList.listChanges to keep list changes separate from property changes (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « pkg/observe/lib/src/list_path_observer.dart ('k') | pkg/observe/lib/src/path_observer.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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);
}
« no previous file with comments | « pkg/observe/lib/src/list_path_observer.dart ('k') | pkg/observe/lib/src/path_observer.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698