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();
});
}