Make the widgets binding reusable. (#3479)
Previously the widgets layer only provided a concrete binding, which
makes it awkward to extend it compared to other bindings. This moves
widgets to the same style as the other layers.
In a subsequent patch I'll use this to make the tests layer saner.
diff --git a/examples/layers/widgets/spinning_mixed.dart b/examples/layers/widgets/spinning_mixed.dart
index b6e1e7a..e7c094b 100644
--- a/examples/layers/widgets/spinning_mixed.dart
+++ b/examples/layers/widgets/spinning_mixed.dart
@@ -89,7 +89,7 @@
}
void main() {
- WidgetFlutterBinding.ensureInitialized();
+ Widgeteer binding = WidgetFlutterBinding.ensureInitialized();
RenderProxyBox proxy = new RenderProxyBox();
attachWidgetTreeToRenderTree(proxy);
@@ -101,6 +101,6 @@
transformBox = new RenderTransform(child: flexRoot, transform: new Matrix4.identity());
RenderPadding root = new RenderPadding(padding: new EdgeInsets.all(80.0), child: transformBox);
- WidgetFlutterBinding.instance.renderView.child = root;
- WidgetFlutterBinding.instance.addPersistentFrameCallback(rotate);
+ binding.renderView.child = root;
+ binding.addPersistentFrameCallback(rotate);
}
diff --git a/packages/flutter/benchmark/stocks/build_bench.dart b/packages/flutter/benchmark/stocks/build_bench.dart
index 8a623b8..41c35cf 100644
--- a/packages/flutter/benchmark/stocks/build_bench.dart
+++ b/packages/flutter/benchmark/stocks/build_bench.dart
@@ -27,14 +27,14 @@
appState = tester.stateOf(find.byType(stocks.StocksApp));
});
- WidgetFlutterBinding binding = WidgetFlutterBinding.instance;
+ BuildOwner buildOwner = Widgeteer.instance.buildOwner;
Stopwatch watch = new Stopwatch()
..start();
for (int i = 0; i < _kNumberOfIterations || _kRunForever; ++i) {
appState.setState(_doNothing);
- binding.buildOwner.buildDirtyElements();
+ buildOwner.buildDirtyElements();
}
watch.stop();
diff --git a/packages/flutter/benchmark/stocks/layout_bench.dart b/packages/flutter/benchmark/stocks/layout_bench.dart
index 1ce9f42..720f2af 100644
--- a/packages/flutter/benchmark/stocks/layout_bench.dart
+++ b/packages/flutter/benchmark/stocks/layout_bench.dart
@@ -24,14 +24,14 @@
ViewConfiguration big = const ViewConfiguration(size: const Size(360.0, 640.0));
ViewConfiguration small = const ViewConfiguration(size: const Size(355.0, 635.0));
- RenderView renderView = WidgetFlutterBinding.instance.renderView;
+ RenderView renderView = Widgeteer.instance.renderView;
Stopwatch watch = new Stopwatch()
..start();
for (int i = 0; i < _kNumberOfIterations || _kRunForever; ++i) {
renderView.configuration = (i % 2 == 0) ? big : small;
- WidgetFlutterBinding.instance.pipelineOwner.flushLayout();
+ Renderer.instance.pipelineOwner.flushLayout();
}
watch.stop();
diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart
index 46fde26..e794300 100644
--- a/packages/flutter/lib/src/rendering/binding.dart
+++ b/packages/flutter/lib/src/rendering/binding.dart
@@ -20,9 +20,7 @@
export 'package:flutter/gestures.dart' show HitTestResult;
/// The glue between the render tree and the Flutter engine.
-abstract class Renderer extends Object with Scheduler, Services
- implements HitTestable {
-
+abstract class Renderer implements Scheduler, Services, HitTestable {
@override
void initInstances() {
super.initInstances();
@@ -73,6 +71,12 @@
});
}
+ /// Creates a [RenderView] object to be the root of the
+ /// [RenderObject] rendering tree, and initializes it so that it
+ /// will be rendered when the engine is next ready to display a
+ /// frame.
+ ///
+ /// Called automatically when the binding is created.
void initRenderView() {
if (renderView == null) {
renderView = new RenderView();
@@ -88,6 +92,8 @@
/// The render tree that's attached to the output surface.
RenderView get renderView => _renderView;
RenderView _renderView;
+ /// Sets the given [RenderView] object (which must not be null), and its tree, to
+ /// be the new render tree to display. The previous tree, if any, is detached.
void set renderView(RenderView value) {
assert(value != null);
if (_renderView == value)
@@ -98,11 +104,17 @@
_renderView.attach(pipelineOwner);
}
+ /// Invoked when the system metrics change.
+ ///
+ /// See [ui.window.onMetricsChanged].
void handleMetricsChanged() {
assert(renderView != null);
renderView.configuration = new ViewConfiguration(size: ui.window.size);
}
+ /// Prepares the rendering library to handle semantics requests from the engine.
+ ///
+ /// Called automatically when the binding is created.
void initSemantics() {
SemanticsNode.onSemanticsEnabled = renderView.scheduleInitialSemantics;
shell.provideService(mojom.SemanticsServer.serviceName, (core.MojoMessagePipeEndpoint endpoint) {
@@ -116,6 +128,8 @@
}
/// Pump the rendering pipeline to generate a frame.
+ ///
+ /// Called automatically by the engine when it is time to lay out and paint a frame.
void beginFrame() {
assert(renderView != null);
pipelineOwner.flushLayout();
diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart
index 41ff9d2..6e890a3 100644
--- a/packages/flutter/lib/src/widgets/app.dart
+++ b/packages/flutter/lib/src/widgets/app.dart
@@ -98,7 +98,7 @@
WidgetsAppState<WidgetsApp> createState() => new WidgetsAppState<WidgetsApp>();
}
-class WidgetsAppState<T extends WidgetsApp> extends State<T> implements BindingObserver {
+class WidgetsAppState<T extends WidgetsApp> extends State<T> implements WidgetsBindingObserver {
GlobalObjectKey _navigator;
@@ -109,12 +109,12 @@
super.initState();
_navigator = new GlobalObjectKey(this);
didChangeLocale(ui.window.locale);
- WidgetFlutterBinding.instance.addObserver(this);
+ Widgeteer.instance.addObserver(this);
}
@override
void dispose() {
- WidgetFlutterBinding.instance.removeObserver(this);
+ Widgeteer.instance.removeObserver(this);
super.dispose();
}
diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart
index cc9e03e..291dda0 100644
--- a/packages/flutter/lib/src/widgets/binding.dart
+++ b/packages/flutter/lib/src/widgets/binding.dart
@@ -15,44 +15,58 @@
export 'dart:ui' show AppLifecycleState, Locale;
-class BindingObserver {
+/// Interface for classes that register with the Widgets layer binding.
+///
+/// See [Widgeteer.addObserver] and [Widgeteer.removeObserver].
+abstract class WidgetsBindingObserver {
+ /// Called when the system tells the app to pop the current route.
+ /// For example, on Android, this is called when the user presses
+ /// the back button.
+ ///
+ /// Observers are notified in registration order until one returns
+ /// true. If none return true, the application quits.
+ ///
+ /// Observers are expected to return true if they were able to
+ /// handle the notification, for example by closing an active dialog
+ /// box, and false otherwise. The [WidgetsApp] widget uses this
+ /// mechanism to notify the [Navigator] widget that it should pop
+ /// its current route if possible.
bool didPopRoute() => false;
+
+ /// Called when the application's dimensions change. For example,
+ /// when a phone is rotated.
void didChangeMetrics() { }
+
+ /// Called when the system tells the app that the user's locale has
+ /// changed. For example, if the user changes the system language
+ /// settings.
void didChangeLocale(Locale locale) { }
+
+ /// Called when the system puts the app in the background or returns
+ /// the app to the foreground.
void didChangeAppLifecycleState(AppLifecycleState state) { }
}
-/// A concrete binding for applications based on the Widgets framework.
-/// This is the glue that binds the framework to the Flutter engine.
-class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Services, Renderer {
-
- WidgetFlutterBinding() {
- buildOwner.onBuildScheduled = ensureVisualUpdate;
- }
-
- /// Creates and initializes the WidgetFlutterBinding. This constructor is
- /// idempotent; calling it a second time will just return the
- /// previously-created instance.
- static WidgetFlutterBinding ensureInitialized() {
- if (_instance == null)
- new WidgetFlutterBinding();
- return _instance;
- }
-
- final BuildOwner _buildOwner = new BuildOwner();
- /// The [BuildOwner] in charge of executing the build pipeline for the
- /// widget tree rooted at this binding.
- BuildOwner get buildOwner => _buildOwner;
-
+/// The glue between the widgets layer and the Flutter engine.
+abstract class Widgeteer implements Gesturer, Renderer {
@override
void initInstances() {
super.initInstances();
_instance = this;
+ buildOwner.onBuildScheduled = ensureVisualUpdate;
ui.window.onLocaleChanged = handleLocaleChanged;
ui.window.onPopRoute = handlePopRoute;
ui.window.onAppLifecycleStateChanged = handleAppLifecycleStateChanged;
}
+ /// The current [Widgeteer], if one has been created.
+ ///
+ /// If you need the binding to be constructed before calling [runApp],
+ /// you can ensure a Widget binding has been constructed by calling the
+ /// `WidgetFlutterBinding.ensureInitialized()` function.
+ static Widgeteer get instance => _instance;
+ static Widgeteer _instance;
+
@override
void initServiceExtensions() {
super.initServiceExtensions();
@@ -69,48 +83,91 @@
);
}
- /// The one static instance of this class.
+ /// The [BuildOwner] in charge of executing the build pipeline for the
+ /// widget tree rooted at this binding.
+ BuildOwner get buildOwner => _buildOwner;
+ final BuildOwner _buildOwner = new BuildOwner();
+
+ final List<WidgetsBindingObserver> _observers = <WidgetsBindingObserver>[];
+
+ /// Registers the given object as a binding observer. Binding
+ /// observers are notified when various application events occur,
+ /// for example when the system locale changes. Generally, one
+ /// widget in the widget tree registers itself as a binding
+ /// observer, and converts the system state into inherited widgets.
///
- /// Only valid after the WidgetFlutterBinding constructor) has been called.
- /// Only one binding class can be instantiated per process. If another
- /// BindingBase implementation has been instantiated before this one (e.g.
- /// bindings from other frameworks based on the Flutter "rendering" library),
- /// then WidgetFlutterBinding.instance will not be valid (and will throw in
- /// checked mode).
- static WidgetFlutterBinding get instance => _instance;
- static WidgetFlutterBinding _instance;
+ /// For example, the [WidgetsApp] widget registers as a binding
+ /// observer and passes the screen size to a [MediaQuery] widget
+ /// each time it is built, which enables other widgets to use the
+ /// [MediaQuery.of] static method and (implicitly) the
+ /// [InheritedWidget] mechanism to be notified whenever the screen
+ /// size changes (e.g. whenever the screen rotates).
+ void addObserver(WidgetsBindingObserver observer) => _observers.add(observer);
- final List<BindingObserver> _observers = new List<BindingObserver>();
+ /// Unregisters the given observer. This should be used sparingly as
+ /// it is relatively expensive (O(N) in the number of registered
+ /// observers).
+ bool removeObserver(WidgetsBindingObserver observer) => _observers.remove(observer);
- void addObserver(BindingObserver observer) => _observers.add(observer);
- bool removeObserver(BindingObserver observer) => _observers.remove(observer);
-
+ /// Invoked when the system metrics change.
+ ///
+ /// Notifies all the observers using
+ /// [WidgetsBindingObserver.didChangeMetrics].
+ ///
+ /// See [ui.window.onMetricsChanged].
@override
void handleMetricsChanged() {
super.handleMetricsChanged();
- for (BindingObserver observer in _observers)
+ for (WidgetsBindingObserver observer in _observers)
observer.didChangeMetrics();
}
+ /// Invoked when the system locale changes.
+ ///
+ /// Calls [dispatchLocaleChanged] to notify the binding observers.
+ ///
+ /// See [ui.window.onLocaleChanged].
void handleLocaleChanged() {
dispatchLocaleChanged(ui.window.locale);
}
+ /// Notify all the observers that the locale has changed (using
+ /// [WidgetsBindingObserver.didChangeLocale]), giving them the
+ /// `locale` argument.
void dispatchLocaleChanged(Locale locale) {
- for (BindingObserver observer in _observers)
+ for (WidgetsBindingObserver observer in _observers)
observer.didChangeLocale(locale);
}
+ /// Invoked when the system pops the current route.
+ ///
+ /// This first notifies the binding observers (using
+ /// [WidgetsBindingObserver.didPopRoute]), in registration order,
+ /// until one returns true, meaning that it was able to handle the
+ /// request (e.g. by closing a dialog box). If none return true,
+ /// then the application is shut down.
+ ///
+ /// [WidgetsApp] uses this in conjunction with a [Navigator] to
+ /// cause the back button to close dialog boxes, return from modal
+ /// pages, and so forth.
+ ///
+ /// See [ui.window.onPopRoute].
void handlePopRoute() {
- for (BindingObserver observer in _observers) {
+ for (WidgetsBindingObserver observer in _observers) {
if (observer.didPopRoute())
return;
}
activity.finishCurrentActivity();
}
+ /// Invoked when the application lifecycle state changes.
+ ///
+ /// Notifies all the observers using
+ /// [WidgetsBindingObserver.didChangeAppLifecycleState].
+ ///
+ /// See [ui.window.onAppLifecycleStateChanged].
void handleAppLifecycleStateChanged(AppLifecycleState state) {
- for (BindingObserver observer in _observers)
+ for (WidgetsBindingObserver observer in _observers)
observer.didChangeAppLifecycleState(state);
}
@@ -123,6 +180,8 @@
/// The [Element] that is at the root of the hierarchy (and which wraps the
/// [RenderView] object at the root of the rendering hierarchy).
+ ///
+ /// This is initialized the first time [runApp] is called.
Element get renderViewElement => _renderViewElement;
Element _renderViewElement;
void _runApp(Widget app) {
@@ -142,18 +201,20 @@
}
/// Inflate the given widget and attach it to the screen.
+///
+/// Initializes the binding using [WidgetFlutterBinding] if necessary.
void runApp(Widget app) {
WidgetFlutterBinding.ensureInitialized()._runApp(app);
}
/// Print a string representation of the currently running app.
void debugDumpApp() {
- assert(WidgetFlutterBinding.instance != null);
- assert(WidgetFlutterBinding.instance.renderViewElement != null);
+ assert(Widgeteer.instance != null);
+ assert(Widgeteer.instance.renderViewElement != null);
String mode = 'RELEASE MODE';
assert(() { mode = 'CHECKED MODE'; return true; });
- debugPrint('${WidgetFlutterBinding.instance.runtimeType} - $mode');
- debugPrint(WidgetFlutterBinding.instance.renderViewElement.toStringDeep());
+ debugPrint('${Widgeteer.instance.runtimeType} - $mode');
+ debugPrint(Widgeteer.instance.renderViewElement.toStringDeep());
}
/// This class provides a bridge from a RenderObject to an Element tree. The
@@ -259,3 +320,19 @@
renderObject.child = null;
}
}
+
+/// A concrete binding for applications based on the Widgets framework.
+/// This is the glue that binds the framework to the Flutter engine.
+class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Services, Renderer, Widgeteer {
+ /// Creates and initializes the WidgetFlutterBinding. This function
+ /// is idempotent; calling it a second time will just return the
+ /// previously-created instance.
+ ///
+ /// You only need to call this method if you need the binding to be
+ /// initialized before calling [runApp].
+ static WidgetFlutterBinding ensureInitialized() {
+ if (Widgeteer.instance == null)
+ new WidgetFlutterBinding();
+ return Widgeteer.instance;
+ }
+}
diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart
index defc875..c76c0d3 100644
--- a/packages/flutter/lib/src/widgets/drag_target.dart
+++ b/packages/flutter/lib/src/widgets/drag_target.dart
@@ -417,7 +417,7 @@
_lastOffset = globalPosition - dragStartPoint;
_entry.markNeedsBuild();
HitTestResult result = new HitTestResult();
- WidgetFlutterBinding.instance.hitTest(result, globalPosition + feedbackOffset);
+ Widgeteer.instance.hitTest(result, globalPosition + feedbackOffset);
List<_DragTargetState<T>> targets = _getDragTargets(result.path).toList();
diff --git a/packages/flutter_driver/lib/src/extension.dart b/packages/flutter_driver/lib/src/extension.dart
index eaac038..e6e0c9e 100644
--- a/packages/flutter_driver/lib/src/extension.dart
+++ b/packages/flutter_driver/lib/src/extension.dart
@@ -21,7 +21,7 @@
const String _extensionMethod = 'ext.flutter.$_extensionMethodName';
const Duration _kDefaultTimeout = const Duration(seconds: 5);
-class _DriverBinding extends WidgetFlutterBinding {
+class _DriverBinding extends WidgetFlutterBinding { // TODO(ianh): refactor so we're not extending a concrete binding
@override
void initServiceExtensions() {
super.initServiceExtensions();
@@ -41,9 +41,9 @@
/// Call this function prior to running your application, e.g. before you call
/// `runApp`.
void enableFlutterDriverExtension() {
- assert(WidgetFlutterBinding.instance == null);
+ assert(Widgeteer.instance == null);
new _DriverBinding();
- assert(WidgetFlutterBinding.instance is _DriverBinding);
+ assert(Widgeteer.instance is _DriverBinding);
}
/// Handles a command and returns a result.
diff --git a/packages/flutter_markdown/test/flutter_markdown_test.dart b/packages/flutter_markdown/test/flutter_markdown_test.dart
index 942ab53..975a3f5 100644
--- a/packages/flutter_markdown/test/flutter_markdown_test.dart
+++ b/packages/flutter_markdown/test/flutter_markdown_test.dart
@@ -100,9 +100,9 @@
tester.pumpWidget(new Markdown(data: "Data1"));
_expectTextStrings(tester.widgets, <String>["Data1"]);
- String stateBefore = WidgetFlutterBinding.instance.renderViewElement.toStringDeep();
+ String stateBefore = Widgeteer.instance.renderViewElement.toStringDeep();
tester.pumpWidget(new Markdown(data: "Data1"));
- String stateAfter = WidgetFlutterBinding.instance.renderViewElement.toStringDeep();
+ String stateAfter = Widgeteer.instance.renderViewElement.toStringDeep();
expect(stateBefore, equals(stateAfter));
tester.pumpWidget(new Markdown(data: "Data2"));
@@ -119,9 +119,9 @@
tester.pumpWidget(new Markdown(data: "Test", markdownStyle: style1));
- String stateBefore = WidgetFlutterBinding.instance.renderViewElement.toStringDeep();
+ String stateBefore = Widgeteer.instance.renderViewElement.toStringDeep();
tester.pumpWidget(new Markdown(data: "Test", markdownStyle: style2));
- String stateAfter = WidgetFlutterBinding.instance.renderViewElement.toStringDeep();
+ String stateAfter = Widgeteer.instance.renderViewElement.toStringDeep();
expect(stateBefore, isNot(stateAfter));
});
});
diff --git a/packages/flutter_test/lib/flutter_test.dart b/packages/flutter_test/lib/flutter_test.dart
index 81a1550..5883d9b 100644
--- a/packages/flutter_test/lib/flutter_test.dart
+++ b/packages/flutter_test/lib/flutter_test.dart
@@ -5,7 +5,7 @@
/// Testing library for flutter, built on top of `package:test`.
library flutter_test;
-export 'src/element_tree_tester.dart';
+export 'src/binding.dart';
export 'src/instrumentation.dart';
export 'src/service_mocker.dart';
export 'src/test_pointer.dart';
diff --git a/packages/flutter_test/lib/src/element_tree_tester.dart b/packages/flutter_test/lib/src/binding.dart
similarity index 95%
rename from packages/flutter_test/lib/src/element_tree_tester.dart
rename to packages/flutter_test/lib/src/binding.dart
index 3b4be16..41d579c 100644
--- a/packages/flutter_test/lib/src/element_tree_tester.dart
+++ b/packages/flutter_test/lib/src/binding.dart
@@ -23,18 +23,18 @@
sendSemanticsTree
}
-class _SteppedWidgetFlutterBinding extends WidgetFlutterBinding {
- _SteppedWidgetFlutterBinding._(this.async);
+class _SteppedWidgetFlutterBinding extends WidgetFlutterBinding { // TODO(ianh): refactor so we're not extending a concrete binding
+ _SteppedWidgetFlutterBinding(this.async);
final FakeAsync async;
/// Creates and initializes the binding. This constructor is
/// idempotent; calling it a second time will just return the
/// previously-created instance.
- static WidgetFlutterBinding ensureInitialized(FakeAsync async) {
- if (WidgetFlutterBinding.instance == null)
- new _SteppedWidgetFlutterBinding._(async);
- return WidgetFlutterBinding.instance;
+ static Widgeteer ensureInitialized(FakeAsync async) {
+ if (Widgeteer.instance == null)
+ new _SteppedWidgetFlutterBinding(async);
+ return Widgeteer.instance;
}
EnginePhase phase = EnginePhase.sendSemanticsTree;
diff --git a/packages/flutter_test/lib/src/instrumentation.dart b/packages/flutter_test/lib/src/instrumentation.dart
index 19c9ce6..d0d774e 100644
--- a/packages/flutter_test/lib/src/instrumentation.dart
+++ b/packages/flutter_test/lib/src/instrumentation.dart
@@ -17,10 +17,10 @@
/// This class provides hooks for accessing the rendering tree and dispatching
/// fake tap/drag/etc. events.
class Instrumentation {
- Instrumentation({ WidgetFlutterBinding binding })
+ Instrumentation({ Widgeteer binding })
: this.binding = binding ?? WidgetFlutterBinding.ensureInitialized();
- final WidgetFlutterBinding binding;
+ final Widgeteer binding;
/// Returns a list of all the [Layer] objects in the rendering.
List<Layer> get layers => _layers(binding.renderView.layer);
diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart
index 2faa639..faac1c5 100644
--- a/packages/flutter_test/lib/src/widget_tester.dart
+++ b/packages/flutter_test/lib/src/widget_tester.dart
@@ -7,7 +7,7 @@
import 'package:quiver/testing/async.dart';
import 'package:test/test.dart';
-import 'element_tree_tester.dart';
+import 'binding.dart';
import 'test_pointer.dart';
/// Runs the [callback] inside the Flutter test environment.
@@ -57,7 +57,7 @@
/// Exposes the [Element] tree created from widgets.
final ElementTreeTester elementTreeTester;
- WidgetFlutterBinding get binding => elementTreeTester.binding;
+ Widgeteer get binding => elementTreeTester.binding;
/// Renders the UI from the given [widget].
///