Commit 0e11b0e6 authored by Ian Hickson's avatar Ian Hickson

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.
parent cf56caa7
......@@ -89,7 +89,7 @@ void rotate(Duration timeStamp) {
}
void main() {
WidgetFlutterBinding.ensureInitialized();
Widgeteer binding = WidgetFlutterBinding.ensureInitialized();
RenderProxyBox proxy = new RenderProxyBox();
attachWidgetTreeToRenderTree(proxy);
......@@ -101,6 +101,6 @@ void main() {
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);
}
......@@ -27,14 +27,14 @@ void main() {
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();
......
......@@ -24,14 +24,14 @@ void main() {
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();
......
......@@ -20,9 +20,7 @@ import 'semantics.dart';
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 @@ abstract class Renderer extends Object with Scheduler, Services
});
}
/// 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 @@ abstract class Renderer extends Object with Scheduler, Services
/// 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 @@ abstract class Renderer extends Object with Scheduler, Services
_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 @@ abstract class Renderer extends Object with Scheduler, Services
}
/// 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();
......
......@@ -98,7 +98,7 @@ class WidgetsApp extends StatefulWidget {
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 @@ class WidgetsAppState<T extends WidgetsApp> extends State<T> implements BindingO
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();
}
......
......@@ -15,44 +15,58 @@ import 'framework.dart';
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 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service
);
}
/// The one static instance of this class.
///
/// 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;
final List<BindingObserver> _observers = new List<BindingObserver>();
/// 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();
void addObserver(BindingObserver observer) => _observers.add(observer);
bool removeObserver(BindingObserver observer) => _observers.remove(observer);
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.
///
/// 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);
/// 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);
/// 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 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service
/// 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 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service
}
/// 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 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObje
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;
}
}
......@@ -417,7 +417,7 @@ class _DragAvatar<T> extends Drag {
_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();
......
......@@ -21,7 +21,7 @@ const String _extensionMethodName = 'driver';
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 @@ class _DriverBinding extends WidgetFlutterBinding {
/// 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.
......
......@@ -100,9 +100,9 @@ void main() {
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 @@ void main() {
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));
});
});
......
......@@ -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';
......
......@@ -23,18 +23,18 @@ enum EnginePhase {
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;
......
......@@ -17,10 +17,10 @@ typedef Point SizeToPointFunction(Size size);
/// 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);
......
......@@ -7,7 +7,7 @@ import 'package:flutter/widgets.dart';
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 @@ class WidgetTester {
/// 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].
///
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment