Commit 243960d7 authored by krisgiesing's avatar krisgiesing

Merge pull request #3010 from krisgiesing/offscreen_layout

Part 2 of independent layout pipelines
parents 4ff2a338 9dfd5d40
...@@ -33,6 +33,7 @@ class Rectangle extends StatelessWidget { ...@@ -33,6 +33,7 @@ class Rectangle extends StatelessWidget {
double value; double value;
RenderObjectToWidgetElement<RenderBox> element; RenderObjectToWidgetElement<RenderBox> element;
BuildOwner owner;
void attachWidgetTreeToRenderTree(RenderProxyBox container) { void attachWidgetTreeToRenderTree(RenderProxyBox container) {
element = new RenderObjectToWidgetAdapter<RenderBox>( element = new RenderObjectToWidgetAdapter<RenderBox>(
container: container, container: container,
...@@ -70,7 +71,7 @@ void attachWidgetTreeToRenderTree(RenderProxyBox container) { ...@@ -70,7 +71,7 @@ void attachWidgetTreeToRenderTree(RenderProxyBox container) {
mainAxisAlignment: MainAxisAlignment.spaceBetween mainAxisAlignment: MainAxisAlignment.spaceBetween
) )
) )
).attachToRenderTree(element); ).attachToRenderTree(owner, element);
} }
Duration timeBase; Duration timeBase;
......
...@@ -34,7 +34,7 @@ void main() { ...@@ -34,7 +34,7 @@ void main() {
for (int i = 0; i < _kNumberOfIterations || _kRunForever; ++i) { for (int i = 0; i < _kNumberOfIterations || _kRunForever; ++i) {
appState.setState(_doNothing); appState.setState(_doNothing);
binding.buildDirtyElements(); binding.buildOwner.buildDirtyElements();
} }
watch.stop(); watch.stop();
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:developer';
import 'dart:ui' as ui show window; import 'dart:ui' as ui show window;
import 'dart:ui' show AppLifecycleState, Locale; import 'dart:ui' show AppLifecycleState, Locale;
...@@ -26,6 +25,10 @@ class BindingObserver { ...@@ -26,6 +25,10 @@ class BindingObserver {
/// This is the glue that binds the framework to the Flutter engine. /// This is the glue that binds the framework to the Flutter engine.
class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Services, Renderer { class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Services, Renderer {
WidgetFlutterBinding() {
buildOwner.onBuildScheduled = ensureVisualUpdate;
}
/// Creates and initializes the WidgetFlutterBinding. This constructor is /// Creates and initializes the WidgetFlutterBinding. This constructor is
/// idempotent; calling it a second time will just return the /// idempotent; calling it a second time will just return the
/// previously-created instance. /// previously-created instance.
...@@ -35,11 +38,15 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service ...@@ -35,11 +38,15 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service
return _instance; 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;
@override @override
void initInstances() { void initInstances() {
super.initInstances(); super.initInstances();
_instance = this; _instance = this;
BuildableElement.scheduleBuildFor = scheduleBuildFor;
ui.window.onLocaleChanged = handleLocaleChanged; ui.window.onLocaleChanged = handleLocaleChanged;
ui.window.onPopRoute = handlePopRoute; ui.window.onPopRoute = handlePopRoute;
ui.window.onAppLifecycleStateChanged = handleAppLifecycleStateChanged; ui.window.onAppLifecycleStateChanged = handleAppLifecycleStateChanged;
...@@ -92,60 +99,9 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service ...@@ -92,60 +99,9 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service
@override @override
void beginFrame() { void beginFrame() {
buildDirtyElements(); buildOwner.buildDirtyElements();
super.beginFrame(); super.beginFrame();
Element.finalizeTree(); buildOwner.finalizeTree();
}
List<BuildableElement> _dirtyElements = <BuildableElement>[];
/// Adds an element to the dirty elements list so that it will be rebuilt
/// when buildDirtyElements is called.
void scheduleBuildFor(BuildableElement element) {
assert(!_dirtyElements.contains(element));
assert(element.dirty);
if (_dirtyElements.isEmpty)
ensureVisualUpdate();
_dirtyElements.add(element);
}
static int _elementSort(BuildableElement a, BuildableElement b) {
if (a.depth < b.depth)
return -1;
if (b.depth < a.depth)
return 1;
if (b.dirty && !a.dirty)
return -1;
if (a.dirty && !b.dirty)
return 1;
return 0;
}
/// Builds all the elements that were marked as dirty using schedule(), in depth order.
/// If elements are marked as dirty while this runs, they must be deeper than the algorithm
/// has yet reached.
/// This is called by beginFrame().
void buildDirtyElements() {
if (_dirtyElements.isEmpty)
return;
Timeline.startSync('Build');
BuildableElement.lockState(() {
_dirtyElements.sort(_elementSort);
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
_dirtyElements[index].rebuild();
index += 1;
if (dirtyCount < _dirtyElements.length) {
_dirtyElements.sort(_elementSort);
dirtyCount = _dirtyElements.length;
}
}
assert(!_dirtyElements.any((BuildableElement element) => element.dirty));
_dirtyElements.clear();
}, building: true);
assert(_dirtyElements.isEmpty);
Timeline.finishSync();
} }
/// The [Element] that is at the root of the hierarchy (and which wraps the /// The [Element] that is at the root of the hierarchy (and which wraps the
...@@ -157,7 +113,7 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service ...@@ -157,7 +113,7 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service
container: renderView, container: renderView,
debugShortDescription: '[root]', debugShortDescription: '[root]',
child: app child: app
).attachToRenderTree(_renderViewElement); ).attachToRenderTree(buildOwner, _renderViewElement);
beginFrame(); beginFrame();
} }
} }
...@@ -205,10 +161,11 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi ...@@ -205,10 +161,11 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
@override @override
void updateRenderObject(BuildContext context, RenderObject renderObject) { } void updateRenderObject(BuildContext context, RenderObject renderObject) { }
RenderObjectToWidgetElement<T> attachToRenderTree([RenderObjectToWidgetElement<T> element]) { RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
BuildableElement.lockState(() { owner.lockState(() {
if (element == null) { if (element == null) {
element = createElement(); element = createElement();
element.assignOwner(owner);
element.mount(null, null); element.mount(null, null);
} else { } else {
element.update(this); element.update(this);
...@@ -229,7 +186,7 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi ...@@ -229,7 +186,7 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
/// whose container is the RenderView that connects to the Flutter engine. In /// whose container is the RenderView that connects to the Flutter engine. In
/// this usage, it is normally instantiated by the bootstrapping logic in the /// this usage, it is normally instantiated by the bootstrapping logic in the
/// WidgetFlutterBinding singleton created by runApp(). /// WidgetFlutterBinding singleton created by runApp().
class RenderObjectToWidgetElement<T extends RenderObject> extends RenderObjectElement { class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObjectElement {
RenderObjectToWidgetElement(RenderObjectToWidgetAdapter<T> widget) : super(widget); RenderObjectToWidgetElement(RenderObjectToWidgetAdapter<T> widget) : super(widget);
@override @override
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:developer';
import 'debug.dart'; import 'debug.dart';
...@@ -613,17 +614,15 @@ class _InactiveElements { ...@@ -613,17 +614,15 @@ class _InactiveElements {
}); });
} }
void unmountAll() { void _unmountAll() {
BuildableElement.lockState(() { try {
try { _locked = true;
_locked = true; for (Element element in _elements)
for (Element element in _elements) _unmount(element);
_unmount(element); } finally {
} finally { _elements.clear();
_elements.clear(); _locked = false;
_locked = false; }
}
});
} }
void _deactivateRecursively(Element element) { void _deactivateRecursively(Element element) {
...@@ -652,8 +651,6 @@ class _InactiveElements { ...@@ -652,8 +651,6 @@ class _InactiveElements {
} }
} }
final _InactiveElements _inactiveElements = new _InactiveElements();
typedef void ElementVisitor(Element element); typedef void ElementVisitor(Element element);
abstract class BuildContext { abstract class BuildContext {
...@@ -667,6 +664,120 @@ abstract class BuildContext { ...@@ -667,6 +664,120 @@ abstract class BuildContext {
void visitChildElements(void visitor(Element element)); void visitChildElements(void visitor(Element element));
} }
class BuildOwner {
BuildOwner({ this.onBuildScheduled });
/// Called on each build pass when the first buildable element is marked dirty
VoidCallback onBuildScheduled;
final _InactiveElements _inactiveElements = new _InactiveElements();
final List<BuildableElement> _dirtyElements = <BuildableElement>[];
/// Adds an element to the dirty elements list so that it will be rebuilt
/// when buildDirtyElements is called.
void scheduleBuildFor(BuildableElement element) {
assert(!_dirtyElements.contains(element));
assert(element.dirty);
if (_dirtyElements.isEmpty && onBuildScheduled != null)
onBuildScheduled();
_dirtyElements.add(element);
}
int _debugStateLockLevel = 0;
bool get _debugStateLocked => _debugStateLockLevel > 0;
bool _debugBuilding = false;
BuildableElement _debugCurrentBuildTarget;
/// Establishes a scope in which widget build functions can run.
///
/// Inside a build scope, widget build functions are allowed to run, but
/// State.setState() is forbidden. This mechanism prevents build functions
/// from transitively requiring other build functions to run, potentially
/// causing infinite loops.
///
/// After unwinding the last build scope on the stack, the framework verifies
/// that each global key is used at most once and notifies listeners about
/// changes to global keys.
void lockState(void callback(), { bool building: false }) {
assert(_debugStateLockLevel >= 0);
assert(() {
if (building) {
assert(!_debugBuilding);
assert(_debugCurrentBuildTarget == null);
_debugBuilding = true;
}
_debugStateLockLevel += 1;
return true;
});
try {
callback();
} finally {
assert(() {
_debugStateLockLevel -= 1;
if (building) {
assert(_debugBuilding);
assert(_debugCurrentBuildTarget == null);
_debugBuilding = false;
}
return true;
});
}
assert(_debugStateLockLevel >= 0);
}
static int _elementSort(BuildableElement a, BuildableElement b) {
if (a.depth < b.depth)
return -1;
if (b.depth < a.depth)
return 1;
if (b.dirty && !a.dirty)
return -1;
if (a.dirty && !b.dirty)
return 1;
return 0;
}
/// Builds all the elements that were marked as dirty using schedule(), in depth order.
/// If elements are marked as dirty while this runs, they must be deeper than the algorithm
/// has yet reached.
/// This is called by beginFrame().
void buildDirtyElements() {
if (_dirtyElements.isEmpty)
return;
Timeline.startSync('Build');
lockState(() {
_dirtyElements.sort(_elementSort);
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
_dirtyElements[index].rebuild();
index += 1;
if (dirtyCount < _dirtyElements.length) {
_dirtyElements.sort(_elementSort);
dirtyCount = _dirtyElements.length;
}
}
assert(!_dirtyElements.any((BuildableElement element) => element.dirty));
_dirtyElements.clear();
}, building: true);
assert(_dirtyElements.isEmpty);
Timeline.finishSync();
}
/// Complete the element build pass by unmounting any elements that are no
/// longer active.
/// This is called by beginFrame().
void finalizeTree() {
lockState(() {
_inactiveElements._unmountAll();
});
assert(GlobalKey._debugCheckForDuplicates);
scheduleMicrotask(GlobalKey._notifyListeners);
}
}
/// Elements are the instantiations of Widget configurations. /// Elements are the instantiations of Widget configurations.
/// ///
/// Elements can, in principle, have children. Only subclasses of /// Elements can, in principle, have children. Only subclasses of
...@@ -696,6 +807,10 @@ abstract class Element implements BuildContext { ...@@ -696,6 +807,10 @@ abstract class Element implements BuildContext {
Widget get widget => _widget; Widget get widget => _widget;
Widget _widget; Widget _widget;
BuildOwner _owner;
/// The owner for this node (null if unattached).
BuildOwner get owner => _owner;
bool _active = false; bool _active = false;
RenderObject get renderObject { RenderObject get renderObject {
...@@ -721,7 +836,7 @@ abstract class Element implements BuildContext { ...@@ -721,7 +836,7 @@ abstract class Element implements BuildContext {
@override @override
void visitChildElements(void visitor(Element element)) { void visitChildElements(void visitor(Element element)) {
// don't allow visitChildElements() during build, since children aren't necessarily built yet // don't allow visitChildElements() during build, since children aren't necessarily built yet
assert(!BuildableElement._debugStateLocked); assert(owner == null || !owner._debugStateLocked);
visitChildren(visitor); visitChildren(visitor);
} }
...@@ -774,12 +889,6 @@ abstract class Element implements BuildContext { ...@@ -774,12 +889,6 @@ abstract class Element implements BuildContext {
return inflateWidget(newWidget, newSlot); return inflateWidget(newWidget, newSlot);
} }
static void finalizeTree() {
_inactiveElements.unmountAll();
assert(GlobalKey._debugCheckForDuplicates);
scheduleMicrotask(GlobalKey._notifyListeners);
}
/// Called when an Element is given a new parent shortly after having been /// Called when an Element is given a new parent shortly after having been
/// created. Use this to initialize state that depends on having a parent. For /// created. Use this to initialize state that depends on having a parent. For
/// state that is independent of the position in the tree, it's better to just /// state that is independent of the position in the tree, it's better to just
...@@ -796,6 +905,8 @@ abstract class Element implements BuildContext { ...@@ -796,6 +905,8 @@ abstract class Element implements BuildContext {
_slot = newSlot; _slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1; _depth = _parent != null ? _parent.depth + 1 : 1;
_active = true; _active = true;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner;
if (widget.key is GlobalKey) { if (widget.key is GlobalKey) {
final GlobalKey key = widget.key; final GlobalKey key = widget.key;
key._register(this); key._register(this);
...@@ -874,7 +985,7 @@ abstract class Element implements BuildContext { ...@@ -874,7 +985,7 @@ abstract class Element implements BuildContext {
if (element._parent != null && !element._parent.detachChild(element)) if (element._parent != null && !element._parent.detachChild(element))
return null; return null;
assert(element._parent == null); assert(element._parent == null);
_inactiveElements.remove(element); owner._inactiveElements.remove(element);
return element; return element;
} }
...@@ -914,7 +1025,7 @@ abstract class Element implements BuildContext { ...@@ -914,7 +1025,7 @@ abstract class Element implements BuildContext {
assert(child._parent == this); assert(child._parent == this);
child._parent = null; child._parent = null;
child.detachRenderObject(); child.detachRenderObject();
_inactiveElements.add(child); // this eventually calls child.deactivate() owner._inactiveElements.add(child); // this eventually calls child.deactivate()
} }
void _activateWithParent(Element parent, dynamic newSlot) { void _activateWithParent(Element parent, dynamic newSlot) {
...@@ -1117,8 +1228,6 @@ class ErrorWidget extends LeafRenderObjectWidget { ...@@ -1117,8 +1228,6 @@ class ErrorWidget extends LeafRenderObjectWidget {
RenderBox createRenderObject(BuildContext context) => new RenderErrorBox(message); RenderBox createRenderObject(BuildContext context) => new RenderErrorBox(message);
} }
typedef void BuildScheduler(BuildableElement element);
/// Base class for instantiations of widgets that have builders and can be /// Base class for instantiations of widgets that have builders and can be
/// marked dirty. /// marked dirty.
abstract class BuildableElement extends Element { abstract class BuildableElement extends Element {
...@@ -1139,50 +1248,6 @@ abstract class BuildableElement extends Element { ...@@ -1139,50 +1248,6 @@ abstract class BuildableElement extends Element {
return true; return true;
} }
static BuildScheduler scheduleBuildFor;
static int _debugStateLockLevel = 0;
static bool get _debugStateLocked => _debugStateLockLevel > 0;
static bool _debugBuilding = false;
static BuildableElement _debugCurrentBuildTarget;
/// Establishes a scope in which widget build functions can run.
///
/// Inside a build scope, widget build functions are allowed to run, but
/// State.setState() is forbidden. This mechanism prevents build functions
/// from transitively requiring other build functions to run, potentially
/// causing infinite loops.
///
/// After unwinding the last build scope on the stack, the framework verifies
/// that each global key is used at most once and notifies listeners about
/// changes to global keys.
static void lockState(void callback(), { bool building: false }) {
assert(_debugStateLockLevel >= 0);
assert(() {
if (building) {
assert(!_debugBuilding);
assert(_debugCurrentBuildTarget == null);
_debugBuilding = true;
}
_debugStateLockLevel += 1;
return true;
});
try {
callback();
} finally {
assert(() {
_debugStateLockLevel -= 1;
if (building) {
assert(_debugBuilding);
assert(_debugCurrentBuildTarget == null);
_debugBuilding = false;
}
return true;
});
}
assert(_debugStateLockLevel >= 0);
}
/// Marks the element as dirty and adds it to the global list of widgets to /// Marks the element as dirty and adds it to the global list of widgets to
/// rebuild in the next frame. /// rebuild in the next frame.
/// ///
...@@ -1194,10 +1259,11 @@ abstract class BuildableElement extends Element { ...@@ -1194,10 +1259,11 @@ abstract class BuildableElement extends Element {
assert(_debugLifecycleState != _ElementLifecycle.defunct); assert(_debugLifecycleState != _ElementLifecycle.defunct);
if (!_active) if (!_active)
return; return;
assert(owner != null);
assert(_debugLifecycleState == _ElementLifecycle.active); assert(_debugLifecycleState == _ElementLifecycle.active);
assert(() { assert(() {
if (_debugBuilding) { if (owner._debugBuilding) {
if (_debugCurrentBuildTarget == null) { if (owner._debugCurrentBuildTarget == null) {
// If _debugCurrentBuildTarget is null, we're not actually building a // If _debugCurrentBuildTarget is null, we're not actually building a
// widget but instead building the root of the tree via runApp. // widget but instead building the root of the tree via runApp.
// TODO(abarth): Remove these cases and ensure that we always have // TODO(abarth): Remove these cases and ensure that we always have
...@@ -1206,7 +1272,7 @@ abstract class BuildableElement extends Element { ...@@ -1206,7 +1272,7 @@ abstract class BuildableElement extends Element {
} }
bool foundTarget = false; bool foundTarget = false;
visitAncestorElements((Element element) { visitAncestorElements((Element element) {
if (element == _debugCurrentBuildTarget) { if (element == owner._debugCurrentBuildTarget) {
foundTarget = true; foundTarget = true;
return false; return false;
} }
...@@ -1215,7 +1281,7 @@ abstract class BuildableElement extends Element { ...@@ -1215,7 +1281,7 @@ abstract class BuildableElement extends Element {
if (foundTarget) if (foundTarget)
return true; return true;
} }
if (_debugStateLocked && (!_debugAllowIgnoredCallsToMarkNeedsBuild || !dirty)) { if (owner._debugStateLocked && (!_debugAllowIgnoredCallsToMarkNeedsBuild || !dirty)) {
throw new FlutterError( throw new FlutterError(
'setState() or markNeedsBuild() called during build.\n' 'setState() or markNeedsBuild() called during build.\n'
'This widget cannot be marked as needing to build because the framework ' 'This widget cannot be marked as needing to build because the framework '
...@@ -1232,8 +1298,7 @@ abstract class BuildableElement extends Element { ...@@ -1232,8 +1298,7 @@ abstract class BuildableElement extends Element {
if (dirty) if (dirty)
return; return;
_dirty = true; _dirty = true;
assert(scheduleBuildFor != null); owner.scheduleBuildFor(this);
scheduleBuildFor(this);
} }
/// Called by the binding when scheduleBuild() has been called to mark this /// Called by the binding when scheduleBuild() has been called to mark this
...@@ -1246,17 +1311,17 @@ abstract class BuildableElement extends Element { ...@@ -1246,17 +1311,17 @@ abstract class BuildableElement extends Element {
return; return;
} }
assert(_debugLifecycleState == _ElementLifecycle.active); assert(_debugLifecycleState == _ElementLifecycle.active);
assert(_debugStateLocked); assert(owner._debugStateLocked);
BuildableElement debugPreviousBuildTarget; BuildableElement debugPreviousBuildTarget;
assert(() { assert(() {
debugPreviousBuildTarget = _debugCurrentBuildTarget; debugPreviousBuildTarget = owner._debugCurrentBuildTarget;
_debugCurrentBuildTarget = this; owner._debugCurrentBuildTarget = this;
return true; return true;
}); });
performRebuild(); performRebuild();
assert(() { assert(() {
assert(_debugCurrentBuildTarget == this); assert(owner._debugCurrentBuildTarget == this);
_debugCurrentBuildTarget = debugPreviousBuildTarget; owner._debugCurrentBuildTarget = debugPreviousBuildTarget;
return true; return true;
}); });
assert(!_dirty); assert(!_dirty);
...@@ -1879,6 +1944,26 @@ abstract class RenderObjectElement extends BuildableElement { ...@@ -1879,6 +1944,26 @@ abstract class RenderObjectElement extends BuildableElement {
} }
} }
/// Instantiation of RenderObjectWidgets at the root of the tree
///
/// Only root elements may have their owner set explicitly. All other
/// elements inherit their owner from their parent.
abstract class RootRenderObjectElement extends RenderObjectElement {
RootRenderObjectElement(RenderObjectWidget widget): super(widget);
void assignOwner(BuildOwner owner) {
_owner = owner;
}
@override
void mount(Element parent, dynamic newSlot) {
// Root elements should never have parents
assert(parent == null);
assert(newSlot == null);
super.mount(parent, newSlot);
}
}
/// Instantiation of RenderObjectWidgets that have no children /// Instantiation of RenderObjectWidgets that have no children
class LeafRenderObjectElement extends RenderObjectElement { class LeafRenderObjectElement extends RenderObjectElement {
LeafRenderObjectElement(LeafRenderObjectWidget widget): super(widget); LeafRenderObjectElement(LeafRenderObjectWidget widget): super(widget);
......
...@@ -252,7 +252,7 @@ class _MixedViewportElement extends RenderObjectElement { ...@@ -252,7 +252,7 @@ class _MixedViewportElement extends RenderObjectElement {
_resetCache(); _resetCache();
_lastLayoutConstraints = constraints; _lastLayoutConstraints = constraints;
} }
BuildableElement.lockState(() { owner.lockState(() {
_doLayout(constraints); _doLayout(constraints);
}, building: true); }, building: true);
} }
......
...@@ -157,7 +157,7 @@ abstract class VirtualViewportElement extends RenderObjectElement { ...@@ -157,7 +157,7 @@ abstract class VirtualViewportElement extends RenderObjectElement {
assert(startOffsetBase != null); assert(startOffsetBase != null);
assert(startOffsetLimit != null); assert(startOffsetLimit != null);
_updatePaintOffset(); _updatePaintOffset();
BuildableElement.lockState(_materializeChildren, building: true); owner.lockState(_materializeChildren, building: true);
} }
void _materializeChildren() { void _materializeChildren() {
......
// 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 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:test/test.dart';
const Size _kTestViewSize = const Size(800.0, 600.0);
class OffscreenRenderView extends RenderView {
OffscreenRenderView() {
configuration = new ViewConfiguration(size: _kTestViewSize);
}
@override
void scheduleInitialFrame() {
scheduleInitialLayout();
scheduleInitialPaint(new TransformLayer(transform: new Matrix4.identity()));
// Don't call Scheduler.instance.ensureVisualUpdate()
}
@override
void compositeFrame() {
// Don't draw to ui.window
}
}
class OffscreenWidgetTree {
OffscreenWidgetTree() {
renderView.attach(pipelineOwner);
renderView.scheduleInitialFrame();
}
final RenderView renderView = new OffscreenRenderView();
final BuildOwner buildOwner = new BuildOwner();
final PipelineOwner pipelineOwner = new PipelineOwner();
RenderObjectToWidgetElement<RenderBox> root;
void pumpWidget(Widget app) {
root = new RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: app
).attachToRenderTree(buildOwner, root);
pumpFrame();
}
void pumpFrame() {
buildOwner.buildDirtyElements();
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame();
if (SemanticsNode.hasListeners) {
pipelineOwner.flushSemantics();
SemanticsNode.sendSemanticsTree();
}
buildOwner.finalizeTree();
}
}
class Counter {
int count = 0;
}
class Trigger {
VoidCallback callback;
void fire() {
if (callback != null)
callback();
}
}
class TriggerableWidget extends StatefulWidget {
TriggerableWidget({ this.trigger, this.counter });
final Trigger trigger;
final Counter counter;
@override
TriggerableState createState() => new TriggerableState();
}
class TriggerableState extends State<TriggerableWidget> {
@override
void initState() {
super.initState();
config.trigger.callback = this.fire;
}
@override
void didUpdateConfig(TriggerableWidget oldConfig) {
config.trigger.callback = this.fire;
}
int _count = 0;
void fire() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
config.counter.count++;
return new Text("Bang $_count!");
}
}
void main() {
test('no crosstalk between widget build owners', () {
testWidgets((WidgetTester tester) {
Trigger trigger1 = new Trigger();
Counter counter1 = new Counter();
Trigger trigger2 = new Trigger();
Counter counter2 = new Counter();
OffscreenWidgetTree tree = new OffscreenWidgetTree();
// Both counts should start at zero
expect(counter1.count, equals(0));
expect(counter2.count, equals(0));
// Lay out the "onscreen" in the default test binding
tester.pumpWidget(new TriggerableWidget(trigger: trigger1, counter: counter1));
// Only the "onscreen" widget should have built
expect(counter1.count, equals(1));
expect(counter2.count, equals(0));
// Lay out the "offscreen" in a separate tree
tree.pumpWidget(new TriggerableWidget(trigger: trigger2, counter: counter2));
// Now both widgets should have built
expect(counter1.count, equals(1));
expect(counter2.count, equals(1));
// Mark both as needing layout
trigger1.fire();
trigger2.fire();
// Marking as needing layout shouldn't immediately build anything
expect(counter1.count, equals(1));
expect(counter2.count, equals(1));
// Pump the "onscreen" layout
tester.pump();
// Only the "onscreen" widget should have rebuilt
expect(counter1.count, equals(2));
expect(counter2.count, equals(1));
// Pump the "offscreen" layout
tree.pumpFrame();
// Now both widgets should have rebuilt
expect(counter1.count, equals(2));
expect(counter2.count, equals(2));
// Mark both as needing layout, again
trigger1.fire();
trigger2.fire();
// Now pump the "offscreen" layout first
tree.pumpFrame();
// Only the "offscreen" widget should have rebuilt
expect(counter1.count, equals(2));
expect(counter2.count, equals(3));
// Pump the "onscreen" layout
tester.pump();
// Now both widgets should have rebuilt
expect(counter1.count, equals(3));
expect(counter2.count, equals(3));
});
});
}
...@@ -38,9 +38,9 @@ class _SteppedWidgetFlutterBinding extends WidgetFlutterBinding { ...@@ -38,9 +38,9 @@ class _SteppedWidgetFlutterBinding extends WidgetFlutterBinding {
// Pump the rendering pipeline up to the given phase. // Pump the rendering pipeline up to the given phase.
@override @override
void beginFrame() { void beginFrame() {
buildDirtyElements(); buildOwner.buildDirtyElements();
_beginFrame(); _beginFrame();
Element.finalizeTree(); buildOwner.finalizeTree();
} }
// Cloned from Renderer.beginFrame() but with early-exit semantics. // Cloned from Renderer.beginFrame() but with early-exit semantics.
...@@ -84,7 +84,7 @@ class WidgetTester extends Instrumentation { ...@@ -84,7 +84,7 @@ class WidgetTester extends Instrumentation {
final FakeAsync async; final FakeAsync async;
final Clock clock; final Clock clock;
/// Calls [runApp()] with the given widget, then triggers a frame sequent and /// Calls [runApp()] with the given widget, then triggers a frame sequence and
/// flushes microtasks, by calling [pump()] with the same duration (if any). /// flushes microtasks, by calling [pump()] with the same duration (if any).
/// The supplied EnginePhase is the final phase reached during the pump pass; /// The supplied EnginePhase is the final phase reached during the pump pass;
/// if not supplied, the whole pass is executed. /// if not supplied, the whole pass is executed.
......
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