Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library observe.src.observable_list; | 5 library observe.src.observable_list; |
| 6 | 6 |
| 7 import 'dart:async'; | 7 import 'dart:async'; |
| 8 import 'dart:collection' show ListBase; | 8 import 'dart:collection' show ListBase, UnmodifiableListView; |
| 9 import 'package:observe/observe.dart'; | 9 import 'package:observe/observe.dart'; |
| 10 import 'list_diff.dart' show projectListSplices, calcSplices; | |
| 10 | 11 |
| 11 /** | 12 /** |
| 12 * Represents an observable list of model values. If any items are added, | 13 * Represents an observable list of model values. If any items are added, |
| 13 * removed, or replaced, then observers that are listening to [changes] | 14 * removed, or replaced, then observers that are listening to [changes] |
| 14 * will be notified. | 15 * will be notified. |
| 15 */ | 16 */ |
| 16 class ObservableList<E> extends ListBase<E> with ChangeNotifier { | 17 class ObservableList<E> extends ListBase<E> with ChangeNotifier { |
| 17 List<ListChangeRecord> _listRecords; | 18 List<ListChangeRecord> _listRecords; |
| 18 | 19 |
| 20 StreamController _listChanges; | |
| 21 | |
| 19 /** The inner [List<E>] with the actual storage. */ | 22 /** The inner [List<E>] with the actual storage. */ |
| 20 final List<E> _list; | 23 final List<E> _list; |
| 21 | 24 |
| 22 /** | 25 /** |
| 23 * Creates an observable list of the given [length]. | 26 * Creates an observable list of the given [length]. |
| 24 * | 27 * |
| 25 * If no [length] argument is supplied an extendable list of | 28 * If no [length] argument is supplied an extendable list of |
| 26 * length 0 is created. | 29 * length 0 is created. |
| 27 * | 30 * |
| 28 * If a [length] argument is supplied, a fixed size list of that | 31 * If a [length] argument is supplied, a fixed size list of that |
| 29 * length is created. | 32 * length is created. |
| 30 */ | 33 */ |
| 31 ObservableList([int length]) | 34 ObservableList([int length]) |
| 32 : _list = length != null ? new List<E>(length) : <E>[]; | 35 : _list = length != null ? new List<E>(length) : <E>[]; |
| 33 | 36 |
| 34 /** | 37 /** |
| 35 * Creates an observable list with the elements of [other]. The order in | 38 * Creates an observable list with the elements of [other]. The order in |
| 36 * the list will be the order provided by the iterator of [other]. | 39 * the list will be the order provided by the iterator of [other]. |
| 37 */ | 40 */ |
| 38 factory ObservableList.from(Iterable<E> other) => | 41 factory ObservableList.from(Iterable<E> other) => |
| 39 new ObservableList<E>()..addAll(other); | 42 new ObservableList<E>()..addAll(other); |
| 40 | 43 |
| 44 /** | |
| 45 * The stream of summarized list changes, delivered asynchronously. | |
| 46 * | |
| 47 * Each list change record contains information about an individual mutation. | |
| 48 * The records are projected so they can be applied sequentially. For example, | |
| 49 * this set of mutations: | |
| 50 * | |
| 51 * var model = new ObservableList.from(['a', 'b']); | |
| 52 * model.listChanges.listen((records) => records.forEach(print)); | |
| 53 * model.removeAt(1); | |
| 54 * model.insertAll(0, ['c', 'd', 'e']); | |
| 55 * model.removeRange(1, 3); | |
| 56 * model.insert(1, 'f'); | |
| 57 * | |
| 58 * The change records will be summarized so they can be "played back", using | |
| 59 * the final list positions to figure out which item was added: | |
| 60 * | |
| 61 * #<ListChangeRecord index: 0, removed: [], addedCount: 2> | |
| 62 * #<ListChangeRecord index: 3, removed: [b], addedCount: 0> | |
|
Siggi Cherem (dart-lang)
2013/10/31 00:38:45
I'm not sure I follow this summary.
The operation
Jennifer Messerly
2013/10/31 02:19:34
it's removeAt(1) ... so it removes 'b' not 'a' :)
| |
| 63 * | |
| 64 * [deliverChanges] can be called to force synchronous delivery. | |
| 65 */ | |
| 66 Stream<List<ListChangeRecord>> get listChanges { | |
| 67 if (_listChanges == null) { | |
| 68 // TODO(jmesserly): split observed/unobserved notions? | |
| 69 _listChanges = new StreamController.broadcast(sync: true, | |
| 70 onCancel: () { _listChanges = null; }); | |
| 71 } | |
| 72 return _listChanges.stream; | |
| 73 } | |
| 74 | |
| 75 bool get _hasListObservers => | |
| 76 _listChanges != null && _listChanges.hasListener; | |
| 77 | |
| 41 @reflectable int get length => _list.length; | 78 @reflectable int get length => _list.length; |
| 42 | 79 |
| 43 @reflectable set length(int value) { | 80 @reflectable set length(int value) { |
| 44 int len = _list.length; | 81 int len = _list.length; |
| 45 if (len == value) return; | 82 if (len == value) return; |
| 46 | 83 |
| 47 // Produce notifications if needed | 84 // Produce notifications if needed |
| 48 if (hasObservers) { | 85 notifyPropertyChange(#length, len, value); |
| 86 if (_hasListObservers) { | |
| 49 if (value < len) { | 87 if (value < len) { |
| 50 // Remove items, then adjust length. Note the reverse order. | 88 _recordChange(new ListChangeRecord(this, value, |
| 51 _recordChange(new ListChangeRecord(value, removedCount: len - value)); | 89 removed: _list.getRange(value, len).toList())); |
| 52 } else { | 90 } else { |
| 53 // Adjust length then add items | 91 _recordChange(new ListChangeRecord(this, len, addedCount: value - len)); |
| 54 _recordChange(new ListChangeRecord(len, addedCount: value - len)); | |
| 55 } | 92 } |
| 56 } | 93 } |
| 57 | 94 |
| 58 _list.length = value; | 95 _list.length = value; |
| 59 } | 96 } |
| 60 | 97 |
| 61 @reflectable E operator [](int index) => _list[index]; | 98 @reflectable E operator [](int index) => _list[index]; |
| 62 | 99 |
| 63 @reflectable void operator []=(int index, E value) { | 100 @reflectable void operator []=(int index, E value) { |
| 64 var oldValue = _list[index]; | 101 var oldValue = _list[index]; |
|
Siggi Cherem (dart-lang)
2013/10/31 00:38:45
So what happens if I use .changes instead of .list
Jennifer Messerly
2013/10/31 02:19:34
.changes only gives property changes (including "l
| |
| 65 if (hasObservers) { | 102 if (_hasListObservers) { |
| 66 _recordChange(new ListChangeRecord(index, addedCount: 1, | 103 _recordChange(new ListChangeRecord(this, index, addedCount: 1, |
| 67 removedCount: 1)); | 104 removed: [oldValue])); |
| 68 } | 105 } |
| 69 _list[index] = value; | 106 _list[index] = value; |
| 70 } | 107 } |
| 71 | 108 |
| 72 // The following methods are here so that we can provide nice change events. | 109 // The following methods are here so that we can provide nice change events. |
| 73 | 110 |
| 74 void setAll(int index, Iterable<E> iterable) { | 111 void setAll(int index, Iterable<E> iterable) { |
| 75 if (iterable is! List && iterable is! Set) { | 112 if (iterable is! List && iterable is! Set) { |
| 76 iterable = iterable.toList(); | 113 iterable = iterable.toList(); |
| 77 } | 114 } |
| 78 var len = iterable.length; | 115 var len = iterable.length; |
| 116 if (_hasListObservers && len > 0) { | |
| 117 _recordChange(new ListChangeRecord(this, index, addedCount: len, | |
| 118 removed: _list.getRange(index, len).toList())); | |
| 119 } | |
| 79 _list.setAll(index, iterable); | 120 _list.setAll(index, iterable); |
| 80 if (hasObservers && len > 0) { | |
| 81 _recordChange( | |
| 82 new ListChangeRecord(index, addedCount: len, removedCount: len)); | |
| 83 } | |
| 84 } | 121 } |
| 85 | 122 |
| 86 void add(E value) { | 123 void add(E value) { |
| 87 int len = _list.length; | 124 int len = _list.length; |
| 88 if (hasObservers) { | 125 notifyPropertyChange(#length, len, len + 1); |
| 89 _recordChange(new ListChangeRecord(len, addedCount: 1)); | 126 if (_hasListObservers) { |
| 127 _recordChange(new ListChangeRecord(this, len, addedCount: 1)); | |
| 90 } | 128 } |
| 91 | 129 |
| 92 _list.add(value); | 130 _list.add(value); |
| 93 } | 131 } |
| 94 | 132 |
| 95 void addAll(Iterable<E> iterable) { | 133 void addAll(Iterable<E> iterable) { |
| 96 int len = _list.length; | 134 int len = _list.length; |
| 97 _list.addAll(iterable); | 135 _list.addAll(iterable); |
| 136 | |
| 137 notifyPropertyChange(#length, len, _list.length); | |
| 138 | |
| 98 int added = _list.length - len; | 139 int added = _list.length - len; |
| 99 if (hasObservers && added > 0) { | 140 if (_hasListObservers && added > 0) { |
| 100 _recordChange(new ListChangeRecord(len, addedCount: added)); | 141 _recordChange(new ListChangeRecord(this, len, addedCount: added)); |
| 101 } | 142 } |
| 102 } | 143 } |
| 103 | 144 |
| 104 bool remove(Object element) { | 145 bool remove(Object element) { |
| 105 for (int i = 0; i < this.length; i++) { | 146 for (int i = 0; i < this.length; i++) { |
| 106 if (this[i] == element) { | 147 if (this[i] == element) { |
| 107 removeRange(i, i + 1); | 148 removeRange(i, i + 1); |
| 108 return true; | 149 return true; |
| 109 } | 150 } |
| 110 } | 151 } |
| 111 return false; | 152 return false; |
| 112 } | 153 } |
| 113 | 154 |
| 114 void removeRange(int start, int end) { | 155 void removeRange(int start, int end) { |
| 115 _rangeCheck(start, end); | 156 _rangeCheck(start, end); |
| 116 int length = end - start; | 157 int rangeLength = end - start; |
| 117 _list.setRange(start, this.length - length, this, end); | 158 int len = _list.length; |
| 118 | 159 |
| 119 int len = _list.length; | 160 notifyPropertyChange(#length, len, len - rangeLength); |
| 120 _list.length -= length; | 161 if (_hasListObservers && rangeLength > 0) { |
| 121 if (hasObservers && length > 0) { | 162 _recordChange(new ListChangeRecord(this, start, |
| 122 _recordChange(new ListChangeRecord(start, removedCount: length)); | 163 removed: _list.getRange(start, end).toList())); |
| 123 } | 164 } |
| 165 | |
| 166 _list.removeRange(start, end); | |
| 124 } | 167 } |
| 125 | 168 |
| 126 void insertAll(int index, Iterable<E> iterable) { | 169 void insertAll(int index, Iterable<E> iterable) { |
| 127 if (index < 0 || index > length) { | 170 if (index < 0 || index > length) { |
| 128 throw new RangeError.range(index, 0, length); | 171 throw new RangeError.range(index, 0, length); |
| 129 } | 172 } |
| 130 // TODO(floitsch): we can probably detect more cases. | 173 // TODO(floitsch): we can probably detect more cases. |
| 131 if (iterable is! List && iterable is! Set) { | 174 if (iterable is! List && iterable is! Set) { |
| 132 iterable = iterable.toList(); | 175 iterable = iterable.toList(); |
| 133 } | 176 } |
| 134 int insertionLength = iterable.length; | 177 int insertionLength = iterable.length; |
| 135 // There might be errors after the length change, in which case the list | 178 // There might be errors after the length change, in which case the list |
| 136 // will end up being modified but the operation not complete. Unless we | 179 // will end up being modified but the operation not complete. Unless we |
| 137 // always go through a "toList" we can't really avoid that. | 180 // always go through a "toList" we can't really avoid that. |
| 138 int len = _list.length; | 181 int len = _list.length; |
| 139 _list.length += insertionLength; | 182 _list.length += insertionLength; |
| 140 | 183 |
| 141 _list.setRange(index + insertionLength, this.length, this, index); | 184 _list.setRange(index + insertionLength, this.length, this, index); |
| 142 _list.setAll(index, iterable); | 185 _list.setAll(index, iterable); |
| 143 | 186 |
| 144 if (hasObservers && insertionLength > 0) { | 187 notifyPropertyChange(#length, len, _list.length); |
| 145 _recordChange(new ListChangeRecord(index, addedCount: insertionLength)); | 188 |
| 189 if (_hasListObservers && insertionLength > 0) { | |
| 190 _recordChange(new ListChangeRecord(this, index, | |
| 191 addedCount: insertionLength)); | |
| 146 } | 192 } |
| 147 } | 193 } |
| 148 | 194 |
| 149 void insert(int index, E element) { | 195 void insert(int index, E element) { |
| 150 if (index < 0 || index > length) { | 196 if (index < 0 || index > length) { |
| 151 throw new RangeError.range(index, 0, length); | 197 throw new RangeError.range(index, 0, length); |
| 152 } | 198 } |
| 153 if (index == length) { | 199 if (index == length) { |
| 154 add(element); | 200 add(element); |
| 155 return; | 201 return; |
| 156 } | 202 } |
| 157 // We are modifying the length just below the is-check. Without the check | 203 // We are modifying the length just below the is-check. Without the check |
| 158 // Array.copy could throw an exception, leaving the list in a bad state | 204 // Array.copy could throw an exception, leaving the list in a bad state |
| 159 // (with a length that has been increased, but without a new element). | 205 // (with a length that has been increased, but without a new element). |
| 160 if (index is! int) throw new ArgumentError(index); | 206 if (index is! int) throw new ArgumentError(index); |
| 161 _list.length++; | 207 _list.length++; |
| 162 _list.setRange(index + 1, length, this, index); | 208 _list.setRange(index + 1, length, this, index); |
| 163 if (hasObservers) { | 209 |
| 164 _recordChange(new ListChangeRecord(index, addedCount: 1)); | 210 notifyPropertyChange(#length, _list.length - 1, _list.length); |
| 211 if (_hasListObservers) { | |
| 212 _recordChange(new ListChangeRecord(this, index, addedCount: 1)); | |
| 165 } | 213 } |
| 166 _list[index] = element; | 214 _list[index] = element; |
| 167 } | 215 } |
| 168 | 216 |
| 169 | 217 |
| 170 E removeAt(int index) { | 218 E removeAt(int index) { |
| 171 E result = this[index]; | 219 E result = this[index]; |
| 172 removeRange(index, index + 1); | 220 removeRange(index, index + 1); |
| 173 return result; | 221 return result; |
| 174 } | 222 } |
| 175 | 223 |
| 176 void _rangeCheck(int start, int end) { | 224 void _rangeCheck(int start, int end) { |
| 177 if (start < 0 || start > this.length) { | 225 if (start < 0 || start > this.length) { |
| 178 throw new RangeError.range(start, 0, this.length); | 226 throw new RangeError.range(start, 0, this.length); |
| 179 } | 227 } |
| 180 if (end < start || end > this.length) { | 228 if (end < start || end > this.length) { |
| 181 throw new RangeError.range(end, start, this.length); | 229 throw new RangeError.range(end, start, this.length); |
| 182 } | 230 } |
| 183 } | 231 } |
| 184 | 232 |
| 185 void _recordChange(ListChangeRecord record) { | 233 void _recordChange(ListChangeRecord record) { |
| 234 if (!_hasListObservers) return; | |
| 235 | |
| 186 if (_listRecords == null) { | 236 if (_listRecords == null) { |
| 187 _listRecords = []; | 237 _listRecords = []; |
| 188 scheduleMicrotask(deliverChanges); | 238 scheduleMicrotask(deliverListChanges); |
| 189 } | 239 } |
| 190 _listRecords.add(record); | 240 _listRecords.add(record); |
| 191 } | 241 } |
| 192 | 242 |
| 193 bool deliverChanges() { | 243 bool deliverListChanges() { |
| 194 if (_listRecords == null) return false; | 244 if (_listRecords == null) return false; |
| 195 _summarizeRecords(); | 245 var records = projectListSplices(this, _listRecords); |
| 196 return super.deliverChanges(); | 246 _listRecords = null; |
| 247 | |
| 248 if (_hasListObservers) { | |
| 249 _listChanges.add(new UnmodifiableListView<ListChangeRecord>(records)); | |
| 250 return true; | |
| 251 } | |
| 252 return false; | |
| 197 } | 253 } |
| 198 | 254 |
| 199 /** | 255 /** |
| 200 * We need to summarize change records. Consumers of these records want to | 256 * Calculates the changes to the list, if lacking individual splice mutation |
| 201 * apply the batch sequentially, and ensure that they can find inserted | 257 * information. |
| 202 * items by looking at that position in the list. This property does not | |
| 203 * hold in our record-as-you-go records. Consider: | |
| 204 * | 258 * |
| 205 * var model = toObservable(['a', 'b']); | 259 * This is not needed for change records produced by [ObservableList] itself, |
| 206 * model.removeAt(1); | 260 * but it can be used if the list instance was replaced by another list. |
| 207 * model.insertAll(0, ['c', 'd', 'e']); | |
| 208 * model.removeRange(1, 3); | |
| 209 * model.insert(1, 'f'); | |
| 210 * | 261 * |
| 211 * Here, we inserted some records and then removed some of them. | 262 * The minimal set of splices can be synthesized given the previous state and |
| 212 * If someone processed these records naively, they would "play back" the | 263 * final state of a list. The basic approach is to calculate the edit distance |
| 213 * insert incorrectly, because those items will be shifted. | 264 * matrix and choose the shortest path through it. |
| 214 * | 265 * |
| 215 * We summarize changes using a straightforward technique: | 266 * Complexity is `O(l * p)` where `l` is the length of the current list and |
| 216 * Simulate the moves and use the final item positions to synthesize a | 267 * `p` is the length of the old list. |
| 217 * new list of changes records. This has the advantage of not depending | |
| 218 * on the actual *values*, so we don't need to perform N^2 edit | |
| 219 */ | 268 */ |
| 220 // TODO(jmesserly): there's probably something smarter here, but this | 269 static List<ListChangeRecord> calculateChangeRecords( |
| 221 // algorithm is pretty simple. It has complexity equivalent to the original | 270 List<Object> oldValue, List<Object> newValue) => |
| 222 // list modifications. | 271 calcSplices(newValue, 0, newValue.length, oldValue, 0, oldValue.length); |
| 223 // One simple idea: we can simply update the index map as we do the operations | |
| 224 // to the list, then produce the records at the end. | |
| 225 void _summarizeRecords() { | |
| 226 int oldLength = length; | |
| 227 for (var r in _listRecords) { | |
| 228 oldLength += r.removedCount - r.addedCount; | |
| 229 } | |
| 230 | |
| 231 if (length != oldLength) { | |
| 232 notifyPropertyChange(#length, oldLength, length); | |
| 233 } | |
| 234 | |
| 235 if (_listRecords.length == 1) { | |
| 236 notifyChange(_listRecords[0]); | |
| 237 _listRecords = null; | |
| 238 return; | |
| 239 } | |
| 240 | |
| 241 var items = []; | |
| 242 for (int i = 0; i < oldLength; i++) items.add(i); | |
| 243 for (var r in _listRecords) { | |
| 244 items.removeRange(r.index, r.index + r.removedCount); | |
| 245 | |
| 246 // Represent inserts with -1. | |
| 247 items.insertAll(r.index, new List.filled(r.addedCount, -1)); | |
| 248 } | |
| 249 assert(items.length == length); | |
| 250 | |
| 251 _listRecords = null; | |
| 252 | |
| 253 int index = 0; | |
| 254 int offset = 0; | |
| 255 while (index < items.length) { | |
| 256 // Skip unchanged items. | |
| 257 while (index < items.length && items[index] == index + offset) { | |
| 258 index++; | |
| 259 } | |
| 260 | |
| 261 // Find inserts | |
| 262 int startIndex = index; | |
| 263 while (index < items.length && items[index] == -1) { | |
| 264 index++; | |
| 265 } | |
| 266 | |
| 267 int added = index - startIndex; | |
| 268 | |
| 269 // Use the delta between our actual and expected position to determine | |
| 270 // how much was removed. | |
| 271 int actualItem = index < items.length ? items[index] : oldLength; | |
| 272 int expectedItem = startIndex + offset; | |
| 273 | |
| 274 int removed = actualItem - expectedItem; | |
| 275 | |
| 276 if (added > 0 || removed > 0) { | |
| 277 notifyChange(new ListChangeRecord(startIndex, addedCount: added, | |
| 278 removedCount: removed)); | |
| 279 } | |
| 280 | |
| 281 offset += removed - added; | |
| 282 } | |
| 283 } | |
| 284 } | 272 } |
| OLD | NEW |