Merge pull request #3010 from krisgiesing/offscreen_layout
Part 2 of independent layout pipelines
This commit is contained in:
commit
243960d741
@ -33,6 +33,7 @@ class Rectangle extends StatelessWidget {
|
||||
|
||||
double value;
|
||||
RenderObjectToWidgetElement<RenderBox> element;
|
||||
BuildOwner owner;
|
||||
void attachWidgetTreeToRenderTree(RenderProxyBox container) {
|
||||
element = new RenderObjectToWidgetAdapter<RenderBox>(
|
||||
container: container,
|
||||
@ -70,7 +71,7 @@ void attachWidgetTreeToRenderTree(RenderProxyBox container) {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween
|
||||
)
|
||||
)
|
||||
).attachToRenderTree(element);
|
||||
).attachToRenderTree(owner, element);
|
||||
}
|
||||
|
||||
Duration timeBase;
|
||||
|
@ -34,7 +34,7 @@ void main() {
|
||||
|
||||
for (int i = 0; i < _kNumberOfIterations || _kRunForever; ++i) {
|
||||
appState.setState(_doNothing);
|
||||
binding.buildDirtyElements();
|
||||
binding.buildOwner.buildDirtyElements();
|
||||
}
|
||||
|
||||
watch.stop();
|
||||
|
@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:developer';
|
||||
import 'dart:ui' as ui show window;
|
||||
import 'dart:ui' show AppLifecycleState, Locale;
|
||||
|
||||
@ -26,6 +25,10 @@ class BindingObserver {
|
||||
/// 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.
|
||||
@ -35,11 +38,15 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service
|
||||
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
|
||||
void initInstances() {
|
||||
super.initInstances();
|
||||
_instance = this;
|
||||
BuildableElement.scheduleBuildFor = scheduleBuildFor;
|
||||
ui.window.onLocaleChanged = handleLocaleChanged;
|
||||
ui.window.onPopRoute = handlePopRoute;
|
||||
ui.window.onAppLifecycleStateChanged = handleAppLifecycleStateChanged;
|
||||
@ -92,60 +99,9 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service
|
||||
|
||||
@override
|
||||
void beginFrame() {
|
||||
buildDirtyElements();
|
||||
buildOwner.buildDirtyElements();
|
||||
super.beginFrame();
|
||||
Element.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();
|
||||
buildOwner.finalizeTree();
|
||||
}
|
||||
|
||||
/// 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
|
||||
container: renderView,
|
||||
debugShortDescription: '[root]',
|
||||
child: app
|
||||
).attachToRenderTree(_renderViewElement);
|
||||
).attachToRenderTree(buildOwner, _renderViewElement);
|
||||
beginFrame();
|
||||
}
|
||||
}
|
||||
@ -205,10 +161,11 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderObject renderObject) { }
|
||||
|
||||
RenderObjectToWidgetElement<T> attachToRenderTree([RenderObjectToWidgetElement<T> element]) {
|
||||
BuildableElement.lockState(() {
|
||||
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
|
||||
owner.lockState(() {
|
||||
if (element == null) {
|
||||
element = createElement();
|
||||
element.assignOwner(owner);
|
||||
element.mount(null, null);
|
||||
} else {
|
||||
element.update(this);
|
||||
@ -229,7 +186,7 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
|
||||
/// whose container is the RenderView that connects to the Flutter engine. In
|
||||
/// this usage, it is normally instantiated by the bootstrapping logic in the
|
||||
/// 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);
|
||||
|
||||
@override
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'debug.dart';
|
||||
|
||||
@ -613,17 +614,15 @@ class _InactiveElements {
|
||||
});
|
||||
}
|
||||
|
||||
void unmountAll() {
|
||||
BuildableElement.lockState(() {
|
||||
try {
|
||||
_locked = true;
|
||||
for (Element element in _elements)
|
||||
_unmount(element);
|
||||
} finally {
|
||||
_elements.clear();
|
||||
_locked = false;
|
||||
}
|
||||
});
|
||||
void _unmountAll() {
|
||||
try {
|
||||
_locked = true;
|
||||
for (Element element in _elements)
|
||||
_unmount(element);
|
||||
} finally {
|
||||
_elements.clear();
|
||||
_locked = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _deactivateRecursively(Element element) {
|
||||
@ -652,8 +651,6 @@ class _InactiveElements {
|
||||
}
|
||||
}
|
||||
|
||||
final _InactiveElements _inactiveElements = new _InactiveElements();
|
||||
|
||||
typedef void ElementVisitor(Element element);
|
||||
|
||||
abstract class BuildContext {
|
||||
@ -667,6 +664,120 @@ abstract class BuildContext {
|
||||
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 can, in principle, have children. Only subclasses of
|
||||
@ -696,6 +807,10 @@ abstract class Element implements BuildContext {
|
||||
Widget get widget => _widget;
|
||||
Widget _widget;
|
||||
|
||||
BuildOwner _owner;
|
||||
/// The owner for this node (null if unattached).
|
||||
BuildOwner get owner => _owner;
|
||||
|
||||
bool _active = false;
|
||||
|
||||
RenderObject get renderObject {
|
||||
@ -721,7 +836,7 @@ abstract class Element implements BuildContext {
|
||||
@override
|
||||
void visitChildElements(void visitor(Element element)) {
|
||||
// don't allow visitChildElements() during build, since children aren't necessarily built yet
|
||||
assert(!BuildableElement._debugStateLocked);
|
||||
assert(owner == null || !owner._debugStateLocked);
|
||||
visitChildren(visitor);
|
||||
}
|
||||
|
||||
@ -774,12 +889,6 @@ abstract class Element implements BuildContext {
|
||||
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
|
||||
/// 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
|
||||
@ -796,6 +905,8 @@ abstract class Element implements BuildContext {
|
||||
_slot = newSlot;
|
||||
_depth = _parent != null ? _parent.depth + 1 : 1;
|
||||
_active = true;
|
||||
if (parent != null) // Only assign ownership if the parent is non-null
|
||||
_owner = parent.owner;
|
||||
if (widget.key is GlobalKey) {
|
||||
final GlobalKey key = widget.key;
|
||||
key._register(this);
|
||||
@ -874,7 +985,7 @@ abstract class Element implements BuildContext {
|
||||
if (element._parent != null && !element._parent.detachChild(element))
|
||||
return null;
|
||||
assert(element._parent == null);
|
||||
_inactiveElements.remove(element);
|
||||
owner._inactiveElements.remove(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
@ -914,7 +1025,7 @@ abstract class Element implements BuildContext {
|
||||
assert(child._parent == this);
|
||||
child._parent = null;
|
||||
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) {
|
||||
@ -1117,8 +1228,6 @@ class ErrorWidget extends LeafRenderObjectWidget {
|
||||
RenderBox createRenderObject(BuildContext context) => new RenderErrorBox(message);
|
||||
}
|
||||
|
||||
typedef void BuildScheduler(BuildableElement element);
|
||||
|
||||
/// Base class for instantiations of widgets that have builders and can be
|
||||
/// marked dirty.
|
||||
abstract class BuildableElement extends Element {
|
||||
@ -1139,50 +1248,6 @@ abstract class BuildableElement extends Element {
|
||||
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
|
||||
/// rebuild in the next frame.
|
||||
///
|
||||
@ -1194,10 +1259,11 @@ abstract class BuildableElement extends Element {
|
||||
assert(_debugLifecycleState != _ElementLifecycle.defunct);
|
||||
if (!_active)
|
||||
return;
|
||||
assert(owner != null);
|
||||
assert(_debugLifecycleState == _ElementLifecycle.active);
|
||||
assert(() {
|
||||
if (_debugBuilding) {
|
||||
if (_debugCurrentBuildTarget == null) {
|
||||
if (owner._debugBuilding) {
|
||||
if (owner._debugCurrentBuildTarget == null) {
|
||||
// If _debugCurrentBuildTarget is null, we're not actually building a
|
||||
// widget but instead building the root of the tree via runApp.
|
||||
// TODO(abarth): Remove these cases and ensure that we always have
|
||||
@ -1206,7 +1272,7 @@ abstract class BuildableElement extends Element {
|
||||
}
|
||||
bool foundTarget = false;
|
||||
visitAncestorElements((Element element) {
|
||||
if (element == _debugCurrentBuildTarget) {
|
||||
if (element == owner._debugCurrentBuildTarget) {
|
||||
foundTarget = true;
|
||||
return false;
|
||||
}
|
||||
@ -1215,7 +1281,7 @@ abstract class BuildableElement extends Element {
|
||||
if (foundTarget)
|
||||
return true;
|
||||
}
|
||||
if (_debugStateLocked && (!_debugAllowIgnoredCallsToMarkNeedsBuild || !dirty)) {
|
||||
if (owner._debugStateLocked && (!_debugAllowIgnoredCallsToMarkNeedsBuild || !dirty)) {
|
||||
throw new FlutterError(
|
||||
'setState() or markNeedsBuild() called during build.\n'
|
||||
'This widget cannot be marked as needing to build because the framework '
|
||||
@ -1232,8 +1298,7 @@ abstract class BuildableElement extends Element {
|
||||
if (dirty)
|
||||
return;
|
||||
_dirty = true;
|
||||
assert(scheduleBuildFor != null);
|
||||
scheduleBuildFor(this);
|
||||
owner.scheduleBuildFor(this);
|
||||
}
|
||||
|
||||
/// Called by the binding when scheduleBuild() has been called to mark this
|
||||
@ -1246,17 +1311,17 @@ abstract class BuildableElement extends Element {
|
||||
return;
|
||||
}
|
||||
assert(_debugLifecycleState == _ElementLifecycle.active);
|
||||
assert(_debugStateLocked);
|
||||
assert(owner._debugStateLocked);
|
||||
BuildableElement debugPreviousBuildTarget;
|
||||
assert(() {
|
||||
debugPreviousBuildTarget = _debugCurrentBuildTarget;
|
||||
_debugCurrentBuildTarget = this;
|
||||
debugPreviousBuildTarget = owner._debugCurrentBuildTarget;
|
||||
owner._debugCurrentBuildTarget = this;
|
||||
return true;
|
||||
});
|
||||
performRebuild();
|
||||
assert(() {
|
||||
assert(_debugCurrentBuildTarget == this);
|
||||
_debugCurrentBuildTarget = debugPreviousBuildTarget;
|
||||
assert(owner._debugCurrentBuildTarget == this);
|
||||
owner._debugCurrentBuildTarget = debugPreviousBuildTarget;
|
||||
return true;
|
||||
});
|
||||
assert(!_dirty);
|
||||
@ -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
|
||||
class LeafRenderObjectElement extends RenderObjectElement {
|
||||
LeafRenderObjectElement(LeafRenderObjectWidget widget): super(widget);
|
||||
|
@ -252,7 +252,7 @@ class _MixedViewportElement extends RenderObjectElement {
|
||||
_resetCache();
|
||||
_lastLayoutConstraints = constraints;
|
||||
}
|
||||
BuildableElement.lockState(() {
|
||||
owner.lockState(() {
|
||||
_doLayout(constraints);
|
||||
}, building: true);
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ abstract class VirtualViewportElement extends RenderObjectElement {
|
||||
assert(startOffsetBase != null);
|
||||
assert(startOffsetLimit != null);
|
||||
_updatePaintOffset();
|
||||
BuildableElement.lockState(_materializeChildren, building: true);
|
||||
owner.lockState(_materializeChildren, building: true);
|
||||
}
|
||||
|
||||
void _materializeChildren() {
|
||||
|
163
packages/flutter/test/widget/independent_widget_layout_test.dart
Normal file
163
packages/flutter/test/widget/independent_widget_layout_test.dart
Normal file
@ -0,0 +1,163 @@
|
||||
// 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 {
|
||||
// Pump the rendering pipeline up to the given phase.
|
||||
@override
|
||||
void beginFrame() {
|
||||
buildDirtyElements();
|
||||
buildOwner.buildDirtyElements();
|
||||
_beginFrame();
|
||||
Element.finalizeTree();
|
||||
buildOwner.finalizeTree();
|
||||
}
|
||||
|
||||
// Cloned from Renderer.beginFrame() but with early-exit semantics.
|
||||
@ -84,7 +84,7 @@ class WidgetTester extends Instrumentation {
|
||||
final FakeAsync async;
|
||||
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).
|
||||
/// The supplied EnginePhase is the final phase reached during the pump pass;
|
||||
/// if not supplied, the whole pass is executed.
|
||||
|
Loading…
x
Reference in New Issue
Block a user