Add CombinedListView (#50)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7cca956..97792cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.14.0
+
+* Add `CombinedListView`, a view of several lists concatenated together.
+
 ## 1.13.0
 
 * Add `EqualityBy`
diff --git a/lib/collection.dart b/lib/collection.dart
index 612508b..2ea73a7 100644
--- a/lib/collection.dart
+++ b/lib/collection.dart
@@ -4,6 +4,7 @@
 
 export "src/algorithms.dart";
 export "src/canonicalized_map.dart";
+export "src/combined_wrappers/combined_list.dart";
 export "src/comparators.dart";
 export "src/equality.dart";
 export "src/equality_map.dart";
diff --git a/lib/src/combined_wrappers/combined_list.dart b/lib/src/combined_wrappers/combined_list.dart
new file mode 100644
index 0000000..d2e0349
--- /dev/null
+++ b/lib/src/combined_wrappers/combined_list.dart
@@ -0,0 +1,66 @@
+// Copyright (c) 2017, 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:collection';
+
+/// A view of several lists combined into a single list.
+///
+/// All methods and accessors treat the [CombinedListView] list as if it were a
+/// single concatenated list, but the underlying implementation is based on
+/// lazily accessing individual list instances. This means that if the
+/// underlying lists change, the [CombinedListView] will reflect those changes.
+///
+/// The index operator (`[]`) and [length] property of a [CombinedListView] are
+/// both `O(lists)` rather than `O(1)`. A [CombinedListView] is unmodifiable.
+class CombinedListView<T> extends ListBase<T>
+    implements UnmodifiableListView<T> {
+  static void _throw() {
+    throw new UnsupportedError('Cannot modify an unmodifiable List');
+  }
+
+  /// The lists that this combines.
+  final List<List<T>> _lists;
+
+  /// Creates a combined view of [lists].
+  CombinedListView(this._lists);
+
+  set length(int length) {
+    _throw();
+  }
+
+  int get length => _lists.fold(0, (length, list) => length + list.length);
+
+  T operator [](int index) {
+    var initialIndex = index;
+    for (var i = 0; i < _lists.length; i++) {
+      var list = _lists[i];
+      if (index < list.length) {
+        return list[index];
+      }
+      index -= list.length;
+    }
+    throw new RangeError.index(initialIndex, this, 'index', null, length);
+  }
+
+  void operator []=(int index, T value) {
+    _throw();
+  }
+
+  void clear() {
+    _throw();
+  }
+
+  bool remove(Object element) {
+    _throw();
+    return null;
+  }
+
+  void removeWhere(bool filter(T element)) {
+    _throw();
+  }
+
+  void retainWhere(bool filter(T element)) {
+    _throw();
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 587f9f0..5060112 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: collection
-version: 1.13.0
+version: 1.14.0-dev
 author: Dart Team <misc@dartlang.org>
 description: Collections and utilities functions and classes related to collections.
 homepage: https://www.github.com/dart-lang/collection
diff --git a/test/combined_list_view_test.dart b/test/combined_list_view_test.dart
new file mode 100644
index 0000000..d61e51a
--- /dev/null
+++ b/test/combined_list_view_test.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2017, 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:collection/collection.dart';
+import 'package:test/test.dart';
+
+import 'unmodifiable_collection_test.dart' as common;
+
+void main() {
+  var list1 = [1, 2, 3];
+  var list2 = [4, 5, 6];
+  var list3 = [7, 8, 9];
+  var concat = []..addAll(list1)..addAll(list2)..addAll(list3);
+
+  // In every way possible this should test the same as an UnmodifiableListView.
+  common.testUnmodifiableList(concat, new CombinedListView(
+    [list1, list2, list3]
+  ), 'combineLists');
+
+  common.testUnmodifiableList(concat, new CombinedListView(
+    [list1, [], list2, [], list3, []]
+  ), 'combineLists');
+
+  test('should function as an empty list when no lists are passed', () {
+    var empty = new CombinedListView([]);
+    expect(empty, isEmpty);
+    expect(empty.length, 0);
+    expect(() => empty[0], throwsRangeError);
+  });
+
+  test('should function as an empty list when only empty lists are passed', () {
+    var empty = new CombinedListView([[], [], []]);
+    expect(empty, isEmpty);
+    expect(empty.length, 0);
+    expect(() => empty[0], throwsRangeError);
+  });
+
+  test('should reflect underlying changes back to the combined list', () {
+    var backing1 = <int>[];
+    var backing2 = <int>[];
+    var combined = new CombinedListView([backing1, backing2]);
+    expect(combined, isEmpty);
+    backing1.addAll(list1);
+    expect(combined, list1);
+    backing2.addAll(list2);
+    expect(combined, backing1.toList()..addAll(backing2));
+  });
+
+  test('should reflect underlying changes from the list of lists', () {
+    var listOfLists = <List<int>>[];
+    var combined = new CombinedListView(listOfLists);
+    expect(combined, isEmpty);
+    listOfLists.add(list1);
+    expect(combined, list1);
+    listOfLists.add(list2);
+    expect(combined, []..addAll(list1)..addAll(list2));
+    listOfLists.clear();
+    expect(combined, isEmpty);
+  });
+
+  test('should reflect underlying changes with a single list', () {
+    var backing1 = <int>[];
+    var combined = new CombinedListView([backing1]);
+    expect(combined, isEmpty);
+    backing1.addAll(list1);
+    expect(combined, list1);
+  });
+}