Refactor and deprecate parts of ObservableList... (#12)

* Refactor ObservableList, start more deprecations.

* More tests and fixes.

* Fix remaining tests.

* Add changelog for 0.18

* Dartfmt.

* Fix bad merge.

* Tiny changes.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6f9d8f..b41cd9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,21 @@
+## 0.18.0
+
+* Refactor and deprecate `ObservableList`-specific API
+    * `ObservableList.applyChangeRecords`
+    * `ObservableList.calculateChangeRecords`
+    * `ObservableList.withLength`
+    * `ObservableList.deliverListChanges`
+    * `ObservableList.discardListChanges`
+    * `ObservableList.hasListChanges`
+    * `ObservableList.listChanges`
+    * `ObservableList.notifyListChange`
+* Potentially breaking: `ObservableList` may no longer be extended
+
+It is also considered deprecated to be notified of `length`, `isEmpty`
+and `isNotEmpty` `PropertyChangeRecord`s on `ObservableList` - in a
+future release `ObservableList.changes` will be
+`Stream<List<ListChangeRecord>>`.
+
 ## 0.17.0+1
 
 * Revert `PropertyChangeMixin`, which does not work in dart2js
diff --git a/lib/observable.dart b/lib/observable.dart
index 516ff70..a2d1fca 100644
--- a/lib/observable.dart
+++ b/lib/observable.dart
@@ -5,10 +5,10 @@
 library observable;
 
 export 'src/change_notifier.dart' show ChangeNotifier, PropertyChangeNotifier;
+export 'src/collections.dart' show ObservableList;
 export 'src/differs.dart' show Differ, EqualityDiffer, ListDiffer, MapDiffer;
 export 'src/records.dart'
     show ChangeRecord, ListChangeRecord, MapChangeRecord, PropertyChangeRecord;
 export 'src/observable.dart';
-export 'src/observable_list.dart';
 export 'src/observable_map.dart';
 export 'src/to_observable.dart';
diff --git a/lib/src/change_notifier.dart b/lib/src/change_notifier.dart
index f60eb16..775df10 100644
--- a/lib/src/change_notifier.dart
+++ b/lib/src/change_notifier.dart
@@ -49,7 +49,6 @@
   ///
   /// Returns `true` if changes were emitted.
   @override
-  @protected
   @mustCallSuper
   bool deliverChanges() {
     List<ChangeRecord> changes;
diff --git a/lib/src/collections.dart b/lib/src/collections.dart
new file mode 100644
index 0000000..7aaff1d
--- /dev/null
+++ b/lib/src/collections.dart
@@ -0,0 +1 @@
+export 'collections/observable_list.dart';
diff --git a/lib/src/collections/observable_list.dart b/lib/src/collections/observable_list.dart
new file mode 100644
index 0000000..5cc643a
--- /dev/null
+++ b/lib/src/collections/observable_list.dart
@@ -0,0 +1,438 @@
+import 'dart:async';
+
+import 'package:collection/collection.dart';
+import 'package:observable/observable.dart';
+import 'package:observable/src/differs.dart';
+
+/// A [List] that broadcasts [changes] to subscribers for efficient mutations.
+///
+/// When client code expects a read heavy/write light workload, it is often more
+/// efficient to notify _when_ something has changed, instead of constantly
+/// diffing lists to find a single change (like an inserted record). You may
+/// accept an observable list to be notified of mutations:
+///     set names(List<String> names) {
+///       clearAndWrite(names);
+///       if (names is ObservableList<String>) {
+///         names.listChanges.listen(smallIncrementalUpdate);
+///       }
+///     }
+///
+/// *See [ListDiffer] to manually diff two lists instead*
+abstract class ObservableList<E> implements List<E>, Observable {
+  /// Applies [changes] to [previous] based on the [current] values.
+  ///
+  /// ## Deprecated
+  ///
+  /// If you need this functionality, copy it into your own library. The only
+  /// known usage is in `package:template_binding` - it will be upgraded before
+  /// removing this method.
+  @Deprecated('Use ListChangeRecord#apply instead')
+  static void applyChangeRecords/*<T>*/(
+    List/*<T>*/ previous,
+    List/*<T>*/ current,
+    List<ListChangeRecord/*<T>*/ > changes,
+  ) {
+    if (identical(previous, current)) {
+      throw new ArgumentError("Can't use same list for previous and current");
+    }
+    for (final change in changes) {
+      change.apply(previous);
+    }
+  }
+
+  /// Calculates change records between [previous] and [current].
+  ///
+  /// ## Deprecated
+  ///
+  /// This was moved into `ListDiffer.diff`.
+  @Deprecated('Use `ListDiffer.diff` instead')
+  static List<ListChangeRecord/*<T>*/ > calculateChangeRecords/*<T>*/(
+    List/*<T>*/ previous,
+    List/*<T>*/ current,
+  ) {
+    return const ListDiffer/*<T>*/().diff(previous, current);
+  }
+
+  /// Creates an observable list of the given [length].
+  factory ObservableList([int length]) {
+    final list = length != null ? new List<E>(length) : <E>[];
+    return new _ObservableDelegatingList(list);
+  }
+
+  /// Create a new observable list.
+  ///
+  /// Optionally define a [list] to use as a backing store.
+  factory ObservableList.delegate([List<E> list]) {
+    return new _ObservableDelegatingList(list ?? <E>[]);
+  }
+
+  /// Create a new observable list from [elements].
+  factory ObservableList.from(Iterable<E> elements) {
+    return new _ObservableDelegatingList(elements.toList());
+  }
+
+  /// Creates a new observable list of the given [length].
+  @Deprecated('Use the default constructor')
+  factory ObservableList.withLength(int length) {
+    return new ObservableList<E>(length);
+  }
+
+  @Deprecated('No longer supported. Just use deliverChanges')
+  bool deliverListChanges();
+
+  @Deprecated('No longer supported')
+  void discardListChanges();
+
+  @Deprecated('The `changes` stream emits ListChangeRecord now')
+  bool get hasListObservers;
+
+  /// A stream of summarized list changes, delivered asynchronously.
+  @Deprecated(''
+      'The `changes` stream will soon only emit ListChangeRecord; '
+      'either continue to use this getter until removed, or use the changes '
+      'stream with a `Stream.where` guard')
+  Stream<List<ListChangeRecord<E>>> get listChanges;
+
+  @Deprecated('Should no longer be used external from ObservableList')
+  void notifyListChange(
+    int index, {
+    List<E> removed: const [],
+    int addedCount: 0,
+  });
+}
+
+class _ObservableDelegatingList<E> extends DelegatingList<E>
+    implements ObservableList<E> {
+  final _listChanges = new ChangeNotifier<ListChangeRecord<E>>();
+  final _propChanges = new ChangeNotifier<PropertyChangeRecord>();
+
+  StreamController<List<ChangeRecord>> _allChanges;
+
+  _ObservableDelegatingList(List<E> store) : super(store);
+
+  // Observable
+
+  @override
+  Stream<List<ChangeRecord>> get changes {
+    if (_allChanges == null) {
+      StreamSubscription listSub;
+      StreamSubscription propSub;
+      _allChanges = new StreamController<List<ChangeRecord>>.broadcast(
+          sync: true,
+          onListen: () {
+            listSub = listChanges.listen(_allChanges.add);
+            propSub = _propChanges.changes.listen(_allChanges.add);
+          },
+          onCancel: () {
+            listSub.cancel();
+            propSub.cancel();
+          });
+    }
+    return _allChanges.stream;
+  }
+
+  // ChangeNotifier (deprecated for ObservableList)
+
+  @override
+  bool deliverChanges() {
+    final deliveredListChanges = _listChanges.deliverChanges();
+    final deliveredPropChanges = _propChanges.deliverChanges();
+    return deliveredListChanges || deliveredPropChanges;
+  }
+
+  @override
+  void discardListChanges() {
+    // This used to do something, but now we just make it a no-op.
+  }
+
+  @override
+  bool get hasObservers {
+    return _listChanges.hasObservers || _propChanges.hasObservers;
+  }
+
+  @override
+  void notifyChange([ChangeRecord change]) {
+    if (change is ListChangeRecord<E>) {
+      _listChanges.notifyChange(change);
+    } else if (change is PropertyChangeRecord) {
+      _propChanges.notifyChange(change);
+    }
+  }
+
+  @override
+  /*=T*/ notifyPropertyChange/*<T>*/(
+    Symbol field,
+    /*=T*/
+    oldValue,
+    /*=T*/
+    newValue,
+  ) {
+    if (oldValue != newValue) {
+      _propChanges.notifyChange(
+        new PropertyChangeRecord/*<T>*/(this, field, oldValue, newValue),
+      );
+    }
+    return newValue;
+  }
+
+  @override
+  void observed() {}
+
+  @override
+  void unobserved() {}
+
+  // ObservableList (deprecated)
+
+  @override
+  bool deliverListChanges() => _listChanges.deliverChanges();
+
+  @override
+  bool get hasListObservers => _listChanges.hasObservers;
+
+  @override
+  Stream<List<ListChangeRecord<E>>> get listChanges {
+    return _listChanges.changes.map((r) => projectListSplices(this, r));
+  }
+
+  @override
+  void notifyListChange(
+    int index, {
+    List<E> removed: const [],
+    int addedCount: 0,
+  }) {
+    _listChanges.notifyChange(new ListChangeRecord<E>(
+      this,
+      index,
+      removed: removed,
+      addedCount: addedCount,
+    ));
+  }
+
+  void _notifyChangeLength(int oldLength, int newLength) {
+    notifyPropertyChange(#length, oldLength, newLength);
+    notifyPropertyChange(#isEmpty, oldLength == 0, newLength == 0);
+    notifyPropertyChange(#isNotEmpty, oldLength != 0, newLength != 0);
+  }
+
+  // List
+
+  @override
+  operator []=(int index, E newValue) {
+    final oldValue = this[index];
+    if (hasObservers && oldValue != newValue) {
+      notifyListChange(index, removed: [oldValue], addedCount: 1);
+    }
+    super[index] = newValue;
+  }
+
+  @override
+  void add(E value) {
+    if (hasObservers) {
+      _notifyChangeLength(length, length + 1);
+      notifyListChange(length, addedCount: 1);
+    }
+    super.add(value);
+  }
+
+  @override
+  void addAll(Iterable<E> values) {
+    final oldLength = this.length;
+    super.addAll(values);
+    final newLength = this.length;
+    final addedCount = newLength - oldLength;
+    if (hasObservers && addedCount > 0) {
+      notifyListChange(oldLength, addedCount: addedCount);
+      _notifyChangeLength(oldLength, newLength);
+    }
+  }
+
+  @override
+  void clear() {
+    if (hasObservers) {
+      notifyListChange(0, removed: toList());
+      _notifyChangeLength(length, 0);
+    }
+    super.clear();
+  }
+
+  @override
+  void fillRange(int start, int end, [E value]) {
+    if (hasObservers) {
+      notifyListChange(
+        start,
+        addedCount: end - start,
+        removed: getRange(start, end).toList(),
+      );
+    }
+    super.fillRange(start, end, value);
+  }
+
+  @override
+  void insert(int index, E element) {
+    super.insert(index, element);
+    if (hasObservers) {
+      notifyListChange(index, addedCount: 1);
+      _notifyChangeLength(length - 1, length);
+    }
+  }
+
+  @override
+  void insertAll(int index, Iterable<E> values) {
+    final oldLength = this.length;
+    super.insertAll(index, values);
+    final newLength = this.length;
+    final addedCount = newLength - oldLength;
+    if (hasObservers && addedCount > 0) {
+      notifyListChange(index, addedCount: addedCount);
+      _notifyChangeLength(oldLength, newLength);
+    }
+  }
+
+  @override
+  set length(int newLength) {
+    final currentLength = this.length;
+    if (currentLength == newLength) {
+      return;
+    }
+    if (hasObservers) {
+      if (newLength < currentLength) {
+        notifyListChange(
+          newLength,
+          removed: getRange(newLength, currentLength).toList(),
+        );
+      } else {
+        notifyListChange(currentLength, addedCount: newLength - currentLength);
+      }
+    }
+    super.length = newLength;
+    if (hasObservers) {
+      _notifyChangeLength(currentLength, newLength);
+    }
+  }
+
+  @override
+  bool remove(Object element) {
+    if (!hasObservers) {
+      return super.remove(element);
+    }
+    for (var i = 0; i < this.length; i++) {
+      if (this[i] == element) {
+        removeAt(i);
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @override
+  E removeAt(int index) {
+    if (hasObservers) {
+      final element = this[index];
+      notifyListChange(index, removed: [element]);
+      _notifyChangeLength(length, length - 1);
+    }
+    return super.removeAt(index);
+  }
+
+  @override
+  E removeLast() {
+    final element = super.removeLast();
+    if (hasObservers) {
+      notifyListChange(length, removed: [element]);
+      _notifyChangeLength(length + 1, length);
+    }
+    return element;
+  }
+
+  @override
+  void removeRange(int start, int end) {
+    final rangeLength = end - start;
+    if (hasObservers && rangeLength > 0) {
+      final removed = getRange(start, end).toList();
+      notifyListChange(start, removed: removed);
+      _notifyChangeLength(length, length - removed.length);
+    }
+    super.removeRange(start, end);
+  }
+
+  @override
+  void removeWhere(bool test(E element)) {
+    // We have to re-implement this if we have observers.
+    if (!hasObservers) return super.removeWhere(test);
+
+    // Produce as few change records as possible - if we have multiple removals
+    // in a sequence we want to produce a single record instead of a record for
+    // every element removed.
+    int firstRemovalIndex;
+
+    for (var i = 0; i < length; i++) {
+      var element = this[i];
+      if (test(element)) {
+        if (firstRemovalIndex == null) {
+          // This is the first item we've removed for this sequence.
+          firstRemovalIndex = i;
+        }
+      } else if (firstRemovalIndex != null) {
+        // We have a previous sequence of removals, but are not removing more.
+        removeRange(firstRemovalIndex, i--);
+        firstRemovalIndex = null;
+      }
+    }
+
+    // We have have a pending removal that was never finished.
+    if (firstRemovalIndex != null) {
+      removeRange(firstRemovalIndex, length);
+    }
+  }
+
+  @override
+  void replaceRange(int start, int end, Iterable<E> newContents) {
+    // This could be optimized not to emit two change records but would require
+    // code duplication with these methods. Since this is not used extremely
+    // often in my experience OK to just defer to these methods.
+    removeRange(start, end);
+    insertAll(start, newContents);
+  }
+
+  @override
+  void retainWhere(bool test(E element)) {
+    // This should be functionally the opposite of removeWhere.
+    removeWhere((E element) => !test(element));
+  }
+
+  @override
+  void setAll(int index, Iterable<E> elements) {
+    if (!hasObservers) {
+      super.setAll(index, elements);
+      return;
+    }
+    // Manual invocation of this method is required to get nicer change events.
+    var i = index;
+    final removed = <E>[];
+    for (var e in elements) {
+      removed.add(this[i]);
+      super[i++] = e;
+    }
+    if (removed.isNotEmpty) {
+      notifyListChange(index, removed: removed, addedCount: removed.length);
+    }
+  }
+
+  @override
+  void setRange(int start, int end, Iterable<E> elements, [int skipCount = 0]) {
+    if (!hasObservers) {
+      super.setRange(start, end, elements, skipCount);
+      return;
+    }
+    final iterator = elements.skip(skipCount).iterator..moveNext();
+    final removed = <E>[];
+    for (var i = start; i < end; i++) {
+      removed.add(super[i]);
+      super[i] = iterator.current;
+      iterator.moveNext();
+    }
+    if (removed.isNotEmpty) {
+      notifyListChange(start, removed: removed, addedCount: removed.length);
+    }
+  }
+}
diff --git a/lib/src/observable_list.dart b/lib/src/observable_list.dart
deleted file mode 100644
index 1618ce6..0000000
--- a/lib/src/observable_list.dart
+++ /dev/null
@@ -1,333 +0,0 @@
-// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-library observable.src.observable_list;
-
-import 'dart:async';
-import 'dart:collection' show ListBase, UnmodifiableListView;
-
-import 'differs.dart';
-import 'records.dart';
-import 'observable.dart' show Observable;
-
-/// Represents an observable list of model values. If any items are added,
-/// removed, or replaced, then observers that are listening to [changes]
-/// will be notified.
-class ObservableList<E> extends ListBase<E> with Observable {
-  List<ListChangeRecord> _listRecords;
-
-  StreamController<List<ListChangeRecord>> _listChanges;
-
-  /// The inner [List<E>] with the actual storage.
-  final List<E> _list;
-
-  /// Creates an observable list of the given [length].
-  ///
-  /// If no [length] argument is supplied an extendable list of
-  /// length 0 is created.
-  ///
-  /// If a [length] argument is supplied, a fixed size list of that
-  /// length is created.
-  ObservableList([int length])
-      : _list = length != null ? new List<E>(length) : <E>[];
-
-  /// Creates an observable list of the given [length].
-  ///
-  /// This constructor exists to work around an issue in the VM whereby
-  /// classes that derive from [ObservableList] and mixin other classes
-  /// require a default generative constructor in the super class that
-  /// does not take optional arguments.
-  ObservableList.withLength(int length) : this(length);
-
-  /// Creates an observable list with the elements of [other]. The order in
-  /// the list will be the order provided by the iterator of [other].
-  factory ObservableList.from(Iterable<E> other) =>
-      new ObservableList<E>()..addAll(other);
-
-  /// The stream of summarized list changes, delivered asynchronously.
-  ///
-  /// Each list change record contains information about an individual mutation.
-  /// The records are projected so they can be applied sequentially. For
-  /// example, this set of mutations:
-  ///
-  ///     var model = new ObservableList.from(['a', 'b']);
-  ///     model.listChanges.listen((records) => records.forEach(print));
-  ///     model.removeAt(1);
-  ///     model.insertAll(0, ['c', 'd', 'e']);
-  ///     model.removeRange(1, 3);
-  ///     model.insert(1, 'f');
-  ///
-  /// The change records will be summarized so they can be "played back", using
-  /// the final list positions to figure out which item was added:
-  ///
-  ///     #<ListChangeRecord index: 0, removed: [], addedCount: 2>
-  ///     #<ListChangeRecord index: 3, removed: [b], addedCount: 0>
-  ///
-  /// [deliverChanges] can be called to force synchronous delivery.
-  Stream<List<ListChangeRecord>> get listChanges {
-    if (_listChanges == null) {
-      // TODO(jmesserly): split observed/unobserved notions?
-      _listChanges = new StreamController.broadcast(
-        sync: true,
-        onCancel: () {
-          _listChanges = null;
-        },
-      );
-    }
-    return _listChanges.stream;
-  }
-
-  bool get hasListObservers => _listChanges != null && _listChanges.hasListener;
-
-  @override
-  int get length => _list.length;
-
-  @override
-  set length(int value) {
-    int len = _list.length;
-    if (len == value) return;
-
-    // Produce notifications if needed
-    _notifyChangeLength(len, value);
-    if (hasListObservers) {
-      if (value < len) {
-        _notifyListChange(value, removed: _list.getRange(value, len).toList());
-      } else {
-        _notifyListChange(len, addedCount: value - len);
-      }
-    }
-
-    _list.length = value;
-  }
-
-  @override
-  E operator [](int index) => _list[index];
-
-  @override
-  void operator []=(int index, E value) {
-    E oldValue = _list[index];
-    if (hasListObservers && oldValue != value) {
-      _notifyListChange(index, addedCount: 1, removed: [oldValue]);
-    }
-    _list[index] = value;
-  }
-
-  // Forwarders so we can reflect on the properties.
-  @override
-  bool get isEmpty => super.isEmpty;
-
-  @override
-  bool get isNotEmpty => super.isNotEmpty;
-
-  // TODO(jmesserly): should we support first/last/single? They're kind of
-  // dangerous to use in a path because they throw exceptions. Also we'd need
-  // to produce property change notifications which seems to conflict with our
-  // existing list notifications.
-
-  // The following methods are here so that we can provide nice change events.
-  @override
-  void setAll(int index, Iterable<E> iterable) {
-    if (iterable is! List && iterable is! Set) {
-      iterable = iterable.toList();
-    }
-    int length = iterable.length;
-    if (hasListObservers && length > 0) {
-      _notifyListChange(index,
-          addedCount: length, removed: _list.sublist(index, length));
-    }
-    _list.setAll(index, iterable);
-  }
-
-  @override
-  void add(E value) {
-    int len = _list.length;
-    _notifyChangeLength(len, len + 1);
-    if (hasListObservers) {
-      _notifyListChange(len, addedCount: 1);
-    }
-
-    _list.add(value);
-  }
-
-  @override
-  void addAll(Iterable<E> iterable) {
-    int len = _list.length;
-    _list.addAll(iterable);
-
-    _notifyChangeLength(len, _list.length);
-
-    int added = _list.length - len;
-    if (hasListObservers && added > 0) {
-      _notifyListChange(len, addedCount: added);
-    }
-  }
-
-  @override
-  bool remove(Object element) {
-    for (int i = 0; i < this.length; i++) {
-      if (this[i] == element) {
-        removeRange(i, i + 1);
-        return true;
-      }
-    }
-    return false;
-  }
-
-  @override
-  void removeRange(int start, int end) {
-    _rangeCheck(start, end);
-    int rangeLength = end - start;
-    int len = _list.length;
-
-    _notifyChangeLength(len, len - rangeLength);
-    if (hasListObservers && rangeLength > 0) {
-      _notifyListChange(start, removed: _list.getRange(start, end).toList());
-    }
-
-    _list.removeRange(start, end);
-  }
-
-  @override
-  void insertAll(int index, Iterable<E> iterable) {
-    if (index < 0 || index > length) {
-      throw new RangeError.range(index, 0, length);
-    }
-    // TODO(floitsch): we can probably detect more cases.
-    if (iterable is! List && iterable is! Set) {
-      iterable = iterable.toList();
-    }
-    int insertionLength = iterable.length;
-    // There might be errors after the length change, in which case the list
-    // will end up being modified but the operation not complete. Unless we
-    // always go through a "toList" we can't really avoid that.
-    int len = _list.length;
-    _list.length += insertionLength;
-
-    _list.setRange(index + insertionLength, this.length, this, index);
-    _list.setAll(index, iterable);
-
-    _notifyChangeLength(len, _list.length);
-
-    if (hasListObservers && insertionLength > 0) {
-      _notifyListChange(index, addedCount: insertionLength);
-    }
-  }
-
-  @override
-  void insert(int index, E element) {
-    if (index < 0 || index > length) {
-      throw new RangeError.range(index, 0, length);
-    }
-    if (index == length) {
-      add(element);
-      return;
-    }
-    // We are modifying the length just below the is-check. Without the check
-    // Array.copy could throw an exception, leaving the list in a bad state
-    // (with a length that has been increased, but without a new element).
-    if (index is! int) throw new ArgumentError(index);
-    _list.length++;
-    _list.setRange(index + 1, length, this, index);
-
-    _notifyChangeLength(_list.length - 1, _list.length);
-    if (hasListObservers) {
-      _notifyListChange(index, addedCount: 1);
-    }
-    _list[index] = element;
-  }
-
-  @override
-  E removeAt(int index) {
-    E result = this[index];
-    removeRange(index, index + 1);
-    return result;
-  }
-
-  void _rangeCheck(int start, int end) {
-    if (start < 0 || start > this.length) {
-      throw new RangeError.range(start, 0, this.length);
-    }
-    if (end < start || end > this.length) {
-      throw new RangeError.range(end, start, this.length);
-    }
-  }
-
-  void _notifyListChange(
-    int index, {
-    List removed: const [],
-    int addedCount: 0,
-  }) {
-    if (!hasListObservers) return;
-    if (_listRecords == null) {
-      _listRecords = [];
-      scheduleMicrotask(deliverListChanges);
-    }
-    _listRecords.add(new ListChangeRecord(
-      this,
-      index,
-      removed: removed,
-      addedCount: addedCount,
-    ));
-  }
-
-  void _notifyChangeLength(int oldValue, int newValue) {
-    notifyPropertyChange(#length, oldValue, newValue);
-    notifyPropertyChange(#isEmpty, oldValue == 0, newValue == 0);
-    notifyPropertyChange(#isNotEmpty, oldValue != 0, newValue != 0);
-  }
-
-  void discardListChanges() {
-    // Leave _listRecords set so we don't schedule another delivery.
-    if (_listRecords != null) _listRecords = [];
-  }
-
-  bool deliverListChanges() {
-    if (_listRecords == null) return false;
-    List<ListChangeRecord> records =
-        projectListSplices/*<E>*/(this, _listRecords);
-    _listRecords = null;
-
-    if (hasListObservers && records.isNotEmpty) {
-      _listChanges.add(new UnmodifiableListView<ListChangeRecord>(records));
-      return true;
-    }
-    return false;
-  }
-
-  /// Calculates the changes to the list, if lacking individual splice mutation
-  /// information.
-  ///
-  /// This is not needed for change records produced by [ObservableList] itself,
-  /// but it can be used if the list instance was replaced by another list.
-  ///
-  /// The minimal set of splices can be synthesized given the previous state and
-  /// final state of a list. The basic approach is to calculate the edit
-  /// distance matrix and choose the shortest path through it.
-  ///
-  /// Complexity is `O(l * p)` where `l` is the length of the current list and
-  /// `p` is the length of the old list.
-  static List<ListChangeRecord/*<E>*/ > calculateChangeRecords/*<E>*/(
-    List/*<E>*/ oldValue,
-    List/*<E>*/ newValue,
-  ) {
-    return const ListDiffer/*<E>*/().diff(oldValue, newValue);
-  }
-
-  /// Updates the [previous] list using the [changeRecords]. For added items,
-  /// the [current] list is used to find the current value.
-  static void applyChangeRecords(List<Object> previous, List<Object> current,
-      List<ListChangeRecord> changeRecords) {
-    if (identical(previous, current)) {
-      throw new ArgumentError("can't use same list for previous and current");
-    }
-
-    for (ListChangeRecord change in changeRecords) {
-      int addEnd = change.index + change.addedCount;
-      int removeEnd = change.index + change.removed.length;
-
-      Iterable addedItems = current.getRange(change.index, addEnd);
-      previous.replaceRange(change.index, removeEnd, addedItems);
-    }
-  }
-}
diff --git a/lib/src/records/property_change_record.dart b/lib/src/records/property_change_record.dart
index 74566d5..a606649 100644
--- a/lib/src/records/property_change_record.dart
+++ b/lib/src/records/property_change_record.dart
@@ -26,6 +26,20 @@
   );
 
   @override
+  bool operator ==(Object o) {
+    if (o is PropertyChangeRecord<T>) {
+      return identical(object, o.object) &&
+          name == o.name &&
+          oldValue == o.oldValue &&
+          newValue == o.newValue;
+    }
+    return false;
+  }
+
+  @override
+  int get hashCode => hash4(object, name, oldValue, newValue);
+
+  @override
   String toString() => ''
       '#<$PropertyChangeRecord $name from $oldValue to: $newValue';
 }
diff --git a/lib/src/to_observable.dart b/lib/src/to_observable.dart
index 3bf24b6..d117c89 100644
--- a/lib/src/to_observable.dart
+++ b/lib/src/to_observable.dart
@@ -7,7 +7,7 @@
 import 'dart:collection';
 
 import 'observable.dart' show Observable;
-import 'observable_list.dart' show ObservableList;
+import 'collections.dart' show ObservableList;
 import 'observable_map.dart' show ObservableMap;
 
 /// Converts the [Iterable] or [Map] to an [ObservableList] or [ObservableMap],
diff --git a/pubspec.yaml b/pubspec.yaml
index f326c5d..57ce0ad 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: observable
-version: 0.17.0+1
+version: 0.18.0
 author: Dart Team <misc@dartlang.org>
 description: Support for marking objects as observable
 homepage: https://github.com/dart-lang/observable
diff --git a/test/collections/observable_list_test.dart b/test/collections/observable_list_test.dart
new file mode 100644
index 0000000..b8e50d0
--- /dev/null
+++ b/test/collections/observable_list_test.dart
@@ -0,0 +1,444 @@
+import 'dart:async';
+
+import 'package:observable/observable.dart';
+import 'package:test/test.dart';
+
+main() {
+  group('$ObservableList', () {
+    group('list api', _runListTests);
+    _runObservableListTests();
+    _runDeprecatedTests();
+  });
+}
+
+bool _hasListChanges(List<ChangeRecord> c) {
+  return c.any((r) => r is ListChangeRecord);
+}
+
+bool _hasPropChanges(List<ChangeRecord> c) {
+  return c.any((r) => r is PropertyChangeRecord);
+}
+
+bool _onlyListChanges(ChangeRecord c) => c is ListChangeRecord;
+
+bool _onlyPropRecords(ChangeRecord c) => c is PropertyChangeRecord;
+
+_runListTests() {
+  // TODO(matanl): Can we run the List-API tests from the SDK?
+  // Any methods actually implemented by ObservableList are below, otherwise I
+  // am relying on the test suite for DelegatingList.
+  test('index set operator', () {
+    final list = new ObservableList<String>(1)..[0] = 'value';
+    expect(list, ['value']);
+  });
+
+  test('add', () {
+    final list = new ObservableList<String>()..add('value');
+    expect(list, ['value']);
+  });
+
+  test('addAll', () {
+    final list = new ObservableList<String>()..addAll(['a', 'b', 'c']);
+    expect(list, ['a', 'b', 'c']);
+  });
+
+  test('clear', () {
+    final list = new ObservableList<String>.from(['a', 'b', 'c'])..clear();
+    expect(list, isEmpty);
+  });
+
+  test('fillRange', () {
+    final list = new ObservableList<String>(5)..fillRange(0, 5, 'a');
+    expect(list, ['a', 'a', 'a', 'a', 'a']);
+  });
+
+  test('insert', () {
+    final list = new ObservableList<String>.from(['a', 'c'])..insert(1, 'b');
+    expect(list, ['a', 'b', 'c']);
+  });
+
+  test('insertAll', () {
+    final list = new ObservableList<String>.from(['c']);
+    list.insertAll(0, ['a', 'b']);
+    expect(list, ['a', 'b', 'c']);
+  });
+
+  test('length', () {
+    final list = new ObservableList<String>()..length = 3;
+    expect(list, [null, null, null]);
+    list.length = 1;
+    expect(list, [null]);
+    list.length = 0;
+    expect(list, isEmpty);
+  });
+
+  test('remove', () {
+    final list = new ObservableList<String>.from(['a', 'b', 'c']);
+    expect(list.remove('b'), isTrue);
+    expect(list, ['a', 'c']);
+  });
+
+  test('removeAt', () {
+    final list = new ObservableList<String>.from(['a', 'b', 'c']);
+    expect(list.removeAt(1), 'b');
+    expect(list, ['a', 'c']);
+  });
+
+  test('removeLast', () {
+    final list = new ObservableList<String>.from(['a', 'b', 'c']);
+    expect(list.removeLast(), 'c');
+  });
+
+  test('removeRange', () {
+    final list = new ObservableList<String>.from(['a', 'b', 'c']);
+    list.removeRange(0, 2);
+    expect(list, ['c']);
+  });
+
+  test('removeWhere', () {
+    final list = new ObservableList<String>.from(['a', 'b', 'a', 'b']);
+    list.removeWhere((v) => v == 'a');
+    expect(list, ['b', 'b']);
+  });
+
+  test('retainWhere', () {
+    final list = new ObservableList<String>.from(['a', 'b', 'a', 'b']);
+    list.retainWhere((v) => v == 'a');
+    expect(list, ['a', 'a']);
+  });
+
+  test('setAll', () {
+    final list = new ObservableList<String>.from(['a', 'b', 'c']);
+    list.setAll(1, ['d', 'f']);
+    expect(list, ['a', 'd', 'f']);
+  });
+
+  test('setRange', () {
+    final list = new ObservableList<String>.from(['a', 'b', 'c']);
+    list.setRange(0, 2, ['d', 'e']);
+    expect(list, ['d', 'e', 'c']);
+    list.setRange(1, 3, ['f', 'g', 'h'], 1);
+    expect(list, ['d', 'g', 'h']);
+  });
+}
+
+// These are the tests we will keep after deprecations occur.
+_runObservableListTests() {
+  group('content changes', () {
+    Completer<List<ListChangeRecord>> completer;
+    List<String> previousState;
+
+    ObservableList<String> list;
+    Stream<List<ChangeRecord>> stream;
+    StreamSubscription sub;
+
+    Future next() {
+      completer = new Completer<List<ListChangeRecord>>.sync();
+      return completer.future;
+    }
+
+    Future<Null> expectChanges(List<ListChangeRecord> changes) {
+      // Applying these change records in order should make the new list.
+      for (final change in changes) {
+        change.apply(previousState);
+      }
+      expect(list, previousState);
+
+      // If these fail, it might be safe to update if optimized/changed.
+      return next().then((actualChanges) {
+        expect(actualChanges, changes);
+      });
+    }
+
+    setUp(() {
+      previousState = ['a', 'b', 'c'];
+      list = new ObservableList<String>.from(previousState);
+      stream = list.changes.where(_hasListChanges);
+      sub = stream.listen((c) {
+        if (completer?.isCompleted == false) {
+          completer.complete(c.where(_onlyListChanges).toList());
+        }
+        previousState = list.toList();
+      });
+    });
+
+    tearDown(() => sub.cancel());
+
+    ListChangeRecord _delta(
+      int index, {
+      List removed: const [],
+      int addedCount: 0,
+    }) {
+      return new ListChangeRecord(
+        list,
+        index,
+        removed: removed,
+        addedCount: addedCount,
+      );
+    }
+
+    test('operator[]=', () async {
+      list[0] = 'd';
+      await expectChanges([
+        _delta(0, removed: ['a'], addedCount: 1),
+      ]);
+    });
+
+    test('add', () async {
+      list.add('d');
+      await expectChanges([
+        _delta(3, addedCount: 1),
+      ]);
+    });
+
+    test('addAll', () async {
+      list.addAll(['d', 'e']);
+      await expectChanges([
+        _delta(3, addedCount: 2),
+      ]);
+    });
+
+    test('clear', () async {
+      list.clear();
+      await expectChanges([
+        _delta(0, removed: ['a', 'b', 'c']),
+      ]);
+    });
+
+    test('fillRange', () async {
+      list.fillRange(1, 3, 'd');
+      await expectChanges([
+        _delta(1, removed: ['b', 'c'], addedCount: 2),
+      ]);
+    });
+
+    test('insert', () async {
+      list.insert(1, 'd');
+      await expectChanges([
+        _delta(1, addedCount: 1),
+      ]);
+    });
+
+    test('insertAll', () async {
+      list.insertAll(1, ['d', 'e']);
+      await expectChanges([
+        _delta(1, addedCount: 2),
+      ]);
+    });
+
+    test('length', () async {
+      list.length = 5;
+      await expectChanges([
+        _delta(3, addedCount: 2),
+      ]);
+      list.length = 1;
+      await expectChanges([
+        _delta(1, removed: ['b', 'c', null, null])
+      ]);
+    });
+
+    test('remove', () async {
+      list.remove('b');
+      await expectChanges([
+        _delta(1, removed: ['b'])
+      ]);
+    });
+
+    test('removeAt', () async {
+      list.removeAt(1);
+      await expectChanges([
+        _delta(1, removed: ['b'])
+      ]);
+    });
+
+    test('removeRange', () async {
+      list.removeRange(0, 2);
+      await expectChanges([
+        _delta(0, removed: ['a', 'b'])
+      ]);
+    });
+
+    test('removeWhere', () async {
+      list.removeWhere((s) => s == 'b');
+      await expectChanges([
+        _delta(1, removed: ['b'])
+      ]);
+    });
+
+    test('replaceRange', () async {
+      list.replaceRange(0, 2, ['d', 'e']);
+      await expectChanges([
+        // Normally would be
+        //   _delta(0, removed: ['a', 'b']),
+        //   _delta(0, addedCount: 2),
+        // But projectListSplices(...) optimizes to single record
+        _delta(0, removed: ['a', 'b'], addedCount: 2),
+      ]);
+    });
+
+    test('retainWhere', () async {
+      list.retainWhere((s) => s == 'b');
+      await expectChanges([
+        _delta(0, removed: ['a']),
+        _delta(1, removed: ['c']),
+      ]);
+    });
+
+    test('setAll', () async {
+      list.setAll(1, ['d', 'e']);
+      await expectChanges([
+        _delta(1, removed: ['b', 'c'], addedCount: 2),
+      ]);
+    });
+
+    test('setRange', () async {
+      list.setRange(0, 2, ['d', 'e']);
+      await expectChanges([
+        _delta(0, removed: ['a', 'b'], addedCount: 2),
+      ]);
+      list.setRange(1, 3, ['f', 'g', 'h'], 1);
+      await expectChanges([
+        _delta(1, removed: ['e', 'c'], addedCount: 2),
+      ]);
+    });
+  });
+}
+
+// These are tests we will remove after deprecations occur.
+_runDeprecatedTests() {
+  group('length changes', () {
+    Completer<List<ListChangeRecord>> completer;
+    ObservableList<String> list;
+    Stream<List<ChangeRecord>> stream;
+    StreamSubscription sub;
+
+    Future next() {
+      completer = new Completer<List<ListChangeRecord>>.sync();
+      return completer.future;
+    }
+
+    setUp(() {
+      list = new ObservableList<String>.from(['a', 'b', 'c']);
+      stream = list.changes.where(_hasPropChanges);
+      sub = stream.listen((c) {
+        if (completer?.isCompleted == false) {
+          completer.complete(c.where(_onlyPropRecords).toList());
+        }
+      });
+    });
+
+    tearDown(() => sub.cancel());
+
+    PropertyChangeRecord _length(int oldLength, int newLength) {
+      return new PropertyChangeRecord(list, #length, oldLength, newLength);
+    }
+
+    PropertyChangeRecord _isEmpty(bool oldValue, bool newValue) {
+      return new PropertyChangeRecord(list, #isEmpty, oldValue, newValue);
+    }
+
+    PropertyChangeRecord _isNotEmpty(bool oldValue, bool newValue) {
+      return new PropertyChangeRecord(list, #isNotEmpty, oldValue, newValue);
+    }
+
+    test('add', () async {
+      list.add('d');
+      expect(await next(), [
+        _length(3, 4),
+      ]);
+    });
+
+    test('addAll', () async {
+      list.addAll(['d', 'e']);
+      expect(await next(), [
+        _length(3, 5),
+      ]);
+    });
+
+    test('clear', () async {
+      list.clear();
+      expect(await next(), [
+        _length(3, 0),
+        _isEmpty(false, true),
+        _isNotEmpty(true, false),
+      ]);
+    });
+
+    test('insert', () async {
+      list.insert(0, 'd');
+      expect(await next(), [
+        _length(3, 4),
+      ]);
+    });
+
+    test('insertAll', () async {
+      list.insertAll(0, ['d', 'e']);
+      expect(await next(), [
+        _length(3, 5),
+      ]);
+    });
+
+    test('length', () async {
+      list.length = 5;
+      expect(await next(), [
+        _length(3, 5),
+      ]);
+
+      list.length = 0;
+      expect(await next(), [
+        _length(5, 0),
+        _isEmpty(false, true),
+        _isNotEmpty(true, false),
+      ]);
+
+      list.length = 1;
+      expect(await next(), [
+        _length(0, 1),
+        _isEmpty(true, false),
+        _isNotEmpty(false, true),
+      ]);
+    });
+
+    test('remove', () async {
+      list.remove('a');
+      expect(await next(), [
+        _length(3, 2),
+      ]);
+    });
+
+    test('removeAt', () async {
+      list.removeAt(0);
+      expect(await next(), [
+        _length(3, 2),
+      ]);
+    });
+
+    test('removeLast', () async {
+      list.removeLast();
+      expect(await next(), [
+        _length(3, 2),
+      ]);
+    });
+
+    test('removeRange', () async {
+      list.removeRange(0, 2);
+      expect(await next(), [
+        _length(3, 1),
+      ]);
+    });
+
+    test('removeWhere', () async {
+      list.removeWhere((s) => s == 'a');
+      expect(await next(), [
+        _length(3, 2),
+      ]);
+    });
+
+    test('retainWhere', () async {
+      list.retainWhere((s) => s == 'a');
+      expect(await next(), [
+        _length(3, 1),
+      ]);
+    });
+  });
+}
diff --git a/test/observable_list_test.dart b/test/observable_list_test.dart
deleted file mode 100644
index ed87d04..0000000
--- a/test/observable_list_test.dart
+++ /dev/null
@@ -1,342 +0,0 @@
-// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'dart:async';
-
-import 'package:observable/observable.dart';
-import 'package:test/test.dart';
-
-import 'observable_test_utils.dart';
-
-main() => _runTests();
-
-_runTests() {
-  // TODO(jmesserly): need all standard List API tests.
-
-  StreamSubscription sub, sub2;
-
-  sharedTearDown() {
-    list = null;
-    sub.cancel();
-    if (sub2 != null) {
-      sub2.cancel();
-      sub2 = null;
-    }
-  }
-
-  group('observe length', () {
-    ObservableList list;
-    List<ChangeRecord> changes;
-
-    setUp(() {
-      list = toObservable([1, 2, 3]) as ObservableList;
-      changes = null;
-      sub = list.changes.listen((records) {
-        changes = getPropertyChangeRecords(records, #length);
-      });
-    });
-
-    tearDown(sharedTearDown);
-
-    test('add changes length', () {
-      list.add(4);
-      expect(list, [1, 2, 3, 4]);
-      return new Future(() {
-        expectChanges(changes, [_lengthChange(3, 4)]);
-      });
-    });
-
-    test('removeObject changes length', () {
-      list.remove(2);
-      expect(list, orderedEquals([1, 3]));
-
-      return new Future(() {
-        expectChanges(changes, [_lengthChange(3, 2)]);
-      });
-    });
-
-    test('removeRange changes length', () {
-      list.add(4);
-      list.removeRange(1, 3);
-      expect(list, [1, 4]);
-      return new Future(() {
-        expectChanges(changes, [_lengthChange(3, 4), _lengthChange(4, 2)]);
-      });
-    });
-
-    test('removeWhere changes length', () {
-      list.add(2);
-      list.removeWhere((e) => e == 2);
-      expect(list, [1, 3]);
-      return new Future(() {
-        expectChanges(changes, [_lengthChange(3, 4), _lengthChange(4, 2)]);
-      });
-    });
-
-    test('length= changes length', () {
-      list.length = 5;
-      expect(list, [1, 2, 3, null, null]);
-      return new Future(() {
-        expectChanges(changes, [_lengthChange(3, 5)]);
-      });
-    });
-
-    test('[]= does not change length', () {
-      list[2] = 9000;
-      expect(list, [1, 2, 9000]);
-      return new Future(() {
-        expectChanges(changes, null);
-      });
-    });
-
-    test('clear changes length', () {
-      list.clear();
-      expect(list, []);
-      return new Future(() {
-        expectChanges(changes, [_lengthChange(3, 0)]);
-      });
-    });
-  });
-
-  group('observe index', () {
-    List<ListChangeRecord> changes;
-
-    setUp(() {
-      list = toObservable([1, 2, 3]) as ObservableList;
-      changes = null;
-      sub = list.listChanges.listen((List<ListChangeRecord> records) {
-        changes = getListChangeRecords(records, 1);
-      });
-    });
-
-    tearDown(sharedTearDown);
-
-    test('add does not change existing items', () {
-      list.add(4);
-      expect(list, [1, 2, 3, 4]);
-      return new Future(() {
-        expectChanges(changes, []);
-      });
-    });
-
-    test('[]= changes item', () {
-      list[1] = 777;
-      expect(list, [1, 777, 3]);
-      return new Future(() {
-        expectChanges(changes, [
-          _change(1, addedCount: 1, removed: [2])
-        ]);
-      });
-    });
-
-    test('[]= on a different item does not fire change', () {
-      list[2] = 9000;
-      expect(list, [1, 2, 9000]);
-      return new Future(() {
-        expectChanges(changes, []);
-      });
-    });
-
-    test('set multiple times results in one change', () {
-      list[1] = 777;
-      list[1] = 42;
-      expect(list, [1, 42, 3]);
-      return new Future(() {
-        expectChanges(changes, [
-          _change(1, addedCount: 1, removed: [2]),
-        ]);
-      });
-    });
-
-    test('set length without truncating item means no change', () {
-      list.length = 2;
-      expect(list, [1, 2]);
-      return new Future(() {
-        expectChanges(changes, []);
-      });
-    });
-
-    test('truncate removes item', () {
-      list.length = 1;
-      expect(list, [1]);
-      return new Future(() {
-        expectChanges(changes, [
-          _change(1, removed: [2, 3])
-        ]);
-      });
-    });
-
-    test('truncate and add new item', () {
-      list.length = 1;
-      list.add(42);
-      expect(list, [1, 42]);
-      return new Future(() {
-        expectChanges(changes, [
-          _change(1, removed: [2, 3], addedCount: 1)
-        ]);
-      });
-    });
-
-    test('truncate and add same item', () {
-      list.length = 1;
-      list.add(2);
-      expect(list, [1, 2]);
-      return new Future(() {
-        expectChanges(changes, []);
-      });
-    });
-  });
-
-  test('toString', () {
-    var list = toObservable([1, 2, 3]);
-    expect(list.toString(), '[1, 2, 3]');
-  });
-
-  group('change records', () {
-    List<ChangeRecord> propRecords;
-    List<ListChangeRecord> listRecords;
-
-    setUp(() {
-      list = toObservable([1, 2, 3, 1, 3, 4]) as ObservableList;
-      propRecords = null;
-      listRecords = null;
-      sub = list.changes.listen((r) => propRecords = r);
-      sub2 = list.listChanges.listen((r) => listRecords = r);
-    });
-
-    tearDown(sharedTearDown);
-
-    test('read operations', () {
-      expect(list.length, 6);
-      expect(list[0], 1);
-      expect(list.indexOf(4), 5);
-      expect(list.indexOf(1), 0);
-      expect(list.indexOf(1, 1), 3);
-      expect(list.lastIndexOf(1), 3);
-      expect(list.last, 4);
-      var copy = new List<int>();
-      list.forEach((int i) => copy.add(i));
-      expect(copy, orderedEquals([1, 2, 3, 1, 3, 4]));
-      return new Future(() {
-        // no change from read-only operators
-        expectChanges(propRecords, null);
-        expectChanges(listRecords, null);
-      });
-    });
-
-    test('add', () {
-      list.add(5);
-      list.add(6);
-      expect(list, orderedEquals([1, 2, 3, 1, 3, 4, 5, 6]));
-
-      return new Future(() {
-        expectChanges(propRecords, [
-          _lengthChange(6, 7),
-          _lengthChange(7, 8),
-        ]);
-        expectChanges(listRecords, [_change(6, addedCount: 2)]);
-      });
-    });
-
-    test('[]=', () {
-      list[1] = list.last;
-      expect(list, orderedEquals([1, 4, 3, 1, 3, 4]));
-
-      return new Future(() {
-        expectChanges(propRecords, null);
-        expectChanges(listRecords, [
-          _change(1, addedCount: 1, removed: [2])
-        ]);
-      });
-    });
-
-    test('removeLast', () {
-      expect(list.removeLast(), 4);
-      expect(list, orderedEquals([1, 2, 3, 1, 3]));
-
-      return new Future(() {
-        expectChanges(propRecords, [_lengthChange(6, 5)]);
-        expectChanges(listRecords, [
-          _change(5, removed: [4])
-        ]);
-      });
-    });
-
-    test('removeRange', () {
-      list.removeRange(1, 4);
-      expect(list, orderedEquals([1, 3, 4]));
-
-      return new Future(() {
-        expectChanges(propRecords, [_lengthChange(6, 3)]);
-        expectChanges(listRecords, [
-          _change(1, removed: [2, 3, 1])
-        ]);
-      });
-    });
-
-    test('removeWhere', () {
-      list.removeWhere((e) => e == 3);
-      expect(list, orderedEquals([1, 2, 1, 4]));
-
-      return new Future(() {
-        expectChanges(propRecords, [_lengthChange(6, 4)]);
-        expectChanges(listRecords, [
-          _change(2, removed: [3]),
-          _change(3, removed: [3])
-        ]);
-      });
-    });
-
-    test('sort', () {
-      list.sort((x, y) => x - y);
-      expect(list, orderedEquals([1, 1, 2, 3, 3, 4]));
-
-      return new Future(() {
-        expectChanges(propRecords, null);
-        expectChanges(listRecords, [
-          _change(1, addedCount: 1),
-          _change(4, removed: [1])
-        ]);
-      });
-    });
-
-    test('sort of 2 elements', () {
-      var list = toObservable([3, 1]);
-      // Dummy listener to record changes.
-      // TODO(jmesserly): should we just record changes always, to support the sync api?
-      sub = list.listChanges.listen((List<ListChangeRecord> records) => null)
-          as StreamSubscription;
-      list.sort();
-      expect(list.deliverListChanges(), true);
-      list.sort();
-      expect(list.deliverListChanges(), false);
-      list.sort();
-      expect(list.deliverListChanges(), false);
-    });
-
-    test('clear', () {
-      list.clear();
-      expect(list, []);
-
-      return new Future(() {
-        expectChanges(propRecords, [
-          _lengthChange(6, 0),
-          new PropertyChangeRecord(list, #isEmpty, false, true),
-          new PropertyChangeRecord(list, #isNotEmpty, true, false),
-        ]);
-        expectChanges(listRecords, [
-          _change(0, removed: [1, 2, 3, 1, 3, 4])
-        ]);
-      });
-    });
-  });
-}
-
-ObservableList list;
-
-PropertyChangeRecord _lengthChange(int oldValue, int newValue) =>
-    new PropertyChangeRecord(list, #length, oldValue, newValue);
-
-_change(int index, {List removed: const [], int addedCount: 0}) =>
-    new ListChangeRecord(list, index, removed: removed, addedCount: addedCount);
diff --git a/test/observable_test_utils.dart b/test/observable_test_utils.dart
index 990cc86..9ce9161 100644
--- a/test/observable_test_utils.dart
+++ b/test/observable_test_utils.dart
@@ -17,8 +17,9 @@
 
 // TODO(jmesserly): use matchers when we have a way to compare ChangeRecords.
 // For now just use the toString.
-void expectChanges(actual, expected, {String reason}) =>
-    expect('$actual', '$expected', reason: reason);
+void expectChanges(actual, expected, {String reason}) {
+  expect('$actual', '$expected', reason: reason);
+}
 
 List<ListChangeRecord> getListChangeRecords(
         List<ListChangeRecord> changes, int index) =>