Listenable.merge (#7256)

Sometimes you have several listenables, but you want to hand them to an
API (e.g. CustomPainter) that only expects one.
diff --git a/packages/flutter/lib/foundation.dart b/packages/flutter/lib/foundation.dart
index fa33ed3..b26a56f 100644
--- a/packages/flutter/lib/foundation.dart
+++ b/packages/flutter/lib/foundation.dart
@@ -14,7 +14,6 @@
 export 'src/foundation/binding.dart';
 export 'src/foundation/change_notifier.dart';
 export 'src/foundation/licenses.dart';
-export 'src/foundation/listenable.dart';
 export 'src/foundation/platform.dart';
 export 'src/foundation/print.dart';
 export 'src/foundation/synchronous_future.dart';
diff --git a/packages/flutter/lib/src/foundation/change_notifier.dart b/packages/flutter/lib/src/foundation/change_notifier.dart
index 5ce4d37..eb0e87c 100644
--- a/packages/flutter/lib/src/foundation/change_notifier.dart
+++ b/packages/flutter/lib/src/foundation/change_notifier.dart
@@ -6,7 +6,27 @@
 
 import 'assertions.dart';
 import 'basic_types.dart';
-import 'listenable.dart';
+
+/// An object that maintains a list of listeners.
+abstract class Listenable {
+  /// Abstract const constructor. This constructor enables subclasses to provide
+  /// const constructors so that they can be used in const expressions.
+  const Listenable();
+
+  /// Return a [Listenable] that triggers when any of the given [Listenable]s
+  /// themselves trigger.
+  ///
+  /// The list must not be changed after this method has been called. Doing so
+  /// will lead to memory leaks or exceptions.
+  factory Listenable.merge(List<Listenable> listenables) = _MergingListenable;
+
+  /// Register a closure to be called when the object notifies its listeners.
+  void addListener(VoidCallback listener);
+
+  /// Remove a previously registered closure from the list of closures that the
+  /// object notifies.
+  void removeListener(VoidCallback listener);
+}
 
 /// A class that can be extended or mixed in that provides a change notification
 /// API using [VoidCallback] for notifications.
@@ -69,3 +89,19 @@
     }
   }
 }
+
+class _MergingListenable extends ChangeNotifier {
+  _MergingListenable(this._children) {
+    for (Listenable child in _children)
+      child.addListener(notifyListeners);
+  }
+
+  final List<Listenable> _children;
+
+  @override
+  void dispose() {
+    for (Listenable child in _children)
+      child.removeListener(notifyListeners);
+    super.dispose();
+  }
+}
diff --git a/packages/flutter/lib/src/foundation/listenable.dart b/packages/flutter/lib/src/foundation/listenable.dart
deleted file mode 100644
index ad193d3..0000000
--- a/packages/flutter/lib/src/foundation/listenable.dart
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright 2016 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'basic_types.dart';
-
-/// An object that maintains a list of listeners.
-abstract class Listenable {
-  /// Abstract const constructor. This constructor enables subclasses to provide
-  /// const constructors so that they can be used in const expressions.
-  const Listenable();
-
-  /// Register a closure to be called when the object notifies its listeners.
-  void addListener(VoidCallback listener);
-
-  /// Remove a previously registered closure from the list of closures that the
-  /// object notifies.
-  void removeListener(VoidCallback listener);
-}
diff --git a/packages/flutter/test/foundation/change_notifier_test.dart b/packages/flutter/test/foundation/change_notifier_test.dart
index 1d40c85..e252f28 100644
--- a/packages/flutter/test/foundation/change_notifier_test.dart
+++ b/packages/flutter/test/foundation/change_notifier_test.dart
@@ -24,52 +24,52 @@
     test.addListener(listener);
     test.addListener(listener);
     test.notify();
-    expect(log, equals(<String>['listener', 'listener']));
+    expect(log, <String>['listener', 'listener']);
     log.clear();
 
     test.removeListener(listener);
     test.notify();
-    expect(log, equals(<String>['listener']));
+    expect(log, <String>['listener']);
     log.clear();
 
     test.removeListener(listener);
     test.notify();
-    expect(log, equals(<String>[]));
+    expect(log, <String>[]);
     log.clear();
 
     test.removeListener(listener);
     test.notify();
-    expect(log, equals(<String>[]));
+    expect(log, <String>[]);
     log.clear();
 
     test.addListener(listener);
     test.notify();
-    expect(log, equals(<String>['listener']));
+    expect(log, <String>['listener']);
     log.clear();
 
     test.addListener(listener1);
     test.notify();
-    expect(log, equals(<String>['listener', 'listener1']));
+    expect(log, <String>['listener', 'listener1']);
     log.clear();
 
     test.addListener(listener2);
     test.notify();
-    expect(log, equals(<String>['listener', 'listener1', 'listener2']));
+    expect(log, <String>['listener', 'listener1', 'listener2']);
     log.clear();
 
     test.removeListener(listener1);
     test.notify();
-    expect(log, equals(<String>['listener', 'listener2']));
+    expect(log, <String>['listener', 'listener2']);
     log.clear();
 
     test.addListener(listener1);
     test.notify();
-    expect(log, equals(<String>['listener', 'listener2', 'listener1']));
+    expect(log, <String>['listener', 'listener2', 'listener1']);
     log.clear();
 
     test.addListener(badListener);
     test.notify();
-    expect(log, equals(<String>['listener', 'listener2', 'listener1', 'badListener']));
+    expect(log, <String>['listener', 'listener2', 'listener1', 'badListener']);
     expect(tester.takeException(), isNullThrownError);
     log.clear();
 
@@ -79,7 +79,7 @@
     test.removeListener(listener2);
     test.addListener(listener2);
     test.notify();
-    expect(log, equals(<String>['badListener', 'listener1', 'listener2']));
+    expect(log, <String>['badListener', 'listener1', 'listener2']);
     expect(tester.takeException(), isNullThrownError);
     log.clear();
   });
@@ -102,15 +102,39 @@
     test.addListener(listener2);
     test.addListener(listener3);
     test.notify();
-    expect(log, equals(<String>['listener1', 'listener2']));
+    expect(log, <String>['listener1', 'listener2']);
     log.clear();
 
     test.notify();
-    expect(log, equals(<String>['listener2', 'listener4']));
+    expect(log, <String>['listener2', 'listener4']);
     log.clear();
 
     test.notify();
-    expect(log, equals(<String>['listener2', 'listener4', 'listener4']));
+    expect(log, <String>['listener2', 'listener4', 'listener4']);
+    log.clear();
+  });
+
+  testWidgets('Merging change notifiers', (WidgetTester tester) async {
+    final TestNotifier source1 = new TestNotifier();
+    final TestNotifier source2 = new TestNotifier();
+    final TestNotifier source3 = new TestNotifier();
+    final List<String> log = <String>[];
+
+    final Listenable merged = new Listenable.merge(<Listenable>[source1, source2]);
+    final VoidCallback listener = () { log.add('listener'); };
+
+    merged.addListener(listener);
+    source1.notify();
+    source2.notify();
+    source3.notify();
+    expect(log, <String>['listener', 'listener']);
+    log.clear();
+
+    merged.removeListener(listener);
+    source1.notify();
+    source2.notify();
+    source3.notify();
+    expect(log, isEmpty);
     log.clear();
   });
 }