Commit cc9d602b authored by Adam Barth's avatar Adam Barth

Block should work inside LazyBlock (#3546)

Previously we were locking down the state even when calling layout in
LazyBlock. Now we lock only when building children. Making this work well
involved moving the catch out of lockState and into the few callers who
actually wanted it.

Fixes #3534
parent 00f10da1
...@@ -285,7 +285,7 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi ...@@ -285,7 +285,7 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
} else { } else {
element.update(this); element.update(this);
} }
}, building: true, context: 'while attaching root widget to rendering tree'); }, building: true);
return element; return element;
} }
......
...@@ -710,7 +710,7 @@ class BuildOwner { ...@@ -710,7 +710,7 @@ class BuildOwner {
/// ///
/// The context argument is used to describe the scope in case an exception is /// The context argument is used to describe the scope in case an exception is
/// caught while invoking the callback. /// caught while invoking the callback.
void lockState(void callback(), { bool building: false, String context }) { void lockState(void callback(), { bool building: false }) {
bool debugPreviouslyBuilding; bool debugPreviouslyBuilding;
assert(_debugStateLockLevel >= 0); assert(_debugStateLockLevel >= 0);
assert(() { assert(() {
...@@ -723,8 +723,6 @@ class BuildOwner { ...@@ -723,8 +723,6 @@ class BuildOwner {
}); });
try { try {
callback(); callback();
} catch (e, stack) {
_debugReportException(context, e, stack);
} finally { } finally {
assert(() { assert(() {
_debugStateLockLevel -= 1; _debugStateLockLevel -= 1;
...@@ -758,23 +756,25 @@ class BuildOwner { ...@@ -758,23 +756,25 @@ class BuildOwner {
if (_dirtyElements.isEmpty) if (_dirtyElements.isEmpty)
return; return;
Timeline.startSync('Build'); Timeline.startSync('Build');
lockState(() { try {
_dirtyElements.sort(_elementSort); lockState(() {
int dirtyCount = _dirtyElements.length; _dirtyElements.sort(_elementSort);
int index = 0; int dirtyCount = _dirtyElements.length;
while (index < dirtyCount) { int index = 0;
_dirtyElements[index].rebuild(); while (index < dirtyCount) {
index += 1; _dirtyElements[index].rebuild();
if (dirtyCount < _dirtyElements.length) { index += 1;
_dirtyElements.sort(_elementSort); if (dirtyCount < _dirtyElements.length) {
dirtyCount = _dirtyElements.length; _dirtyElements.sort(_elementSort);
dirtyCount = _dirtyElements.length;
}
} }
} assert(!_dirtyElements.any((BuildableElement element) => element.dirty));
assert(!_dirtyElements.any((BuildableElement element) => element.dirty)); }, building: true);
} finally {
_dirtyElements.clear(); _dirtyElements.clear();
}, building: true, context: 'while rebuilding dirty elements'); Timeline.finishSync();
assert(_dirtyElements.isEmpty); }
Timeline.finishSync();
} }
/// Complete the element build pass by unmounting any elements that are no /// Complete the element build pass by unmounting any elements that are no
...@@ -789,12 +789,17 @@ class BuildOwner { ...@@ -789,12 +789,17 @@ class BuildOwner {
/// about changes to global keys will run. /// about changes to global keys will run.
void finalizeTree() { void finalizeTree() {
Timeline.startSync('Finalize tree'); Timeline.startSync('Finalize tree');
lockState(() { try {
_inactiveElements._unmountAll(); lockState(() {
}, context: 'while finalizing the widget tree'); _inactiveElements._unmountAll();
assert(GlobalKey._debugCheckForDuplicates); });
scheduleMicrotask(GlobalKey._notifyListeners); assert(GlobalKey._debugCheckForDuplicates);
Timeline.finishSync(); scheduleMicrotask(GlobalKey._notifyListeners);
} catch (e, stack) {
_debugReportException('while finalizing the widget tree', e, stack);
} finally {
Timeline.finishSync();
}
} }
/// Cause the entire subtree rooted at the given [Element] to /// Cause the entire subtree rooted at the given [Element] to
......
...@@ -494,165 +494,184 @@ class _LazyBlockElement extends RenderObjectElement { ...@@ -494,165 +494,184 @@ class _LazyBlockElement extends RenderObjectElement {
void _layout(BoxConstraints constraints) { void _layout(BoxConstraints constraints) {
final double blockExtent = _getMainAxisExtent(renderObject.size); final double blockExtent = _getMainAxisExtent(renderObject.size);
owner.lockState(() { final IndexedBuilder builder = widget.delegate.buildItem;
final IndexedBuilder builder = widget.delegate.buildItem; final double startLogicalOffset = widget.startOffset;
final double startLogicalOffset = widget.startOffset; final double endLogicalOffset = startLogicalOffset + blockExtent;
final double endLogicalOffset = startLogicalOffset + blockExtent; final _RenderLazyBlock block = renderObject;
final _RenderLazyBlock block = renderObject; final BoxConstraints innerConstraints = _getInnerConstraints(constraints);
final BoxConstraints innerConstraints = _getInnerConstraints(constraints);
// A high watermark for which children have been through layout this pass.
// A high watermark for which children have been through layout this pass. int firstLogicalIndexNeedingLayout = _firstChildLogicalIndex;
int firstLogicalIndexNeedingLayout = _firstChildLogicalIndex;
// The index of the current child we're examining. The index is the same one
// The index of the current child we're examining. The index is the same one // used for the builder (as opposed to the physical index in the _children
// used for the builder (as opposed to the physical index in the _children // list).
// list). int currentLogicalIndex = _firstChildLogicalIndex;
int currentLogicalIndex = _firstChildLogicalIndex;
// The offset of the current child we're examining from the start of the
// The offset of the current child we're examining from the start of the // entire block (in the direction of the main axis). As we compute layout
// entire block (in the direction of the main axis). As we compute layout // information, we use dead reckoning to keep track of where all the
// information, we use dead reckoning to keep track of where all the // children are based on this quantity.
// children are based on this quantity. double currentLogicalOffset = _firstChildLogicalOffset;
double currentLogicalOffset = _firstChildLogicalOffset;
// First, we check if we need to inflate any children before the start of
// First, we check if we need to inflate any children before the start of // the viewport. Because we're dead reckoning from the current viewport, we
// the viewport. Because we're dead reckoning from the current viewport, we // inflate the children in reverse tree order.
// inflate the children in reverse tree order.
if (currentLogicalIndex > 0 && currentLogicalOffset > startLogicalOffset) {
if (currentLogicalIndex > 0 && currentLogicalOffset > startLogicalOffset) { final List<Element> newChildren = <Element>[];
final List<Element> newChildren = <Element>[];
while (currentLogicalIndex > 0 && currentLogicalOffset > startLogicalOffset) {
while (currentLogicalIndex > 0 && currentLogicalOffset > startLogicalOffset) { currentLogicalIndex -= 1;
currentLogicalIndex -= 1; Element newElement;
owner.lockState(() {
// TODO(abarth): Handle exceptions from builder gracefully.
Widget newWidget = builder(this, currentLogicalIndex); Widget newWidget = builder(this, currentLogicalIndex);
assert(newWidget != null); if (newWidget == null) {
throw new FlutterError(
'buildItem must not return null after returning non-null.\n'
'If buildItem for a LazyBlockDelegate returns a non-null widget for a given '
'index, it must return non-null widgets for every smaller index as well. The '
'buildItem function for ${widget.delegate.runtimeType} returned null for '
'index $currentLogicalIndex after having returned a non-null value for index '
'${currentLogicalIndex - 1}.'
);
}
newWidget = new RepaintBoundary.wrap(newWidget, currentLogicalIndex); newWidget = new RepaintBoundary.wrap(newWidget, currentLogicalIndex);
newChildren.add(inflateWidget(newWidget, null)); newElement = inflateWidget(newWidget, null);
RenderBox child = block.firstChild; }, building: true);
assert(child == newChildren.last.renderObject); newChildren.add(newElement);
child.layout(innerConstraints, parentUsesSize: true); RenderBox child = block.firstChild;
currentLogicalOffset -= _getMainAxisExtent(child.size); assert(child == newChildren.last.renderObject);
} child.layout(innerConstraints, parentUsesSize: true);
currentLogicalOffset -= _getMainAxisExtent(child.size);
final int numberOfNewChildren = newChildren.length; }
_children.insertAll(0, newChildren.reversed);
_firstChildLogicalIndex = currentLogicalIndex; final int numberOfNewChildren = newChildren.length;
_firstChildLogicalOffset = currentLogicalOffset; _children.insertAll(0, newChildren.reversed);
firstLogicalIndexNeedingLayout = currentLogicalIndex + numberOfNewChildren; _firstChildLogicalIndex = currentLogicalIndex;
} else if (currentLogicalOffset < startLogicalOffset) { _firstChildLogicalOffset = currentLogicalOffset;
// If we didn't need to inflate more children before the viewport, we firstLogicalIndexNeedingLayout = currentLogicalIndex + numberOfNewChildren;
// might need to deactivate children that have left the viewport from the } else if (currentLogicalOffset < startLogicalOffset) {
// top. We repeatedly check whether the first child overlaps the viewport // If we didn't need to inflate more children before the viewport, we
// and deactivate it if it's outside the viewport. // might need to deactivate children that have left the viewport from the
int currentPhysicalIndex = 0; // top. We repeatedly check whether the first child overlaps the viewport
while (block.firstChild != null) { // and deactivate it if it's outside the viewport.
RenderBox child = block.firstChild; int currentPhysicalIndex = 0;
child.layout(innerConstraints, parentUsesSize: true); while (block.firstChild != null) {
firstLogicalIndexNeedingLayout += 1; RenderBox child = block.firstChild;
double childExtent = _getMainAxisExtent(child.size); child.layout(innerConstraints, parentUsesSize: true);
if (currentLogicalOffset + childExtent >= startLogicalOffset) firstLogicalIndexNeedingLayout += 1;
break; double childExtent = _getMainAxisExtent(child.size);
deactivateChild(_children[currentPhysicalIndex]); if (currentLogicalOffset + childExtent >= startLogicalOffset)
_children[currentPhysicalIndex] = null; break;
currentPhysicalIndex += 1; deactivateChild(_children[currentPhysicalIndex]);
currentLogicalIndex += 1; _children[currentPhysicalIndex] = null;
currentLogicalOffset += childExtent; currentPhysicalIndex += 1;
} currentLogicalIndex += 1;
currentLogicalOffset += childExtent;
if (currentPhysicalIndex > 0) {
_children.removeRange(0, currentPhysicalIndex);
_firstChildLogicalIndex = currentLogicalIndex;
_firstChildLogicalOffset = currentLogicalOffset;
}
} }
// We've now established the invariant that the first physical child in the if (currentPhysicalIndex > 0) {
// block is the first child that ought to be visible in the viewport. Now we _children.removeRange(0, currentPhysicalIndex);
// need to walk forward until we've filled up the viewport. We might have _firstChildLogicalIndex = currentLogicalIndex;
// already called layout for some of the children we encounter in this phase _firstChildLogicalOffset = currentLogicalOffset;
// of the algorithm, we we'll need to be careful not to call layout on them again.
if (currentLogicalOffset >= startLogicalOffset) {
// The first element is visible. We need to update our reckoning of where
// the min scroll offset is.
_minScrollOffset = currentLogicalOffset;
_startOffsetLowerLimit = double.NEGATIVE_INFINITY;
} else {
// The first element is not visible. Ensure that we have one blockExtent
// of headroom so we don't hit the min scroll offset prematurely.
_minScrollOffset = currentLogicalOffset - blockExtent;
_startOffsetLowerLimit = currentLogicalOffset;
} }
}
// Materialize new children until we fill the viewport (or run out of // We've now established the invariant that the first physical child in the
// children to materialize). // block is the first child that ought to be visible in the viewport. Now we
// need to walk forward until we've filled up the viewport. We might have
// already called layout for some of the children we encounter in this phase
// of the algorithm, we we'll need to be careful not to call layout on them again.
if (currentLogicalOffset >= startLogicalOffset) {
// The first element is visible. We need to update our reckoning of where
// the min scroll offset is.
_minScrollOffset = currentLogicalOffset;
_startOffsetLowerLimit = double.NEGATIVE_INFINITY;
} else {
// The first element is not visible. Ensure that we have one blockExtent
// of headroom so we don't hit the min scroll offset prematurely.
_minScrollOffset = currentLogicalOffset - blockExtent;
_startOffsetLowerLimit = currentLogicalOffset;
}
RenderBox child; // Materialize new children until we fill the viewport (or run out of
while (currentLogicalOffset < endLogicalOffset) { // children to materialize).
int physicalIndex = currentLogicalIndex - _firstChildLogicalIndex;
if (physicalIndex >= _children.length) { RenderBox child;
assert(physicalIndex == _children.length); while (currentLogicalOffset < endLogicalOffset) {
int physicalIndex = currentLogicalIndex - _firstChildLogicalIndex;
if (physicalIndex >= _children.length) {
assert(physicalIndex == _children.length);
Element newElement;
owner.lockState(() {
// TODO(abarth): Handle exceptions from builder gracefully.
Widget newWidget = builder(this, currentLogicalIndex); Widget newWidget = builder(this, currentLogicalIndex);
if (newWidget == null) if (newWidget == null)
break; return;
newWidget = new RepaintBoundary.wrap(newWidget, currentLogicalIndex); newWidget = new RepaintBoundary.wrap(newWidget, currentLogicalIndex);
Element previousChild = _children.isEmpty ? null : _children.last; Element previousChild = _children.isEmpty ? null : _children.last;
_children.add(inflateWidget(newWidget, previousChild)); newElement = inflateWidget(newWidget, previousChild);
} }, building: true);
child = _getNextWithin(block, child); if (newElement == null)
assert(child != null); return;
if (currentLogicalIndex >= firstLogicalIndexNeedingLayout) { _children.add(newElement);
assert(currentLogicalIndex == firstLogicalIndexNeedingLayout);
child.layout(innerConstraints, parentUsesSize: true);
firstLogicalIndexNeedingLayout += 1;
}
currentLogicalOffset += _getMainAxisExtent(child.size);
currentLogicalIndex += 1;
} }
child = _getNextWithin(block, child);
// We now have all the physical children we ought to have to fill the assert(child != null);
// viewport. The currentLogicalIndex is the index of the first child that if (currentLogicalIndex >= firstLogicalIndexNeedingLayout) {
// we don't need. assert(currentLogicalIndex == firstLogicalIndexNeedingLayout);
child.layout(innerConstraints, parentUsesSize: true);
if (currentLogicalOffset < endLogicalOffset) { firstLogicalIndexNeedingLayout += 1;
// The last element is visible. We need to update our reckoning of where
// the max scroll offset is.
_maxScrollOffset = currentLogicalOffset + widget._mainAxisPadding - blockExtent;
_startOffsetUpperLimit = double.INFINITY;
} else {
// The last element is not visible. Ensure that we have one blockExtent
// of headroom so we don't hit the max scroll offset prematurely.
_maxScrollOffset = currentLogicalOffset;
_startOffsetUpperLimit = currentLogicalOffset - blockExtent;
} }
currentLogicalOffset += _getMainAxisExtent(child.size);
currentLogicalIndex += 1;
}
// Remove any unneeded children. // We now have all the physical children we ought to have to fill the
// viewport. The currentLogicalIndex is the index of the first child that
// we don't need.
int currentPhysicalIndex = currentLogicalIndex - _firstChildLogicalIndex; if (currentLogicalOffset < endLogicalOffset) {
final int numberOfRequiredPhysicalChildren = currentPhysicalIndex; // The last element is visible. We need to update our reckoning of where
while (currentPhysicalIndex < _children.length) { // the max scroll offset is.
deactivateChild(_children[currentPhysicalIndex]); _maxScrollOffset = currentLogicalOffset + widget._mainAxisPadding - blockExtent;
_children[currentPhysicalIndex] = null; _startOffsetUpperLimit = double.INFINITY;
currentPhysicalIndex += 1; } else {
} // The last element is not visible. Ensure that we have one blockExtent
_children.length = numberOfRequiredPhysicalChildren; // of headroom so we don't hit the max scroll offset prematurely.
_maxScrollOffset = currentLogicalOffset;
// We now have the correct physical children, each of which has gone through _startOffsetUpperLimit = currentLogicalOffset - blockExtent;
// layout exactly once. We still need to position them correctly. We }
// position the first physical child at Offset.zero and use the paintOffset
// on the render object to adjust the final paint location of the children.
Offset currentChildOffset = _initialChildOffset;
child = block.firstChild;
while (child != null) {
final _LazyBlockParentData childParentData = child.parentData;
childParentData.offset = currentChildOffset;
currentChildOffset += _getMainAxisOffsetForSize(child.size);
child = childParentData.nextSibling;
}
_updatePaintOffset(); // Remove any unneeded children.
}, building: true, context: 'during $runtimeType layout');
int currentPhysicalIndex = currentLogicalIndex - _firstChildLogicalIndex;
final int numberOfRequiredPhysicalChildren = currentPhysicalIndex;
while (currentPhysicalIndex < _children.length) {
deactivateChild(_children[currentPhysicalIndex]);
_children[currentPhysicalIndex] = null;
currentPhysicalIndex += 1;
}
_children.length = numberOfRequiredPhysicalChildren;
// We now have the correct physical children, each of which has gone through
// layout exactly once. We still need to position them correctly. We
// position the first physical child at Offset.zero and use the paintOffset
// on the render object to adjust the final paint location of the children.
Offset currentChildOffset = _initialChildOffset;
child = block.firstChild;
while (child != null) {
final _LazyBlockParentData childParentData = child.parentData;
childParentData.offset = currentChildOffset;
currentChildOffset += _getMainAxisOffsetForSize(child.size);
child = childParentData.nextSibling;
}
_updatePaintOffset();
LazyBlockExtentsChangedCallback onExtentsChanged = widget.onExtentsChanged; LazyBlockExtentsChangedCallback onExtentsChanged = widget.onExtentsChanged;
if (onExtentsChanged != null) { if (onExtentsChanged != null) {
......
...@@ -164,7 +164,7 @@ abstract class VirtualViewportElement extends RenderObjectElement { ...@@ -164,7 +164,7 @@ abstract class VirtualViewportElement extends RenderObjectElement {
assert(startOffsetBase != null); assert(startOffsetBase != null);
assert(startOffsetLimit != null); assert(startOffsetLimit != null);
_updatePaintOffset(); _updatePaintOffset();
owner.lockState(_materializeChildren, building: true, context: 'during $runtimeType layout'); 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:test/test.dart';
void main() {
test('Block inside LazyBlock', () {
testWidgets((WidgetTester tester) {
tester.pumpWidget(new LazyBlock(
delegate: new LazyBlockChildren(
children: <Widget>[
new Block(
children: <Widget>[
new Text('1'),
new Text('2'),
new Text('3'),
]
),
new Block(
children: <Widget>[
new Text('4'),
new Text('5'),
new Text('6'),
]
),
]
)
));
});
});
}
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