OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2016, 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 observable.src.observable_list; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 import 'dart:collection' show ListBase, UnmodifiableListView; | 8 import 'dart:collection' show ListBase, UnmodifiableListView; |
9 import 'package:observe/observe.dart'; | 9 |
10 import 'list_diff.dart' show projectListSplices, calcSplices; | 10 import 'list_diff.dart' show ListChangeRecord, projectListSplices, calcSplices; |
| 11 import 'observable.dart' show Observable; |
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 class ObservableList<E> extends ListBase<E> with ChangeNotifier { | 16 class ObservableList<E> extends ListBase<E> with Observable { |
16 List<ListChangeRecord> _listRecords; | 17 List<ListChangeRecord> _listRecords; |
17 | 18 |
18 StreamController _listChanges; | 19 StreamController<List<ListChangeRecord>> _listChanges; |
19 | 20 |
20 /// The inner [List<E>] with the actual storage. | 21 /// The inner [List<E>] with the actual storage. |
21 final List<E> _list; | 22 final List<E> _list; |
22 | 23 |
23 /// Creates an observable list of the given [length]. | 24 /// Creates an observable list of the given [length]. |
24 /// | 25 /// |
25 /// If no [length] argument is supplied an extendable list of | 26 /// If no [length] argument is supplied an extendable list of |
26 /// length 0 is created. | 27 /// length 0 is created. |
27 /// | 28 /// |
28 /// If a [length] argument is supplied, a fixed size list of that | 29 /// If a [length] argument is supplied, a fixed size list of that |
29 /// length is created. | 30 /// length is created. |
30 ObservableList([int length]) | 31 ObservableList([int length]) |
31 : _list = length != null ? new List<E>(length) : <E>[]; | 32 : _list = length != null ? new List<E>(length) : <E>[]; |
32 | 33 |
| 34 /// Creates an observable list of the given [length]. |
| 35 /// |
| 36 /// This constructor exists to work around an issue in the VM whereby |
| 37 /// classes that derive from [ObservableList] and mixin other classes |
| 38 /// require a default generative constructor in the super class that |
| 39 /// does not take optional arguments. |
| 40 ObservableList.withLength(int length) : this(length); |
| 41 |
33 /// Creates an observable list with the elements of [other]. The order in | 42 /// Creates an observable list with the elements of [other]. The order in |
34 /// the list will be the order provided by the iterator of [other]. | 43 /// the list will be the order provided by the iterator of [other]. |
35 factory ObservableList.from(Iterable<E> other) => | 44 factory ObservableList.from(Iterable<E> other) => |
36 new ObservableList<E>()..addAll(other); | 45 new ObservableList<E>()..addAll(other); |
37 | 46 |
38 /// The stream of summarized list changes, delivered asynchronously. | 47 /// The stream of summarized list changes, delivered asynchronously. |
39 /// | 48 /// |
40 /// Each list change record contains information about an individual mutation. | 49 /// Each list change record contains information about an individual mutation. |
41 /// The records are projected so they can be applied sequentially. For | 50 /// The records are projected so they can be applied sequentially. For |
42 /// example, this set of mutations: | 51 /// example, this set of mutations: |
43 /// | 52 /// |
44 /// var model = new ObservableList.from(['a', 'b']); | 53 /// var model = new ObservableList.from(['a', 'b']); |
45 /// model.listChanges.listen((records) => records.forEach(print)); | 54 /// model.listChanges.listen((records) => records.forEach(print)); |
46 /// model.removeAt(1); | 55 /// model.removeAt(1); |
47 /// model.insertAll(0, ['c', 'd', 'e']); | 56 /// model.insertAll(0, ['c', 'd', 'e']); |
48 /// model.removeRange(1, 3); | 57 /// model.removeRange(1, 3); |
49 /// model.insert(1, 'f'); | 58 /// model.insert(1, 'f'); |
50 /// | 59 /// |
51 /// The change records will be summarized so they can be "played back", using | 60 /// The change records will be summarized so they can be "played back", using |
52 /// the final list positions to figure out which item was added: | 61 /// the final list positions to figure out which item was added: |
53 /// | 62 /// |
54 /// #<ListChangeRecord index: 0, removed: [], addedCount: 2> | 63 /// #<ListChangeRecord index: 0, removed: [], addedCount: 2> |
55 /// #<ListChangeRecord index: 3, removed: [b], addedCount: 0> | 64 /// #<ListChangeRecord index: 3, removed: [b], addedCount: 0> |
56 /// | 65 /// |
57 /// [deliverChanges] can be called to force synchronous delivery. | 66 /// [deliverChanges] can be called to force synchronous delivery. |
58 Stream<List<ListChangeRecord>> get listChanges { | 67 Stream<List<ListChangeRecord>> get listChanges { |
59 if (_listChanges == null) { | 68 if (_listChanges == null) { |
60 // TODO(jmesserly): split observed/unobserved notions? | 69 // TODO(jmesserly): split observed/unobserved notions? |
61 _listChanges = new StreamController.broadcast(sync: true, | 70 _listChanges = new StreamController.broadcast( |
62 onCancel: () { _listChanges = null; }); | 71 sync: true, |
| 72 onCancel: () { |
| 73 _listChanges = null; |
| 74 }, |
| 75 ); |
63 } | 76 } |
64 return _listChanges.stream; | 77 return _listChanges.stream; |
65 } | 78 } |
66 | 79 |
67 bool get hasListObservers => | 80 bool get hasListObservers => _listChanges != null && _listChanges.hasListener; |
68 _listChanges != null && _listChanges.hasListener; | |
69 | 81 |
70 @reflectable int get length => _list.length; | 82 int get length => _list.length; |
71 | 83 |
72 @reflectable set length(int value) { | 84 set length(int value) { |
73 int len = _list.length; | 85 int len = _list.length; |
74 if (len == value) return; | 86 if (len == value) return; |
75 | 87 |
76 // Produce notifications if needed | 88 // Produce notifications if needed |
77 _notifyChangeLength(len, value); | 89 _notifyChangeLength(len, value); |
78 if (hasListObservers) { | 90 if (hasListObservers) { |
79 if (value < len) { | 91 if (value < len) { |
80 _recordChange(new ListChangeRecord(this, value, | 92 _notifyListChange(value, removed: _list.getRange(value, len).toList()); |
81 removed: _list.getRange(value, len).toList())); | |
82 } else { | 93 } else { |
83 _recordChange(new ListChangeRecord(this, len, addedCount: value - len)); | 94 _notifyListChange(len, addedCount: value - len); |
84 } | 95 } |
85 } | 96 } |
86 | 97 |
87 _list.length = value; | 98 _list.length = value; |
88 } | 99 } |
89 | 100 |
90 @reflectable E operator [](int index) => _list[index]; | 101 E operator [](int index) => _list[index]; |
91 | 102 |
92 @reflectable void operator []=(int index, E value) { | 103 void operator []=(int index, E value) { |
93 var oldValue = _list[index]; | 104 E oldValue = _list[index]; |
94 if (hasListObservers && oldValue != value) { | 105 if (hasListObservers && oldValue != value) { |
95 _recordChange(new ListChangeRecord(this, index, addedCount: 1, | 106 _notifyListChange(index, addedCount: 1, removed: [oldValue]); |
96 removed: [oldValue])); | |
97 } | 107 } |
98 _list[index] = value; | 108 _list[index] = value; |
99 } | 109 } |
100 | 110 |
101 // Forwarders so we can reflect on the properties. | 111 // Forwarders so we can reflect on the properties. |
102 @reflectable bool get isEmpty => super.isEmpty; | 112 bool get isEmpty => super.isEmpty; |
103 @reflectable bool get isNotEmpty => super.isNotEmpty; | 113 bool get isNotEmpty => super.isNotEmpty; |
104 | 114 |
105 // TODO(jmesserly): should we support first/last/single? They're kind of | 115 // TODO(jmesserly): should we support first/last/single? They're kind of |
106 // dangerous to use in a path because they throw exceptions. Also we'd need | 116 // dangerous to use in a path because they throw exceptions. Also we'd need |
107 // to produce property change notifications which seems to conflict with our | 117 // to produce property change notifications which seems to conflict with our |
108 // existing list notifications. | 118 // existing list notifications. |
109 | 119 |
110 // The following methods are here so that we can provide nice change events. | 120 // The following methods are here so that we can provide nice change events. |
111 | 121 |
112 void setAll(int index, Iterable<E> iterable) { | 122 void setAll(int index, Iterable<E> iterable) { |
113 if (iterable is! List && iterable is! Set) { | 123 if (iterable is! List && iterable is! Set) { |
114 iterable = iterable.toList(); | 124 iterable = iterable.toList(); |
115 } | 125 } |
116 var len = iterable.length; | 126 int length = iterable.length; |
117 if (hasListObservers && len > 0) { | 127 if (hasListObservers && length > 0) { |
118 _recordChange(new ListChangeRecord(this, index, addedCount: len, | 128 _notifyListChange(index, |
119 removed: _list.getRange(index, len).toList())); | 129 addedCount: length, removed: _list.sublist(index, length)); |
120 } | 130 } |
121 _list.setAll(index, iterable); | 131 _list.setAll(index, iterable); |
122 } | 132 } |
123 | 133 |
124 void add(E value) { | 134 void add(E value) { |
125 int len = _list.length; | 135 int len = _list.length; |
126 _notifyChangeLength(len, len + 1); | 136 _notifyChangeLength(len, len + 1); |
127 if (hasListObservers) { | 137 if (hasListObservers) { |
128 _recordChange(new ListChangeRecord(this, len, addedCount: 1)); | 138 _notifyListChange(len, addedCount: 1); |
129 } | 139 } |
130 | 140 |
131 _list.add(value); | 141 _list.add(value); |
132 } | 142 } |
133 | 143 |
134 void addAll(Iterable<E> iterable) { | 144 void addAll(Iterable<E> iterable) { |
135 int len = _list.length; | 145 int len = _list.length; |
136 _list.addAll(iterable); | 146 _list.addAll(iterable); |
137 | 147 |
138 _notifyChangeLength(len, _list.length); | 148 _notifyChangeLength(len, _list.length); |
139 | 149 |
140 int added = _list.length - len; | 150 int added = _list.length - len; |
141 if (hasListObservers && added > 0) { | 151 if (hasListObservers && added > 0) { |
142 _recordChange(new ListChangeRecord(this, len, addedCount: added)); | 152 _notifyListChange(len, addedCount: added); |
143 } | 153 } |
144 } | 154 } |
145 | 155 |
146 bool remove(Object element) { | 156 bool remove(Object element) { |
147 for (int i = 0; i < this.length; i++) { | 157 for (int i = 0; i < this.length; i++) { |
148 if (this[i] == element) { | 158 if (this[i] == element) { |
149 removeRange(i, i + 1); | 159 removeRange(i, i + 1); |
150 return true; | 160 return true; |
151 } | 161 } |
152 } | 162 } |
153 return false; | 163 return false; |
154 } | 164 } |
155 | 165 |
156 void removeRange(int start, int end) { | 166 void removeRange(int start, int end) { |
157 _rangeCheck(start, end); | 167 _rangeCheck(start, end); |
158 int rangeLength = end - start; | 168 int rangeLength = end - start; |
159 int len = _list.length; | 169 int len = _list.length; |
160 | 170 |
161 _notifyChangeLength(len, len - rangeLength); | 171 _notifyChangeLength(len, len - rangeLength); |
162 if (hasListObservers && rangeLength > 0) { | 172 if (hasListObservers && rangeLength > 0) { |
163 _recordChange(new ListChangeRecord(this, start, | 173 _notifyListChange(start, removed: _list.getRange(start, end).toList()); |
164 removed: _list.getRange(start, end).toList())); | |
165 } | 174 } |
166 | 175 |
167 _list.removeRange(start, end); | 176 _list.removeRange(start, end); |
168 } | 177 } |
169 | 178 |
170 void insertAll(int index, Iterable<E> iterable) { | 179 void insertAll(int index, Iterable<E> iterable) { |
171 if (index < 0 || index > length) { | 180 if (index < 0 || index > length) { |
172 throw new RangeError.range(index, 0, length); | 181 throw new RangeError.range(index, 0, length); |
173 } | 182 } |
174 // TODO(floitsch): we can probably detect more cases. | 183 // TODO(floitsch): we can probably detect more cases. |
175 if (iterable is! List && iterable is! Set) { | 184 if (iterable is! List && iterable is! Set) { |
176 iterable = iterable.toList(); | 185 iterable = iterable.toList(); |
177 } | 186 } |
178 int insertionLength = iterable.length; | 187 int insertionLength = iterable.length; |
179 // There might be errors after the length change, in which case the list | 188 // There might be errors after the length change, in which case the list |
180 // will end up being modified but the operation not complete. Unless we | 189 // will end up being modified but the operation not complete. Unless we |
181 // always go through a "toList" we can't really avoid that. | 190 // always go through a "toList" we can't really avoid that. |
182 int len = _list.length; | 191 int len = _list.length; |
183 _list.length += insertionLength; | 192 _list.length += insertionLength; |
184 | 193 |
185 _list.setRange(index + insertionLength, this.length, this, index); | 194 _list.setRange(index + insertionLength, this.length, this, index); |
186 _list.setAll(index, iterable); | 195 _list.setAll(index, iterable); |
187 | 196 |
188 _notifyChangeLength(len, _list.length); | 197 _notifyChangeLength(len, _list.length); |
189 | 198 |
190 if (hasListObservers && insertionLength > 0) { | 199 if (hasListObservers && insertionLength > 0) { |
191 _recordChange(new ListChangeRecord(this, index, | 200 _notifyListChange(index, addedCount: insertionLength); |
192 addedCount: insertionLength)); | |
193 } | 201 } |
194 } | 202 } |
195 | 203 |
196 void insert(int index, E element) { | 204 void insert(int index, E element) { |
197 if (index < 0 || index > length) { | 205 if (index < 0 || index > length) { |
198 throw new RangeError.range(index, 0, length); | 206 throw new RangeError.range(index, 0, length); |
199 } | 207 } |
200 if (index == length) { | 208 if (index == length) { |
201 add(element); | 209 add(element); |
202 return; | 210 return; |
203 } | 211 } |
204 // We are modifying the length just below the is-check. Without the check | 212 // We are modifying the length just below the is-check. Without the check |
205 // Array.copy could throw an exception, leaving the list in a bad state | 213 // Array.copy could throw an exception, leaving the list in a bad state |
206 // (with a length that has been increased, but without a new element). | 214 // (with a length that has been increased, but without a new element). |
207 if (index is! int) throw new ArgumentError(index); | 215 if (index is! int) throw new ArgumentError(index); |
208 _list.length++; | 216 _list.length++; |
209 _list.setRange(index + 1, length, this, index); | 217 _list.setRange(index + 1, length, this, index); |
210 | 218 |
211 _notifyChangeLength(_list.length - 1, _list.length); | 219 _notifyChangeLength(_list.length - 1, _list.length); |
212 if (hasListObservers) { | 220 if (hasListObservers) { |
213 _recordChange(new ListChangeRecord(this, index, addedCount: 1)); | 221 _notifyListChange(index, addedCount: 1); |
214 } | 222 } |
215 _list[index] = element; | 223 _list[index] = element; |
216 } | 224 } |
217 | 225 |
218 | |
219 E removeAt(int index) { | 226 E removeAt(int index) { |
220 E result = this[index]; | 227 E result = this[index]; |
221 removeRange(index, index + 1); | 228 removeRange(index, index + 1); |
222 return result; | 229 return result; |
223 } | 230 } |
224 | 231 |
225 void _rangeCheck(int start, int end) { | 232 void _rangeCheck(int start, int end) { |
226 if (start < 0 || start > this.length) { | 233 if (start < 0 || start > this.length) { |
227 throw new RangeError.range(start, 0, this.length); | 234 throw new RangeError.range(start, 0, this.length); |
228 } | 235 } |
229 if (end < start || end > this.length) { | 236 if (end < start || end > this.length) { |
230 throw new RangeError.range(end, start, this.length); | 237 throw new RangeError.range(end, start, this.length); |
231 } | 238 } |
232 } | 239 } |
233 | 240 |
234 void _recordChange(ListChangeRecord record) { | 241 void _notifyListChange(int index, {List removed, int addedCount}) { |
235 if (!hasListObservers) return; | 242 if (!hasListObservers) return; |
236 | |
237 if (_listRecords == null) { | 243 if (_listRecords == null) { |
238 _listRecords = []; | 244 _listRecords = []; |
239 scheduleMicrotask(deliverListChanges); | 245 scheduleMicrotask(deliverListChanges); |
240 } | 246 } |
241 _listRecords.add(record); | 247 _listRecords.add(new ListChangeRecord(this, index, |
| 248 removed: removed, addedCount: addedCount)); |
242 } | 249 } |
243 | 250 |
244 void _notifyChangeLength(int oldValue, int newValue) { | 251 void _notifyChangeLength(int oldValue, int newValue) { |
245 notifyPropertyChange(#length, oldValue, newValue); | 252 notifyPropertyChange(#length, oldValue, newValue); |
246 notifyPropertyChange(#isEmpty, oldValue == 0, newValue == 0); | 253 notifyPropertyChange(#isEmpty, oldValue == 0, newValue == 0); |
247 notifyPropertyChange(#isNotEmpty, oldValue != 0, newValue != 0); | 254 notifyPropertyChange(#isNotEmpty, oldValue != 0, newValue != 0); |
248 } | 255 } |
249 | 256 |
250 /// Deprecated. Name had a typo, use [discardListChanges] instead. | |
251 @deprecated | |
252 void discardListChages() => discardListChanges(); | |
253 | |
254 void discardListChanges() { | 257 void discardListChanges() { |
255 // Leave _listRecords set so we don't schedule another delivery. | 258 // Leave _listRecords set so we don't schedule another delivery. |
256 if (_listRecords != null) _listRecords = []; | 259 if (_listRecords != null) _listRecords = []; |
257 } | 260 } |
258 | 261 |
259 bool deliverListChanges() { | 262 bool deliverListChanges() { |
260 if (_listRecords == null) return false; | 263 if (_listRecords == null) return false; |
261 var records = projectListSplices(this, _listRecords); | 264 List<ListChangeRecord> records = projectListSplices(this, _listRecords); |
262 _listRecords = null; | 265 _listRecords = null; |
263 | 266 |
264 if (hasListObservers && !records.isEmpty) { | 267 if (hasListObservers && records.isNotEmpty) { |
265 _listChanges.add(new UnmodifiableListView<ListChangeRecord>(records)); | 268 _listChanges.add(new UnmodifiableListView<ListChangeRecord>(records)); |
266 return true; | 269 return true; |
267 } | 270 } |
268 return false; | 271 return false; |
269 } | 272 } |
270 | 273 |
271 /// Calculates the changes to the list, if lacking individual splice mutation | 274 /// Calculates the changes to the list, if lacking individual splice mutation |
272 /// information. | 275 /// information. |
273 /// | 276 /// |
274 /// This is not needed for change records produced by [ObservableList] itself, | 277 /// This is not needed for change records produced by [ObservableList] itself, |
275 /// but it can be used if the list instance was replaced by another list. | 278 /// but it can be used if the list instance was replaced by another list. |
276 /// | 279 /// |
277 /// The minimal set of splices can be synthesized given the previous state and | 280 /// The minimal set of splices can be synthesized given the previous state and |
278 /// final state of a list. The basic approach is to calculate the edit | 281 /// final state of a list. The basic approach is to calculate the edit |
279 /// distance matrix and choose the shortest path through it. | 282 /// distance matrix and choose the shortest path through it. |
280 /// | 283 /// |
281 /// Complexity is `O(l * p)` where `l` is the length of the current list and | 284 /// Complexity is `O(l * p)` where `l` is the length of the current list and |
282 /// `p` is the length of the old list. | 285 /// `p` is the length of the old list. |
283 static List<ListChangeRecord> calculateChangeRecords( | 286 static List<ListChangeRecord> calculateChangeRecords( |
284 List<Object> oldValue, List<Object> newValue) => | 287 List<Object> oldValue, List<Object> newValue) => |
285 calcSplices(newValue, 0, newValue.length, oldValue, 0, oldValue.length); | 288 calcSplices(newValue, 0, newValue.length, oldValue, 0, oldValue.length); |
286 | 289 |
287 /// Updates the [previous] list using the change [records]. For added items, | 290 /// Updates the [previous] list using the [changeRecords]. For added items, |
288 /// the [current] list is used to find the current value. | 291 /// the [current] list is used to find the current value. |
289 static void applyChangeRecords(List<Object> previous, List<Object> current, | 292 static void applyChangeRecords(List<Object> previous, List<Object> current, |
290 List<ListChangeRecord> changeRecords) { | 293 List<ListChangeRecord> changeRecords) { |
291 | |
292 if (identical(previous, current)) { | 294 if (identical(previous, current)) { |
293 throw new ArgumentError("can't use same list for previous and current"); | 295 throw new ArgumentError("can't use same list for previous and current"); |
294 } | 296 } |
295 | 297 |
296 for (var change in changeRecords) { | 298 for (ListChangeRecord change in changeRecords) { |
297 int addEnd = change.index + change.addedCount; | 299 int addEnd = change.index + change.addedCount; |
298 int removeEnd = change.index + change.removed.length; | 300 int removeEnd = change.index + change.removed.length; |
299 | 301 |
300 var addedItems = current.getRange(change.index, addEnd); | 302 Iterable addedItems = current.getRange(change.index, addEnd); |
301 previous.replaceRange(change.index, removeEnd, addedItems); | 303 previous.replaceRange(change.index, removeEnd, addedItems); |
302 } | 304 } |
303 } | 305 } |
304 } | 306 } |
OLD | NEW |