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