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

Side by Side 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, 1 month 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 unified diff | Download patch | Annotate | Revision Log
« 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 »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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>
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];
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 }
OLDNEW
« 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