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> |
| 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 } |
OLD | NEW |