[For Consideration] Merging internal fork (#50)

* Revert to version 0.17.0+1.

Fixes for Dart 2.
Resolves fork at version 0.18.0 that prevented support from the package authors.

* Update travis config to ignore failures on stable SDK for now
diff --git a/.travis.yml b/.travis.yml
index b70d6ec..029d2e2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,28 +3,20 @@
 dart:
   - stable
   - dev
-  - 1.22.1
 addons:
   # otherwise a number of tests in test/security/html_sanitizer_test.dart fail
   firefox: "latest"
 dart_task:
+  - dartanalyzer: --fatal-warnings .
   - test: --platform vm
   # Disable parallelism on Firefox (-j 1)
   # Causes flakiness – need to investigate
   - test: --platform firefox -j 1
 matrix:
-  include:
-    # Only validate formatting using the dev release
-    # Formatted with 1.23.0-dev.10.0 which has (good) changes since 1.22.1
-    - dart: dev
-      dart_task: dartfmt
-    # Only care about being analyzer clean for dev and stable
-    - dart: dev
-      dart_task:
-        dartanalyzer: --fatal-warnings lib
-    - dart: stable
-      dart_task:
-        dartanalyzer: --fatal-warnings lib
+  allow_failures:
+  # Until Dart 2 becomes stable
+  - dart: stable
+
 cache:
   directories:
     - $HOME/.pub-cache
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2fc0934..9abaede 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+## 0.21.0
+
+### Breaking Changes
+
+Version 0.21.0 reverts to version 0.17.0+1 with fixes to support Dart 2.
+Versions 0.18, 0.19, and 0.20 were effectively unsupported. This resolves the
+fork that happened at version 0.18 and development can now be supported by the
+authors.
+
 ## 0.20.4+3
 
 * Support the latest release of `pkg/quiver` (0.27).
diff --git a/README.md b/README.md
index 639e746..bd82499 100644
--- a/README.md
+++ b/README.md
@@ -18,67 +18,3 @@
 
 * Listen to `Observable.changes` and be notified when an object changes
 * Use `Differ.diff` to determine changes between two objects
-
-### Examples
-
-Operations on Lists:
-
-```dart
-import 'package:observable/observable.dart';
-
-void main() {
-  var changes;
-
-  ObservableList<String> list = new ObservableList<String>.from(['a', 'b', 'c']);
-  StreamSubscription sub = list.listChanges.listen((c) => changes = c);
-
-  ListChangeRecord _delta(
-    int index, {
-    List removed: const [],
-    int addedCount: 0,
-  }) {
-    return new ListChangeRecord(
-      list,
-      index,
-      removed: removed,
-      addedCount: addedCount,
-    );
-  }
-
-  list.insertAll(1, ['y', 'z']); // changes == [_delta(1, addedCount: 2)]
-}
-```
-
-Diffing two maps:
-
-```dart
-import 'package:observable/observable.dart';
-
-void main() {
-  final map1 = {
-    'key-a': 'value-a',
-    'key-b': 'value-b-old',
-  };
-
-  final map2 = {
-    'key-a': 'value-a',
-    'key-b': 'value-b-new',
-  };
-
-  var diffResult = diff(map1,
-      map2); // [ new MapChangeRecord('key-b', 'value-b-old', 'value-b-new')]
-}
-```
-
-Diffing two sets:
-
-```dart
-import 'package:observable/observable.dart';
-
-void main() {
-  diff(
-    new Set<String>.from(['a', 'b']),
-    new Set<String>.from(['a']),
-  ); // [ new SetChangeRecord.remove('b') ]
-}
-```
diff --git a/.analysis_options b/analysis_options.yaml
similarity index 100%
rename from .analysis_options
rename to analysis_options.yaml
diff --git a/lib/observable.dart b/lib/observable.dart
index be4fe34..516ff70 100644
--- a/lib/observable.dart
+++ b/lib/observable.dart
@@ -5,15 +5,10 @@
 library observable;
 
 export 'src/change_notifier.dart' show ChangeNotifier, PropertyChangeNotifier;
-export 'src/collections.dart' show ObservableList, ObservableMap, ObservableSet;
-export 'src/differs.dart'
-    show Differ, EqualityDiffer, ListDiffer, MapDiffer, SetDiffer;
+export 'src/differs.dart' show Differ, EqualityDiffer, ListDiffer, MapDiffer;
 export 'src/records.dart'
-    show
-        ChangeRecord,
-        ListChangeRecord,
-        MapChangeRecord,
-        PropertyChangeRecord,
-        SetChangeRecord;
+    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 6ba9c9b..82d4e51 100644
--- a/lib/src/change_notifier.dart
+++ b/lib/src/change_notifier.dart
@@ -1,7 +1,3 @@
-// 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:meta/meta.dart';
@@ -10,11 +6,6 @@
 import 'observable.dart';
 import 'records.dart';
 
-// Temporarily exists to keep 'discard changes' API working for now.
-void internalDiscardChanges(ChangeNotifier changeNotifier) {
-  changeNotifier._queue = null;
-}
-
 /// Supplies [changes] and various hooks to implement [Observable].
 ///
 /// May use [notifyChange] to queue a change record; they are asynchronously
@@ -58,6 +49,7 @@
   ///
   /// Returns `true` if changes were emitted.
   @override
+  @visibleForTesting
   @mustCallSuper
   bool deliverChanges() {
     List<ChangeRecord> changes;
diff --git a/lib/src/collections.dart b/lib/src/collections.dart
deleted file mode 100644
index 7441831..0000000
--- a/lib/src/collections.dart
+++ /dev/null
@@ -1,7 +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.
-
-export 'collections/observable_list.dart';
-export 'collections/observable_map.dart';
-export 'collections/observable_set.dart';
diff --git a/lib/src/collections/observable_list.dart b/lib/src/collections/observable_list.dart
deleted file mode 100644
index 6e48e86..0000000
--- a/lib/src/collections/observable_list.dart
+++ /dev/null
@@ -1,519 +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:collection/collection.dart';
-import 'package:observable/observable.dart';
-import 'package:observable/src/change_notifier.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 {
-  /// An empty observable list that never has changes.
-  static const ObservableList EMPTY = const _ObservableUnmodifiableList(
-    const [],
-  );
-
-  /// 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);
-  }
-
-  /// Create new unmodifiable list from [list].
-  ///
-  /// [ObservableList.changes] and [ObservableList.listChanges] both always
-  /// return an empty stream, and mutating or adding change records throws an
-  /// [UnsupportedError].
-  factory ObservableList.unmodifiable(
-    List<E> list,
-  ) {
-    if (list is! UnmodifiableListView<E>) {
-      list = new List<E>.unmodifiable(list);
-    }
-    return new _ObservableUnmodifiableList<E>(list);
-  }
-
-  @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() {
-    internalDiscardChanges(_listChanges);
-  }
-
-  @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))
-        .where((r) => r.isNotEmpty && r != ChangeRecord.ANY);
-  }
-
-  @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);
-    }
-  }
-}
-
-class _ObservableUnmodifiableList<E> extends DelegatingList<E>
-    implements ObservableList<E> {
-  const _ObservableUnmodifiableList(List<E> list) : super(list);
-
-  @override
-  Stream<List<ChangeRecord>> get changes => const Stream.empty();
-
-  @override
-  bool deliverChanges() => false;
-
-  @override
-  bool deliverListChanges() => false;
-
-  @override
-  void discardListChanges() {}
-
-  @override
-  final bool hasListObservers = false;
-
-  @override
-  final bool hasObservers = false;
-
-  @override
-  Stream<List<ListChangeRecord<E>>> get listChanges => const Stream.empty();
-
-  @override
-  void notifyChange([ChangeRecord change]) {
-    throw new UnsupportedError('Not modifiable');
-  }
-
-  @override
-  void notifyListChange(
-    int index, {
-    List<E> removed: const [],
-    int addedCount: 0,
-  }) {
-    throw new UnsupportedError('Not modifiable');
-  }
-
-  @override
-  T notifyPropertyChange<T>(
-    Symbol field,
-    T oldValue,
-    T newValue,
-  ) {
-    throw new UnsupportedError('Not modifiable');
-  }
-
-  @override
-  void observed() {}
-
-  @override
-  void unobserved() {}
-}
diff --git a/lib/src/collections/observable_map.dart b/lib/src/collections/observable_map.dart
deleted file mode 100644
index 3b9cc09..0000000
--- a/lib/src/collections/observable_map.dart
+++ /dev/null
@@ -1,235 +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 'dart:collection';
-
-import 'package:collection/collection.dart';
-import 'package:observable/observable.dart';
-
-/// A [Map] 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 updated key-value). You may
-/// accept an observable map to be notified of mutations:
-///     ```
-///     set grades(Map<String, int> grades) {
-///       buildBook(grades);
-///       if (names is ObservableMap<String, int>) {
-///         grades.changes.listen(updateBook);
-///       }
-///     }
-///     ```
-///
-/// *See [MapDiffer] to manually diff two lists instead*
-abstract class ObservableMap<K, V> implements Map<K, V>, Observable {
-  /// An empty observable map that never has changes.
-  static const ObservableMap EMPTY = const _ObservableUnmodifiableMap(const {});
-
-  /// Creates a new observable map.
-  factory ObservableMap() {
-    return new _ObservableDelegatingMap(new HashMap<K, V>());
-  }
-
-  /// Like [ObservableMap.from], but creates an empty type.
-  factory ObservableMap.createFromType(Map<K, V> other) {
-    ObservableMap<K, V> result;
-    if (other is LinkedHashMap) {
-      result = new ObservableMap<K, V>.linked();
-    } else if (other is SplayTreeMap) {
-      result = new ObservableMap<K, V>.sorted();
-    } else {
-      result = new ObservableMap<K, V>();
-    }
-    return result;
-  }
-
-  /// Create a new observable map using [map] as a backing store.
-  factory ObservableMap.delegate(Map<K, V> map) {
-    return new _ObservableDelegatingMap<K, V>(map);
-  }
-
-  /// Creates a new observable map that contains all entries in [other].
-  ///
-  /// It will attempt to use the same backing map type if the other map is
-  /// either a [LinkedHashMap], [SplayTreeMap], or [HashMap]. Otherwise it will
-  /// fall back to using a [HashMap].
-  factory ObservableMap.from(Map<K, V> other) {
-    return new ObservableMap<K, V>.createFromType(other)..addAll(other);
-  }
-
-  /// Creates a new observable map using a [LinkedHashMap].
-  factory ObservableMap.linked() {
-    return new _ObservableDelegatingMap<K, V>(new LinkedHashMap<K, V>());
-  }
-
-  /// Creates a new observable map using a [SplayTreeMap].
-  factory ObservableMap.sorted() {
-    return new _ObservableDelegatingMap<K, V>(new SplayTreeMap<K, V>());
-  }
-
-  /// Creates a new observable map wrapping [other].
-  @Deprecated('Use ObservableMap.delegate for API consistency')
-  factory ObservableMap.spy(Map<K, V> other) = ObservableMap<K, V>.delegate;
-
-  /// Create a new unmodifiable map from [map].
-  ///
-  /// [ObservableMap.changes] always returns an empty stream, and mutating or
-  /// adding change records throws an [UnsupportedError].
-  factory ObservableMap.unmodifiable(Map<K, V> map) {
-    if (map is! UnmodifiableMapView<K, V>) {
-      map = new Map<K, V>.unmodifiable(map);
-    }
-    return new _ObservableUnmodifiableMap(map);
-  }
-}
-
-class _ObservableDelegatingMap<K, V> extends DelegatingMap<K, V>
-    implements ObservableMap<K, V> {
-  final _allChanges = new ChangeNotifier();
-
-  _ObservableDelegatingMap(Map<K, V> map) : super(map);
-
-  // Observable
-
-  @override
-  Stream<List<ChangeRecord>> get changes => _allChanges.changes;
-
-  // ChangeNotifier (deprecated for ObservableMap)
-
-  @override
-  bool deliverChanges() => _allChanges.deliverChanges();
-
-  @override
-  bool get hasObservers => _allChanges.hasObservers;
-
-  @override
-  void notifyChange([ChangeRecord change]) {
-    if (change is MapChangeRecord && change.oldValue == change.newValue) {
-      return;
-    }
-    _allChanges.notifyChange(change);
-  }
-
-  @override
-  T notifyPropertyChange<T>(
-    Symbol field,
-    T oldValue,
-    T newValue,
-  ) {
-    if (oldValue != newValue) {
-      _allChanges.notifyChange(
-        new PropertyChangeRecord<T>(this, field, oldValue, newValue),
-      );
-    }
-    return newValue;
-  }
-
-  @override
-  void observed() {}
-
-  @override
-  void unobserved() {}
-
-  @override
-  operator []=(K key, V newValue) {
-    if (!hasObservers) {
-      super[key] = newValue;
-      return;
-    }
-
-    final oldLength = super.length;
-    V oldValue = super[key];
-    super[key] = newValue;
-
-    if (oldLength != length) {
-      notifyPropertyChange(#length, oldLength, length);
-      notifyChange(new MapChangeRecord<K, V>.insert(key, newValue));
-    } else {
-      notifyChange(new MapChangeRecord<K, V>(key, oldValue, newValue));
-    }
-  }
-
-  @override
-  void addAll(Map<K, V> other) {
-    if (!hasObservers) {
-      super.addAll(other);
-      return;
-    }
-
-    other.forEach((k, v) => this[k] = v);
-  }
-
-  @override
-  V putIfAbsent(K key, V ifAbsent()) {
-    final oldLength = length;
-    final result = super.putIfAbsent(key, ifAbsent);
-    if (hasObservers && oldLength != length) {
-      notifyPropertyChange(#length, oldLength, length);
-      notifyChange(new MapChangeRecord<K, V>.insert(key, result));
-    }
-    return result;
-  }
-
-  @override
-  V remove(Object key) {
-    final oldLength = length;
-    final result = super.remove(key);
-    if (hasObservers && oldLength != length) {
-      notifyChange(new MapChangeRecord<K, V>.remove(key as K, result));
-      notifyPropertyChange(#length, oldLength, length);
-    }
-    return result;
-  }
-
-  @override
-  void clear() {
-    if (!hasObservers || isEmpty) {
-      super.clear();
-      return;
-    }
-    final oldLength = length;
-    forEach((k, v) {
-      notifyChange(new MapChangeRecord<K, V>.remove(k, v));
-    });
-    notifyPropertyChange(#length, oldLength, 0);
-    super.clear();
-  }
-}
-
-class _ObservableUnmodifiableMap<K, V> extends DelegatingMap<K, V>
-    implements ObservableMap<K, V> {
-  const _ObservableUnmodifiableMap(Map<K, V> map) : super(map);
-
-  @override
-  Stream<List<ChangeRecord>> get changes => const Stream.empty();
-
-  @override
-  bool deliverChanges() => false;
-
-  // TODO: implement hasObservers
-  @override
-  final bool hasObservers = false;
-
-  @override
-  void notifyChange([ChangeRecord change]) {
-    throw new UnsupportedError('Not modifiable');
-  }
-
-  @override
-  T notifyPropertyChange<T>(
-    Symbol field,
-    T oldValue,
-    T newValue,
-  ) {
-    throw new UnsupportedError('Not modifiable');
-  }
-
-  @override
-  void observed() {}
-
-  @override
-  void unobserved() {}
-}
diff --git a/lib/src/collections/observable_set.dart b/lib/src/collections/observable_set.dart
deleted file mode 100644
index 1117a29..0000000
--- a/lib/src/collections/observable_set.dart
+++ /dev/null
@@ -1,216 +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 'dart:collection';
-
-import 'package:collection/collection.dart';
-import 'package:observable/observable.dart';
-
-/// A [Set] 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 element). You may
-/// accept an observable set to be notified of mutations:
-/// ```
-/// set emails(Set<String> emails) {
-///   emailUsers(emails);
-///   if (names is ObservableSet<String>) {
-///     emails.changes.listen(updateEmailList);
-///   }
-/// }
-/// ```
-///
-/// *See [SetDiffer] to manually diff two lists instead*
-abstract class ObservableSet<E>
-    implements Observable<SetChangeRecord<E>>, Set<E> {
-  /// An empty observable set that never has changes.
-  static const ObservableSet EMPTY = const _UnmodifiableObservableSet(
-    const _UnmodifiableEmptySet(),
-  );
-
-  /// Create a new empty observable set.
-  factory ObservableSet() => new _DelegatingObservableSet<E>(new HashSet<E>());
-
-  /// Like [ObservableSet.from], but creates a new empty set.
-  factory ObservableSet.createFromType(Iterable<E> other) {
-    ObservableSet<E> result;
-    if (other is LinkedHashSet) {
-      result = new _DelegatingObservableSet<E>(new LinkedHashSet<E>());
-    } else if (result is SplayTreeSet) {
-      result = new _DelegatingObservableSet<E>(new SplayTreeSet<E>());
-    } else {
-      result = new _DelegatingObservableSet<E>(new HashSet<E>());
-    }
-    return result;
-  }
-
-  /// Create a new observable set using [set] as a backing store.
-  factory ObservableSet.delegate(Set<E> set) = _DelegatingObservableSet<E>;
-
-  /// Creates a new observable set that contains all elements in [other].
-  ///
-  /// It will attempt to use the same backing set type if the other set is
-  /// either a [LinkedHashSet], [SplayTreeSet], or [HashSet]. Otherwise it will
-  /// fall back to using a [HashSet].
-  factory ObservableSet.from(Iterable<E> other) {
-    return new ObservableSet<E>.createFromType(other)..addAll(other);
-  }
-
-  /// Creates a new observable map using a [LinkedHashSet].
-  factory ObservableSet.linked() {
-    return new _DelegatingObservableSet<E>(new LinkedHashSet<E>());
-  }
-
-  /// Creates a new observable map using a [SplayTreeSet].
-  factory ObservableSet.sorted() {
-    return new _DelegatingObservableSet<E>(new SplayTreeSet<E>());
-  }
-
-  /// Create a new unmodifiable set from [set].
-  ///
-  /// [ObservableSet.changes] always returns an empty stream, and mutating or
-  /// adding change records throws an [UnsupportedError].
-  factory ObservableSet.unmodifiable(Set<E> set) {
-    if (set is! UnmodifiableSetView<E>) {
-      set = new UnmodifiableSetView<E>(set);
-    }
-    return new _UnmodifiableObservableSet(set);
-  }
-}
-
-class _DelegatingObservableSet<E> extends DelegatingSet<E>
-    with ChangeNotifier<SetChangeRecord<E>>
-    implements ObservableSet<E> {
-  _DelegatingObservableSet(Set<E> set) : super(set);
-
-  @override
-  bool add(E value) {
-    if (super.add(value)) {
-      if (hasObservers) {
-        notifyChange(new SetChangeRecord<E>.add(value));
-      }
-      return true;
-    }
-    return false;
-  }
-
-  @override
-  void addAll(Iterable<E> values) {
-    values.forEach(add);
-  }
-
-  @override
-  bool remove(Object value) {
-    if (super.remove(value)) {
-      if (hasObservers) {
-        notifyChange(new SetChangeRecord<E>.remove(value as E));
-      }
-      return true;
-    }
-    return false;
-  }
-
-  @override
-  void removeAll(Iterable<Object> values) {
-    values.toList().forEach(remove);
-  }
-
-  @override
-  void removeWhere(bool test(E value)) {
-    removeAll(super.where(test));
-  }
-
-  @override
-  void retainAll(Iterable<Object> elements) {
-    retainWhere(elements.toSet().contains);
-  }
-
-  @override
-  void retainWhere(bool test(E element)) {
-    removeWhere((e) => !test(e));
-  }
-}
-
-class _UnmodifiableEmptySet<E> extends IterableBase<E> implements Set<E> {
-  const _UnmodifiableEmptySet();
-
-  @override
-  bool add(E value) => false;
-
-  @override
-  void addAll(Iterable<E> elements) {}
-
-  @override
-  void clear() {}
-
-  @override
-  bool containsAll(Iterable<Object> other) => other.isEmpty;
-
-  @override
-  Set<E> difference(Set<Object> other) => other.toSet();
-
-  @override
-  Set<E> intersection(Set<Object> other) => this;
-
-  @override
-  Iterator<E> get iterator => const <Null>[].iterator;
-
-  @override
-  E lookup(Object object) => null;
-
-  @override
-  bool remove(Object value) => false;
-
-  @override
-  void removeAll(Iterable<Object> elements) {}
-
-  @override
-  void removeWhere(bool test(E element)) {}
-
-  @override
-  void retainAll(Iterable<Object> elements) {}
-
-  @override
-  void retainWhere(bool test(E element)) {}
-
-  @override
-  Set<E> union(Set<E> other) => other.toSet();
-}
-
-class _UnmodifiableObservableSet<E> extends DelegatingSet<E>
-    implements ObservableSet<E> {
-  const _UnmodifiableObservableSet(Set<E> set) : super(set);
-
-  @override
-  Stream<List<SetChangeRecord<E>>> get changes => const Stream.empty();
-
-  @override
-  bool deliverChanges() => false;
-
-  // TODO: implement hasObservers
-  @override
-  final bool hasObservers = false;
-
-  @override
-  void notifyChange([ChangeRecord change]) {
-    throw new UnsupportedError('Not modifiable');
-  }
-
-  @override
-  T notifyPropertyChange<T>(
-    Symbol field,
-    T oldValue,
-    T newValue,
-  ) {
-    throw new UnsupportedError('Not modifiable');
-  }
-
-  @override
-  void observed() {}
-
-  @override
-  void unobserved() {}
-}
diff --git a/lib/src/differs.dart b/lib/src/differs.dart
index 63d5638..40f005e 100644
--- a/lib/src/differs.dart
+++ b/lib/src/differs.dart
@@ -9,11 +9,11 @@
 import 'package:collection/collection.dart';
 
 import 'records.dart';
+
 import 'internal.dart';
 
 part 'differs/list_differ.dart';
 part 'differs/map_differ.dart';
-part 'differs/set_differ.dart';
 
 /// Generic comparisons between two comparable objects.
 abstract class Differ<E> {
diff --git a/lib/src/differs/list_differ.dart b/lib/src/differs/list_differ.dart
index ab0e5c2..c59830e 100644
--- a/lib/src/differs/list_differ.dart
+++ b/lib/src/differs/list_differ.dart
@@ -206,7 +206,7 @@
   oldEnd -= suffixCount;
 
   if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) {
-    return ListChangeRecord.NONE;
+    return const [];
   }
 
   if (currentStart == currentEnd) {
@@ -440,10 +440,9 @@
 // If someone processed these records naively, they would "play back" the
 // insert incorrectly, because those items will be shifted.
 List<ListChangeRecord<E>> projectListSplices<E>(
-  List<E> list,
-  List<ListChangeRecord<E>> records, [
-  Equality<E> equality = const DefaultEquality(),
-]) {
+    List<E> list, List<ListChangeRecord<E>> records,
+    [Equality<E> equality]) {
+  equality ??= new DefaultEquality<E>();
   if (records.length <= 1) return records;
   final splices = <ListChangeRecord<E>>[];
   final initialSplices = _createInitialSplices(list, records);
diff --git a/lib/src/differs/map_differ.dart b/lib/src/differs/map_differ.dart
index 146834e..6f01fb7 100644
--- a/lib/src/differs/map_differ.dart
+++ b/lib/src/differs/map_differ.dart
@@ -17,7 +17,7 @@
   @override
   List<MapChangeRecord<K, V>> diff(Map<K, V> oldValue, Map<K, V> newValue) {
     if (identical(oldValue, newValue)) {
-      return MapChangeRecord.NONE;
+      return const [];
     }
     final changes = <MapChangeRecord<K, V>>[];
     oldValue.forEach((oldK, oldV) {
diff --git a/lib/src/differs/set_differ.dart b/lib/src/differs/set_differ.dart
deleted file mode 100644
index 31d93cb..0000000
--- a/lib/src/differs/set_differ.dart
+++ /dev/null
@@ -1,31 +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.
-
-part of observable.src.differs;
-
-/// Determines differences between two maps, returning [SetChangeRecord]s.
-///
-/// While [SetChangeRecord] has more information and can be replayed they carry
-/// a more significant cost to calculate and create and should only be used when
-/// the details in the record will actually be used.
-///
-/// See also [EqualityDiffer] for a simpler comparison.
-class SetDiffer<E> implements Differ<Set<E>> {
-  const SetDiffer();
-
-  @override
-  List<SetChangeRecord<E>> diff(Set<E> oldValue, Set<E> newValue) {
-    if (identical(oldValue, newValue)) {
-      return ChangeRecord.NONE;
-    }
-    final changes = <SetChangeRecord<E>>[];
-    for (final added in newValue.difference(oldValue)) {
-      changes.add(new SetChangeRecord<E>.add(added));
-    }
-    for (final removed in oldValue.difference(newValue)) {
-      changes.add(new SetChangeRecord<E>.remove(removed));
-    }
-    return changes;
-  }
-}
diff --git a/lib/src/observable.dart b/lib/src/observable.dart
index 1688202..edc536b 100644
--- a/lib/src/observable.dart
+++ b/lib/src/observable.dart
@@ -22,7 +22,7 @@
   final ChangeNotifier<C> _delegate = new ChangeNotifier<C>();
 
   // Whether Observable was not given a type.
-  final bool _supportsPropertyChanges = PropertyChangeRecord is C;
+  final bool _isNotGeneric = C == dynamic || C == ChangeRecord;
 
   /// Emits a list of changes when the state of the object changes.
   ///
@@ -53,6 +53,8 @@
   ///
   /// Returns `true` if changes were emitted.
   @Deprecated('Use ChangeNotifier instead to have this method available')
+  // REMOVE IGNORE when https://github.com/dart-lang/observable/issues/10
+  // ignore: invalid_use_of_protected_member
   bool deliverChanges() => _delegate.deliverChanges();
 
   /// Notify that the [field] name of this object has been changed.
@@ -76,15 +78,21 @@
     T oldValue,
     T newValue,
   ) {
-    if (hasObservers && oldValue != newValue && _supportsPropertyChanges) {
-      notifyChange(
-        new PropertyChangeRecord(
-          this,
-          field,
-          oldValue,
-          newValue,
-        ) as C,
-      );
+    if (hasObservers && oldValue != newValue) {
+      if (_isNotGeneric) {
+        notifyChange(
+          new PropertyChangeRecord(
+            this,
+            field,
+            oldValue,
+            newValue,
+          ) as C,
+        );
+      } else {
+        // Internal specific patch: Just do nothing.
+        //
+        // Generic typed Observable does not support.
+      }
     }
     return newValue;
   }
diff --git a/lib/src/observable_list.dart b/lib/src/observable_list.dart
new file mode 100644
index 0000000..2549f9b
--- /dev/null
+++ b/lib/src/observable_list.dart
@@ -0,0 +1,332 @@
+// 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<E>> _listRecords;
+
+  StreamController<List<ListChangeRecord<E>>> _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<E>>> 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<E> removed: const [],
+    int addedCount: 0,
+  }) {
+    if (!hasListObservers) return;
+    if (_listRecords == null) {
+      _listRecords = [];
+      scheduleMicrotask(deliverListChanges);
+    }
+    _listRecords.add(new ListChangeRecord<E>(
+      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;
+    final records = projectListSplices<E>(this, _listRecords);
+    _listRecords = null;
+
+    if (hasListObservers && records.isNotEmpty) {
+      _listChanges.add(new UnmodifiableListView<ListChangeRecord<E>>(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 new 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/observable_map.dart b/lib/src/observable_map.dart
new file mode 100644
index 0000000..7f28e20
--- /dev/null
+++ b/lib/src/observable_map.dart
@@ -0,0 +1,229 @@
+// 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_map;
+
+import 'dart:collection';
+
+import 'observable.dart';
+import 'records.dart';
+import 'to_observable.dart';
+
+// TODO(jmesserly): this needs to be faster. We currently require multiple
+// lookups per key to get the old value.
+// TODO(jmesserly): this doesn't implement the precise interfaces like
+// LinkedHashMap, SplayTreeMap or HashMap. However it can use them for the
+// backing store.
+
+/// Represents an observable map of model values. If any items are added,
+/// removed, or replaced, then observers that are listening to [changes]
+/// will be notified.
+class ObservableMap<K, V> extends Observable implements Map<K, V> {
+  final Map<K, V> _map;
+
+  /// Creates an observable map.
+  ObservableMap() : _map = new HashMap<K, V>();
+
+  /// Creates a new observable map using a [LinkedHashMap].
+  ObservableMap.linked() : _map = new LinkedHashMap<K, V>();
+
+  /// Creates a new observable map using a [SplayTreeMap].
+  ObservableMap.sorted() : _map = new SplayTreeMap<K, V>();
+
+  /// Creates an observable map that contains all key value pairs of [other].
+  /// It will attempt to use the same backing map type if the other map is a
+  /// [LinkedHashMap], [SplayTreeMap], or [HashMap]. Otherwise it defaults to
+  /// [HashMap].
+  ///
+  /// Note this will perform a shallow conversion. If you want a deep conversion
+  /// you should use [toObservable].
+  factory ObservableMap.from(Map<K, V> other) {
+    return new ObservableMap<K, V>.createFromType(other)..addAll(other);
+  }
+
+  /// Like [ObservableMap.from], but creates an empty map.
+  factory ObservableMap.createFromType(Map<K, V> other) {
+    ObservableMap<K, V> result;
+    if (other is SplayTreeMap) {
+      result = new ObservableMap<K, V>.sorted();
+    } else if (other is LinkedHashMap) {
+      result = new ObservableMap<K, V>.linked();
+    } else {
+      result = new ObservableMap<K, V>();
+    }
+    return result;
+  }
+
+  /// Creates a new observable map wrapping [other].
+  ObservableMap.spy(Map<K, V> other) : _map = other;
+
+  @override
+  Iterable<K> get keys => _map.keys;
+
+  @override
+  Iterable<V> get values => _map.values;
+
+  @override
+  int get length => _map.length;
+
+  @override
+  bool get isEmpty => length == 0;
+
+  @override
+  bool get isNotEmpty => !isEmpty;
+
+  @override
+  bool containsValue(Object value) => _map.containsValue(value);
+
+  @override
+  bool containsKey(Object key) => _map.containsKey(key);
+
+  @override
+  V operator [](Object key) => _map[key];
+
+  @override
+  void operator []=(K key, V value) {
+    if (!hasObservers) {
+      _map[key] = value;
+      return;
+    }
+
+    int len = _map.length;
+    V oldValue = _map[key];
+
+    _map[key] = value;
+
+    if (len != _map.length) {
+      notifyPropertyChange(#length, len, _map.length);
+      notifyChange(new MapChangeRecord.insert(key, value));
+      _notifyKeysValuesChanged();
+    } else if (oldValue != value) {
+      notifyChange(new MapChangeRecord(key, oldValue, value));
+      _notifyValuesChanged();
+    }
+  }
+
+  @override
+  void addAll(Map<K, V> other) {
+    other.forEach((K key, V value) {
+      this[key] = value;
+    });
+  }
+
+  @override
+  V putIfAbsent(K key, V ifAbsent()) {
+    int len = _map.length;
+    V result = _map.putIfAbsent(key, ifAbsent);
+    if (hasObservers && len != _map.length) {
+      notifyPropertyChange(#length, len, _map.length);
+      notifyChange(new MapChangeRecord.insert(key, result));
+      _notifyKeysValuesChanged();
+    }
+    return result;
+  }
+
+  @override
+  V remove(Object key) {
+    int len = _map.length;
+    V result = _map.remove(key);
+    if (hasObservers && len != _map.length) {
+      notifyChange(new MapChangeRecord.remove(key, result));
+      notifyPropertyChange(#length, len, _map.length);
+      _notifyKeysValuesChanged();
+    }
+    return result;
+  }
+
+  @override
+  void clear() {
+    int len = _map.length;
+    if (hasObservers && len > 0) {
+      _map.forEach((key, value) {
+        notifyChange(new MapChangeRecord.remove(key, value));
+      });
+      notifyPropertyChange(#length, len, 0);
+      _notifyKeysValuesChanged();
+    }
+    _map.clear();
+  }
+
+  @override
+  void forEach(void f(K key, V value)) => _map.forEach(f);
+
+  @override
+  String toString() => Maps.mapToString(this);
+
+  @override
+  // TODO: Dart 2.0 requires this method to be implemented.
+  // ignore: override_on_non_overriding_method
+  Map<K2, V2> cast<K2, V2>() {
+    throw new UnimplementedError("cast");
+  }
+
+  @override
+  // TODO: Dart 2.0 requires this method to be implemented.
+  // ignore: override_on_non_overriding_method
+  Map<K2, V2> retype<K2, V2>() {
+    throw new UnimplementedError("retype");
+  }
+
+  @override
+  // TODO: Dart 2.0 requires this method to be implemented.
+  // ignore: override_on_non_overriding_getter
+  Iterable<Null> get entries {
+    // Change Iterable<Null> to Iterable<MapEntry<K, V>> when
+    // the MapEntry class has been added.
+    throw new UnimplementedError("entries");
+  }
+
+  @override
+  // TODO: Dart 2.0 requires this method to be implemented.
+  // ignore: override_on_non_overriding_method
+  void addEntries(Iterable<Object> entries) {
+    // Change Iterable<Object> to Iterable<MapEntry<K, V>> when
+    // the MapEntry class has been added.
+    throw new UnimplementedError("addEntries");
+  }
+
+  @override
+  // TODO: Dart 2.0 requires this method to be implemented.
+  // ignore: override_on_non_overriding_method
+  Map<K2, V2> map<K2, V2>(Object transform(K key, V value)) {
+    // Change Object to MapEntry<K2, V2> when
+    // the MapEntry class has been added.
+    throw new UnimplementedError("map");
+  }
+
+  @override
+  // TODO: Dart 2.0 requires this method to be implemented.
+  // ignore: override_on_non_overriding_method
+  V update(K key, V update(V value), {V ifAbsent()}) {
+    throw new UnimplementedError("update");
+  }
+
+  @override
+  // TODO: Dart 2.0 requires this method to be implemented.
+  // ignore: override_on_non_overriding_method
+  void updateAll(V update(K key, V value)) {
+    throw new UnimplementedError("updateAll");
+  }
+
+  @override
+  // TODO: Dart 2.0 requires this method to be implemented.
+  // ignore: override_on_non_overriding_method
+  void removeWhere(bool test(K key, V value)) {
+    throw new UnimplementedError("removeWhere");
+  }
+
+  // Note: we don't really have a reasonable old/new value to use here.
+  // But this should fix "keys" and "values" in templates with minimal overhead.
+  void _notifyKeysValuesChanged() {
+    notifyChange(new PropertyChangeRecord(this, #keys, null, null));
+    _notifyValuesChanged();
+  }
+
+  void _notifyValuesChanged() {
+    notifyChange(new PropertyChangeRecord(this, #values, null, null));
+  }
+}
diff --git a/lib/src/records.dart b/lib/src/records.dart
index 6e5342b..87d7d4f 100644
--- a/lib/src/records.dart
+++ b/lib/src/records.dart
@@ -5,14 +5,13 @@
 library observable.src.records;
 
 import 'package:collection/collection.dart';
-import 'package:quiver/core.dart' as quiver;
+import 'package:quiver/core.dart';
 
 import 'internal.dart';
 
 part 'records/list_change_record.dart';
 part 'records/map_change_record.dart';
 part 'records/property_change_record.dart';
-part 'records/set_change_record.dart';
 
 /// Result of a change to an observed object.
 class ChangeRecord {
diff --git a/lib/src/records/list_change_record.dart b/lib/src/records/list_change_record.dart
index 0ebdb43..810843c 100644
--- a/lib/src/records/list_change_record.dart
+++ b/lib/src/records/list_change_record.dart
@@ -12,9 +12,6 @@
 /// the final list positions to figure out which item was added - this removes
 /// the need to incur costly GC on the most common operation (adding).
 class ListChangeRecord<E> implements ChangeRecord {
-  /// Signifies no changes occurred.
-  static const NONE = const <ListChangeRecord>[];
-
   /// How many elements were added at [index] (after removing elements).
   final int addedCount;
 
@@ -70,7 +67,9 @@
 
   /// What elements were added to [object].
   Iterable<E> get added {
-    return addedCount == 0 ? const [] : object.getRange(index, addedCount);
+    return addedCount == 0
+        ? const []
+        : object.getRange(index, index + addedCount);
   }
 
   /// Apply this change record to [list].
@@ -123,8 +122,7 @@
 
   @override
   int get hashCode {
-    return quiver.hash4(
-        object, index, addedCount, const ListEquality().hash(removed));
+    return hash4(object, index, addedCount, const ListEquality().hash(removed));
   }
 
   @override
diff --git a/lib/src/records/map_change_record.dart b/lib/src/records/map_change_record.dart
index 6c61115..cd37fc9 100644
--- a/lib/src/records/map_change_record.dart
+++ b/lib/src/records/map_change_record.dart
@@ -6,9 +6,6 @@
 
 /// A [ChangeRecord] that denotes adding, removing, or updating a map.
 class MapChangeRecord<K, V> implements ChangeRecord {
-  /// Signifies no changes occurred.
-  static const NONE = const <MapChangeRecord>[];
-
   /// The map key that changed.
   final K key;
 
@@ -68,7 +65,7 @@
 
   @override
   int get hashCode {
-    return quiver.hashObjects([
+    return hashObjects([
       key,
       oldValue,
       newValue,
@@ -80,6 +77,6 @@
   @override
   String toString() {
     final kind = isInsert ? 'insert' : isRemove ? 'remove' : 'set';
-    return '#<MapChangeRecord $kind $key from $oldValue to $newValue>';
+    return '#<MapChangeRecord $kind $key from $oldValue to $newValue';
   }
 }
diff --git a/lib/src/records/property_change_record.dart b/lib/src/records/property_change_record.dart
index e42077f..74566d5 100644
--- a/lib/src/records/property_change_record.dart
+++ b/lib/src/records/property_change_record.dart
@@ -26,20 +26,6 @@
   );
 
   @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 => quiver.hash4(object, name, oldValue, newValue);
-
-  @override
   String toString() => ''
       '#<$PropertyChangeRecord $name from $oldValue to: $newValue';
 }
diff --git a/lib/src/records/set_change_record.dart b/lib/src/records/set_change_record.dart
deleted file mode 100644
index ea09f60..0000000
--- a/lib/src/records/set_change_record.dart
+++ /dev/null
@@ -1,41 +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.
-
-part of observable.src.records;
-
-/// A [ChangeRecord] that denotes adding or removing values from a [Set].
-class SetChangeRecord<E> implements ChangeRecord {
-  /// Whether this is a removal operation.
-  final bool isRemove;
-
-  /// Element added or removed in the operation.
-  final E element;
-
-  const SetChangeRecord.add(this.element) : isRemove = false;
-  const SetChangeRecord.remove(this.element) : isRemove = true;
-
-  /// Whether this is an add operation.
-  bool get isAdd => !isRemove;
-
-  /// Apply the change operation to [set].
-  void apply(Set<E> set) {
-    if (isRemove) {
-      set.remove(element);
-    } else {
-      set.add(element);
-    }
-  }
-
-  @override
-  bool operator ==(Object o) =>
-      o is SetChangeRecord<E> && element == o.element && isRemove == o.isRemove;
-
-  @override
-  int get hashCode => quiver.hash2(element, isRemove);
-
-  @override
-  String toString() {
-    return '#<SetChangeRecord ${isRemove ? 'remove' : 'add'} $element>';
-  }
-}
diff --git a/lib/src/to_observable.dart b/lib/src/to_observable.dart
index b4cdaf0..3bf24b6 100644
--- a/lib/src/to_observable.dart
+++ b/lib/src/to_observable.dart
@@ -7,7 +7,8 @@
 import 'dart:collection';
 
 import 'observable.dart' show Observable;
-import 'collections.dart' show ObservableList, ObservableMap;
+import 'observable_list.dart' show ObservableList;
+import 'observable_map.dart' show ObservableMap;
 
 /// Converts the [Iterable] or [Map] to an [ObservableList] or [ObservableMap],
 /// respectively. This is a convenience function to make it easier to convert
diff --git a/pubspec.yaml b/pubspec.yaml
index 64bf8bf..868e3c8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,13 +1,14 @@
 name: observable
-version: 0.20.4+3
+version: 0.21.0
 author: Dart Team <misc@dartlang.org>
 description: Support for marking objects as observable
 homepage: https://github.com/dart-lang/observable
 environment:
-  sdk: '>=1.22.0 <2.0.0'
+  sdk: '>=2.0.0-dev.22.0 <2.0.0'
 dependencies:
-  collection: '^1.11.0'
-  meta: '^1.0.4'
-  quiver: '>=0.24.0 <0.28.0'
+  collection: '^1.14.5'
+  meta: '^1.1.2'
+  quiver: '^0.28.0'
 dev_dependencies:
-  test: '^0.12.17'
+  dart_style: '^1.0.9'
+  test: '^0.12.30'
diff --git a/test/collections/observable_list_test.dart b/test/collections/observable_list_test.dart
deleted file mode 100644
index 8cb9618..0000000
--- a/test/collections/observable_list_test.dart
+++ /dev/null
@@ -1,503 +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';
-
-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<ChangeRecord>> 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('listChanges stream', () {
-    Completer<List<ListChangeRecord>> completer;
-    ObservableList<String> list;
-    List<String> previousState;
-    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);
-      sub = list.listChanges.listen((c) {
-        if (completer?.isCompleted == false) {
-          completer.complete(c.toList());
-        }
-        previousState = list.toList();
-      });
-    });
-
-    tearDown(() => sub.cancel());
-
-    test('updates when an index changes', () async {
-      list[0] = 'd';
-      await expectChanges([
-        new ListChangeRecord.replace(list, 0, ['a']),
-      ]);
-    });
-
-    test('should not emit an empty list', () async {
-      // This is optimized into a no-op.
-      list[0] = 'd';
-      list[0] = 'a';
-      next().then((_) {
-        fail('Should not emit change records');
-      });
-      await new Future.value();
-    });
-  });
-
-  group('length changes', () {
-    Completer<List<ChangeRecord>> 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/collections/observable_set_test.dart b/test/collections/observable_set_test.dart
deleted file mode 100644
index 852fd17..0000000
--- a/test/collections/observable_set_test.dart
+++ /dev/null
@@ -1,163 +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';
-
-main() {
-  group('$ObservableSet', () {
-    group('set api', _runSetTests);
-    _runObservableSetTests();
-  });
-}
-
-_runSetTests() {
-  // TODO(matanl): Can we run the Set-API tests from the SDK?
-  // Any methods actually implemented by ObservableSet are below, otherwise I am
-  // relying on the test suite for DelegatingSet.
-  test('add', () {
-    final set = new ObservableSet<String>();
-    expect(set.add('item'), isTrue);
-    expect(set, ['item']);
-    expect(set.add('item'), isFalse);
-    expect(set, ['item']);
-  });
-
-  test('addAll', () {
-    final set = new ObservableSet<String>.linked();
-    set.addAll(['1', '2', '3']);
-    expect(set, ['1', '2', '3']);
-    set.addAll(['3', '4']);
-    expect(set, ['1', '2', '3', '4']);
-  });
-
-  test('remove', () {
-    final set = new ObservableSet<String>();
-    expect(set.remove('item'), isFalse);
-    expect(set, isEmpty);
-    set.add('item');
-    expect(set, isNotEmpty);
-    expect(set.remove('item'), isTrue);
-    expect(set, isEmpty);
-  });
-
-  test('removeAll', () {
-    final set = new ObservableSet<String>.from(['1', '2', '3']);
-    set.removeAll(['1', '3']);
-    expect(set, ['2']);
-  });
-
-  test('removeWhere', () {
-    final set = new ObservableSet<String>.from(['1', '2', '3']);
-    set.removeWhere((s) => s != '2');
-    expect(set, ['2']);
-  });
-
-  test('retainAll', () {
-    final set = new ObservableSet<String>.from(['1', '2', '3']);
-    set.retainAll(['2']);
-    expect(set, ['2']);
-  });
-
-  test('retainWhere', () {
-    final set = new ObservableSet<String>.from(['1', '2', '3']);
-    set.retainWhere((s) => s == '2');
-    expect(set, ['2']);
-  });
-}
-
-_runObservableSetTests() {
-  group('observable changes', () {
-    Completer<List<SetChangeRecord>> completer;
-    Set<String> previousState;
-
-    ObservableSet<String> set;
-    StreamSubscription sub;
-
-    Future next() {
-      completer = new Completer<List<SetChangeRecord>>.sync();
-      return completer.future;
-    }
-
-    Future<Null> expectChanges(List<SetChangeRecord> changes) {
-      // Applying these change records in order should make the new list.
-      for (final change in changes) {
-        change.apply(previousState);
-      }
-
-      expect(set, previousState);
-
-      // If these fail, it might be safe to update if optimized/changed.
-      return next().then((actualChanges) {
-        for (final change in changes) {
-          expect(actualChanges, contains(change));
-        }
-      });
-    }
-
-    setUp(() {
-      set = new ObservableSet.from(['a', 'b', 'c']);
-      previousState = set.toSet();
-      sub = set.changes.listen((c) {
-        if (completer?.isCompleted == false) {
-          completer.complete(c);
-        }
-        previousState = set.toSet();
-      });
-    });
-
-    tearDown(() => sub.cancel());
-
-    test('add', () async {
-      set.add('value');
-      await expectChanges([
-        new SetChangeRecord.add('value'),
-      ]);
-    });
-
-    test('addAll', () async {
-      set.addAll(['1', '2']);
-      await expectChanges([
-        new SetChangeRecord.add('1'),
-        new SetChangeRecord.add('2'),
-      ]);
-    });
-
-    test('remove', () async {
-      set.remove('a');
-      await expectChanges([
-        new SetChangeRecord.remove('a'),
-      ]);
-    });
-
-    Future expectOnlyItem() {
-      return expectChanges([
-        new SetChangeRecord.remove('a'),
-        new SetChangeRecord.remove('c'),
-      ]);
-    }
-
-    test('removeAll', () async {
-      set.removeAll(['a', 'c']);
-      await expectOnlyItem();
-    });
-
-    test('removeWhere', () async {
-      set.removeWhere((s) => s != 'b');
-      await expectOnlyItem();
-    });
-
-    test('retainAll', () async {
-      set.retainAll(['b']);
-      await expectOnlyItem();
-    });
-
-    test('retainWhere', () async {
-      set.retainWhere((s) => s == 'b');
-      await expectOnlyItem();
-    });
-  });
-}
diff --git a/test/differs/set_differ_test.dart b/test/differs/set_differ_test.dart
deleted file mode 100644
index b6fbf04..0000000
--- a/test/differs/set_differ_test.dart
+++ /dev/null
@@ -1,69 +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 'package:observable/observable.dart';
-import 'package:test/test.dart';
-
-main() {
-  group('$SetDiffer', () {
-    final diff = const SetDiffer<String>().diff;
-
-    test('should emit no changes for identical maps', () {
-      final set = new Set<String>.from(
-        new Iterable.generate(10, (i) => '$i'),
-      );
-      expect(diff(set, set), ChangeRecord.NONE);
-    });
-
-    test('should emit no changes for maps with identical content', () {
-      final set1 = new Set<String>.from(
-        new Iterable.generate(10, (i) => '$i'),
-      );
-      final set2 = new Set<String>.from(
-        new Iterable.generate(10, (i) => '$i'),
-      );
-      expect(diff(set1, set2), ChangeRecord.NONE);
-    });
-
-    test('should detect insertions', () {
-      expect(
-        diff(
-          new Set<String>.from(['a']),
-          new Set<String>.from(['a', 'b']),
-        ),
-        [
-          new SetChangeRecord.add('b'),
-        ],
-      );
-    });
-
-    test('should detect removals', () {
-      expect(
-        diff(
-          new Set<String>.from(['a', 'b']),
-          new Set<String>.from(['a']),
-        ),
-        [
-          new SetChangeRecord.remove('b'),
-        ],
-      );
-    });
-  });
-
-  group('$SetChangeRecord', () {
-    test('should reply an insertion', () {
-      final set1 = new Set<String>.from(['a', 'b']);
-      final set2 = new Set<String>.from(['a', 'b', 'c']);
-      new SetChangeRecord.add('c').apply(set1);
-      expect(set1, set2);
-    });
-
-    test('should replay a removal', () {
-      final set1 = new Set<String>.from(['a', 'b', 'c']);
-      final set2 = new Set<String>.from(['a', 'b']);
-      new SetChangeRecord.remove('c').apply(set1);
-      expect(set1, set2);
-    });
-  });
-}
diff --git a/test/list_change_test.dart b/test/list_change_test.dart
index cf2ac53..8560688 100644
--- a/test/list_change_test.dart
+++ b/test/list_change_test.dart
@@ -12,19 +12,21 @@
 // This file contains code ported from:
 // https://github.com/rafaelw/ChangeSummary/blob/master/tests/test.js
 
-// TODO(jmesserly): port or write array fuzzer tests
-void main() {
-  StreamSubscription sub;
-  ObservableList model;
+main() => listChangeTests();
 
-  tearDown(() async {
-    await sub?.cancel();
+// TODO(jmesserly): port or write array fuzzer tests
+listChangeTests() {
+  StreamSubscription sub;
+  var model;
+
+  tearDown(() {
+    sub?.cancel();
     model = null;
   });
 
   _delta(i, r, a) => new ListChangeRecord(model, i, removed: r, addedCount: a);
 
-  test('sequential adds', () async {
+  test('sequential adds', () {
     model = toObservable([]);
     model.add(0);
 
@@ -35,42 +37,46 @@
     model.add(2);
 
     expect(summary, null);
-    await newMicrotask();
-    expectChanges(summary, [_delta(1, [], 2)]);
+    return new Future(() {
+      expectChanges(summary, [_delta(1, [], 2)]);
+      expect(summary[0].added, [1, 2]);
+      expect(summary[0].removed, []);
+    });
   });
 
-  test('List Splice Truncate And Expand With Length', () async {
+  test('List Splice Truncate And Expand With Length', () {
     model = toObservable(['a', 'b', 'c', 'd', 'e']);
 
     var summary;
     sub = model.listChanges.listen((r) => summary = r);
 
     model.length = 2;
-
-    await newMicrotask();
-    expectChanges(summary, [
-      _delta(2, ['c', 'd', 'e'], 0)
-    ]);
-    summary = null;
-    model.length = 5;
-
-    await newMicrotask();
-
-    expectChanges(summary, [_delta(2, [], 3)]);
+    return new Future(() {
+      expectChanges(summary, [
+        _delta(2, ['c', 'd', 'e'], 0)
+      ]);
+      expect(summary[0].added, []);
+      expect(summary[0].removed, ['c', 'd', 'e']);
+      summary = null;
+      model.length = 5;
+    }).then(newMicrotask).then((_) {
+      expectChanges(summary, [_delta(2, [], 3)]);
+      expect(summary[0].added, [null, null, null]);
+      expect(summary[0].removed, []);
+    });
   });
 
   group('List deltas can be applied', () {
-    Future applyAndCheckDeltas(model, copy, Future changes) async {
-      var summary = await changes;
-      // apply deltas to the copy
-      for (ListChangeRecord delta in summary) {
-        delta.apply(copy);
-      }
+    applyAndCheckDeltas(model, copy, changes) => changes.then((summary) {
+          // apply deltas to the copy
+          for (ListChangeRecord delta in summary) {
+            delta.apply(copy);
+          }
 
-      expect('$copy', '$model', reason: 'summary $summary');
-    }
+          expect('$copy', '$model', reason: 'summary $summary');
+        });
 
-    test('Contained', () async {
+    test('Contained', () {
       var model = toObservable(['a', 'b']);
       var copy = model.toList();
       var changes = model.listChanges.first;
@@ -80,10 +86,10 @@
       model.removeRange(1, 3);
       model.insert(1, 'f');
 
-      await applyAndCheckDeltas(model, copy, changes);
+      return applyAndCheckDeltas(model, copy, changes);
     });
 
-    test('Delete Empty', () async {
+    test('Delete Empty', () {
       var model = toObservable([1]);
       var copy = model.toList();
       var changes = model.listChanges.first;
@@ -91,10 +97,10 @@
       model.removeAt(0);
       model.insertAll(0, ['a', 'b', 'c']);
 
-      await applyAndCheckDeltas(model, copy, changes);
+      return applyAndCheckDeltas(model, copy, changes);
     });
 
-    test('Right Non Overlap', () async {
+    test('Right Non Overlap', () {
       var model = toObservable(['a', 'b', 'c', 'd']);
       var copy = model.toList();
       var changes = model.listChanges.first;
@@ -104,10 +110,10 @@
       model.removeRange(2, 3);
       model.insertAll(2, ['f', 'g']);
 
-      await applyAndCheckDeltas(model, copy, changes);
+      return applyAndCheckDeltas(model, copy, changes);
     });
 
-    test('Left Non Overlap', () async {
+    test('Left Non Overlap', () {
       var model = toObservable(['a', 'b', 'c', 'd']);
       var copy = model.toList();
       var changes = model.listChanges.first;
@@ -117,10 +123,10 @@
       model.removeRange(0, 1);
       model.insert(0, 'e');
 
-      await applyAndCheckDeltas(model, copy, changes);
+      return applyAndCheckDeltas(model, copy, changes);
     });
 
-    test('Right Adjacent', () async {
+    test('Right Adjacent', () {
       var model = toObservable(['a', 'b', 'c', 'd']);
       var copy = model.toList();
       var changes = model.listChanges.first;
@@ -130,10 +136,10 @@
       model.removeRange(2, 3);
       model.insertAll(0, ['f', 'g']);
 
-      await applyAndCheckDeltas(model, copy, changes);
+      return applyAndCheckDeltas(model, copy, changes);
     });
 
-    test('Left Adjacent', () async {
+    test('Left Adjacent', () {
       var model = toObservable(['a', 'b', 'c', 'd']);
       var copy = model.toList();
       var changes = model.listChanges.first;
@@ -144,10 +150,10 @@
       model.removeAt(1);
       model.insertAll(1, ['f', 'g']);
 
-      await applyAndCheckDeltas(model, copy, changes);
+      return applyAndCheckDeltas(model, copy, changes);
     });
 
-    test('Right Overlap', () async {
+    test('Right Overlap', () {
       var model = toObservable(['a', 'b', 'c', 'd']);
       var copy = model.toList();
       var changes = model.listChanges.first;
@@ -157,10 +163,10 @@
       model.removeAt(1);
       model.insertAll(1, ['f', 'g']);
 
-      await applyAndCheckDeltas(model, copy, changes);
+      return applyAndCheckDeltas(model, copy, changes);
     });
 
-    test('Left Overlap', () async {
+    test('Left Overlap', () {
       var model = toObservable(['a', 'b', 'c', 'd']);
       var copy = model.toList();
       var changes = model.listChanges.first;
@@ -172,10 +178,10 @@
       model.insertAll(1, ['h', 'i', 'j']);
       // a [h i j] f g d
 
-      await applyAndCheckDeltas(model, copy, changes);
+      return applyAndCheckDeltas(model, copy, changes);
     });
 
-    test('Prefix And Suffix One In', () async {
+    test('Prefix And Suffix One In', () {
       var model = toObservable(['a', 'b', 'c', 'd']);
       var copy = model.toList();
       var changes = model.listChanges.first;
@@ -183,20 +189,20 @@
       model.insert(0, 'z');
       model.add('z');
 
-      await applyAndCheckDeltas(model, copy, changes);
+      return applyAndCheckDeltas(model, copy, changes);
     });
 
-    test('Remove First', () async {
+    test('Remove First', () {
       var model = toObservable([16, 15, 15]);
       var copy = model.toList();
       var changes = model.listChanges.first;
 
       model.removeAt(0);
 
-      await applyAndCheckDeltas(model, copy, changes);
+      return applyAndCheckDeltas(model, copy, changes);
     });
 
-    test('Update Remove', () async {
+    test('Update Remove', () {
       var model = toObservable(['a', 'b', 'c', 'd']);
       var copy = model.toList();
       var changes = model.listChanges.first;
@@ -206,61 +212,60 @@
       model[0] = 'h';
       model.removeAt(1);
 
-      await applyAndCheckDeltas(model, copy, changes);
+      return applyAndCheckDeltas(model, copy, changes);
     });
 
-    test('Remove Mid List', () async {
+    test('Remove Mid List', () {
       var model = toObservable(['a', 'b', 'c', 'd']);
       var copy = model.toList();
       var changes = model.listChanges.first;
 
       model.removeAt(2);
 
-      await applyAndCheckDeltas(model, copy, changes);
+      return applyAndCheckDeltas(model, copy, changes);
     });
   });
 
   group('edit distance', () {
-    Future assertEditDistance(orig, Future changes, expectedDist) async {
-      var summary = await changes;
-      var actualDistance = 0;
-      for (var delta in summary) {
-        actualDistance += delta.addedCount + delta.removed.length;
-      }
+    assertEditDistance(orig, changes, expectedDist) => changes.then((summary) {
+          var actualDistance = 0;
+          for (var delta in summary) {
+            actualDistance += delta.addedCount + delta.removed.length;
+          }
 
-      expect(actualDistance, expectedDist);
-    }
+          expect(actualDistance, expectedDist);
+        });
 
-    test('add items', () async {
+    test('add items', () {
       var model = toObservable([]);
       var changes = model.listChanges.first;
       model.addAll([1, 2, 3]);
-      await assertEditDistance(model, changes, 3);
+      return assertEditDistance(model, changes, 3);
     });
 
-    test('trunacte and add, sharing a contiguous block', () async {
+    test('trunacte and add, sharing a contiguous block', () {
       var model = toObservable(['x', 'x', 'x', 'x', '1', '2', '3']);
       var changes = model.listChanges.first;
       model.length = 0;
       model.addAll(['1', '2', '3', 'y', 'y', 'y', 'y']);
-      await assertEditDistance(model, changes, 8);
+      return assertEditDistance(model, changes, 8);
     });
 
-    test('truncate and add, sharing a discontiguous block', () async {
+    test('truncate and add, sharing a discontiguous block', () {
       var model = toObservable(['1', '2', '3', '4', '5']);
       var changes = model.listChanges.first;
       model.length = 0;
       model.addAll(['a', '2', 'y', 'y', '4', '5', 'z', 'z']);
-      await assertEditDistance(model, changes, 7);
+      return assertEditDistance(model, changes, 7);
     });
 
-    test('insert at beginning and end', () async {
+    test('insert at beginning and end', () {
       var model = toObservable([2, 3, 4]);
       var changes = model.listChanges.first;
       model.insert(0, 5);
       model[2] = 6;
       model.add(7);
-      await assertEditDistance(model, changes, 4);
+      return assertEditDistance(model, changes, 4);
     });
   });
 }
diff --git a/test/differs/map_differ_test.dart b/test/map_differ_test.dart
similarity index 87%
rename from test/differs/map_differ_test.dart
rename to test/map_differ_test.dart
index 2875015..7fb9ca9 100644
--- a/test/differs/map_differ_test.dart
+++ b/test/map_differ_test.dart
@@ -1,7 +1,3 @@
-// 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 'package:observable/observable.dart';
 import 'package:test/test.dart';
 
@@ -13,7 +9,7 @@
       final map = new Map<String, String>.fromIterable(
         new Iterable.generate(10, (i) => '$i'),
       );
-      expect(diff(map, map), MapChangeRecord.NONE);
+      expect(diff(map, map), isEmpty);
     });
 
     test('should emit no changes for maps with identical content', () {
@@ -23,7 +19,7 @@
       final map2 = new Map<String, String>.fromIterable(
         new Iterable.generate(10, (i) => '$i'),
       );
-      expect(diff(map1, map2), MapChangeRecord.NONE);
+      expect(diff(map1, map2), isEmpty);
     });
 
     test('should detect insertions', () {
@@ -75,7 +71,7 @@
   });
 
   group('$MapChangeRecord', () {
-    test('should replay an insertion', () {
+    test('should reply an insertion', () {
       final map1 = {
         'key-a': 'value-a',
         'key-b': 'value-b',
diff --git a/test/observable_list_test.dart b/test/observable_list_test.dart
new file mode 100644
index 0000000..b999cfd
--- /dev/null
+++ b/test/observable_list_test.dart
@@ -0,0 +1,342 @@
+// 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<int> 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_map_test.dart b/test/observable_map_test.dart
index bddc091..04aeccc 100644
--- a/test/observable_map_test.dart
+++ b/test/observable_map_test.dart
@@ -9,7 +9,9 @@
 
 import 'observable_test_utils.dart';
 
-void main() {
+main() => _runTests();
+
+_runTests() {
   // TODO(jmesserly): need all standard Map API tests.
 
   StreamSubscription sub;
@@ -189,6 +191,79 @@
     expect(map.toString(), '{a: 1, b: 2}');
   });
 
+  group('observe keys/values', () {
+    ObservableMap map;
+    int keysChanged;
+    int valuesChanged;
+
+    setUp(() {
+      map = toObservable({'a': 1, 'b': 2, 'c': 3});
+      keysChanged = 0;
+      valuesChanged = 0;
+      sub = map.changes.listen((records) {
+        keysChanged += getPropertyChangeRecords(records, #keys).length;
+        valuesChanged += getPropertyChangeRecords(records, #values).length;
+      });
+    });
+
+    tearDown(sharedTearDown);
+
+    test('add item changes keys/values', () {
+      map['d'] = 4;
+      expect(map, {'a': 1, 'b': 2, 'c': 3, 'd': 4});
+      return new Future(() {
+        expect(keysChanged, 1);
+        expect(valuesChanged, 1);
+      });
+    });
+
+    test('putIfAbsent changes keys/values', () {
+      map.putIfAbsent('d', () => 4);
+      expect(map, {'a': 1, 'b': 2, 'c': 3, 'd': 4});
+      return new Future(() {
+        expect(keysChanged, 1);
+        expect(valuesChanged, 1);
+      });
+    });
+
+    test('remove changes keys/values', () {
+      map.remove('c');
+      map.remove('a');
+      expect(map, {'b': 2});
+      return new Future(() {
+        expect(keysChanged, 2);
+        expect(valuesChanged, 2);
+      });
+    });
+
+    test('remove non-existent item does not change keys/values', () {
+      map.remove('d');
+      expect(map, {'a': 1, 'b': 2, 'c': 3});
+      return new Future(() {
+        expect(keysChanged, 0);
+        expect(valuesChanged, 0);
+      });
+    });
+
+    test('set existing item does not change keys', () {
+      map['c'] = 9000;
+      expect(map, {'a': 1, 'b': 2, 'c': 9000});
+      return new Future(() {
+        expect(keysChanged, 0);
+        expect(valuesChanged, 1);
+      });
+    });
+
+    test('clear changes keys/values', () {
+      map.clear();
+      expect(map, {});
+      return new Future(() {
+        expect(keysChanged, 1);
+        expect(valuesChanged, 1);
+      });
+    });
+  });
+
   group('change records', () {
     List<ChangeRecord> records;
     ObservableMap map;
@@ -233,6 +308,8 @@
         expectChanges(records, [
           _lengthChange(map, 2, 3),
           _insertKey('c', 3),
+          _propChange(map, #keys),
+          _propChange(map, #values),
         ]);
       });
     });
@@ -247,8 +324,11 @@
       return new Future(() {
         expectChanges(records, [
           _changeKey('a', 1, 42),
+          _propChange(map, #values),
           _lengthChange(map, 2, 3),
           _insertKey('c', 3),
+          _propChange(map, #keys),
+          _propChange(map, #values),
         ]);
       });
     });
@@ -261,6 +341,8 @@
         expectChanges(records, [
           _removeKey('b', 2),
           _lengthChange(map, 2, 1),
+          _propChange(map, #keys),
+          _propChange(map, #values),
         ]);
       });
     });
@@ -274,6 +356,8 @@
           _removeKey('a', 1),
           _removeKey('b', 2),
           _lengthChange(map, 2, 0),
+          _propChange(map, #keys),
+          _propChange(map, #values),
         ]);
       });
     });
@@ -303,3 +387,5 @@
 _insertKey(key, newValue) => new MapChangeRecord.insert(key, newValue);
 
 _removeKey(key, oldValue) => new MapChangeRecord.remove(key, oldValue);
+
+_propChange(map, prop) => new PropertyChangeRecord(map, prop, null, null);
diff --git a/test/observable_test.dart b/test/observable_test.dart
index c902ba8..af4d9c3 100644
--- a/test/observable_test.dart
+++ b/test/observable_test.dart
@@ -9,16 +9,23 @@
 
 import 'observable_test_utils.dart';
 
-void main() {
+main() => observableTests();
+
+void observableTests() {
   // Track the subscriptions so we can clean them up in tearDown.
-  List<StreamSubscription> subs;
+  List subs;
 
   setUp(() {
-    subs = <StreamSubscription>[];
+    subs = [];
   });
 
-  tearDown(() async {
-    for (var sub in subs) await sub.cancel();
+  tearDown(() {
+    for (var sub in subs) sub.cancel();
+  });
+
+  test('handle future result', () {
+    var callback = expectAsync(() {});
+    return new Future(callback);
   });
 
   test('no observers', () {
@@ -41,7 +48,7 @@
     var t = createModel(123);
     int called = 0;
 
-    subs.add(t.changes.listen(expectAsync1((records) {
+    subs.add(t.changes.listen(expectAsync((records) {
       called++;
       expectPropertyChanges(records, 2);
     })));
@@ -55,7 +62,7 @@
     var t = createModel(123);
     int called = 0;
 
-    subs.add(t.changes.listen(expectAsync1((records) {
+    subs.add(t.changes.listen(expectAsync((records) {
       called++;
       expectPropertyChanges(records, 1);
       if (called == 1) {
@@ -74,14 +81,14 @@
       expectPropertyChanges(records, 2);
     }
 
-    subs.add(t.changes.listen(expectAsync1(verifyRecords)));
-    subs.add(t.changes.listen(expectAsync1(verifyRecords)));
+    subs.add(t.changes.listen(expectAsync(verifyRecords)));
+    subs.add(t.changes.listen(expectAsync(verifyRecords)));
 
     t.value = 41;
     t.value = 42;
   });
 
-  test('async processing model', () async {
+  test('async processing model', () {
     var t = createModel(123);
     var records = [];
     subs.add(t.changes.listen((r) {
@@ -91,22 +98,21 @@
     t.value = 42;
     expectChanges(records, [], reason: 'changes delived async');
 
-    await newMicrotask();
+    return new Future(() {
+      expectPropertyChanges(records, 2);
+      records.clear();
 
-    expectPropertyChanges(records, 2);
-    records.clear();
-
-    t.value = 777;
-    expectChanges(records, [], reason: 'changes delived async');
-
-    await newMicrotask();
-    expectPropertyChanges(records, 1);
+      t.value = 777;
+      expectChanges(records, [], reason: 'changes delived async');
+    }).then(newMicrotask).then((_) {
+      expectPropertyChanges(records, 1);
+    });
   });
 
   test('cancel listening', () {
     var t = createModel(123);
     var sub;
-    sub = t.changes.listen(expectAsync1((records) {
+    sub = t.changes.listen(expectAsync((records) {
       expectPropertyChanges(records, 1);
       sub.cancel();
       t.value = 777;
@@ -117,12 +123,12 @@
   test('cancel and reobserve', () {
     var t = createModel(123);
     var sub;
-    sub = t.changes.listen(expectAsync1((records) {
+    sub = t.changes.listen(expectAsync((records) {
       expectPropertyChanges(records, 1);
       sub.cancel();
 
       scheduleMicrotask(() {
-        subs.add(t.changes.listen(expectAsync1((records) {
+        subs.add(t.changes.listen(expectAsync((records) {
           expectPropertyChanges(records, 1);
         })));
         t.value = 777;
diff --git a/test/observable_test_utils.dart b/test/observable_test_utils.dart
index 91164c8..990cc86 100644
--- a/test/observable_test_utils.dart
+++ b/test/observable_test_utils.dart
@@ -13,13 +13,12 @@
 /// to happen in the next microtask:
 ///
 ///     future.then(newMicrotask).then(...)
-Future newMicrotask() => new Future.value();
+newMicrotask(_) => new Future.value();
 
 // 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) =>