Unverified Commit 324fdb6b authored by Yegor's avatar Yegor Committed by GitHub

LayoutBuilder: skip calling builder when constraints are the same (#55414)

Avoid calling `builder` in `ConstrainedLayoutBuilder` when layout constraints are the same.

[Design doc](flutter.dev/go/layout-builder-optimization).

## Related Issues

Fixes https://github.com/flutter/flutter/issues/6469
parent f865ac7e
......@@ -215,6 +215,14 @@ class _BackdropState extends State<Backdrop> with SingleTickerProviderStateMixin
value: 1.0,
vsync: this,
);
_controller.addStatusListener((AnimationStatus status) {
setState(() {
// This is intentionally left empty. The state change itself takes
// place inside the AnimationController, so there's nothing to update.
// All we want is for the widget to rebuild and read the new animation
// state from the AnimationController.
});
});
_frontOpacity = _controller.drive(_frontOpacityTween);
}
......
......@@ -17,6 +17,18 @@ typedef LayoutWidgetBuilder = Widget Function(BuildContext context, BoxConstrain
/// function at layout time and provides the constraints that this widget should
/// adhere to. This is useful when the parent constrains the child's size and layout,
/// and doesn't depend on the child's intrinsic size.
///
/// {@template flutter.widgets.layoutBuilder.builderFunctionInvocation}
/// The [builder] function is called in the following situations:
///
/// * The first time the widget is laid out.
/// * When the parent widget passes different layout constraints.
/// * When the parent widget updates this widget.
/// * When the depedencies that the [builder] function subscribes to change.
///
/// The [builder] function is _not_ called during layout if the parent passes
/// the same constraints repeatedly.
/// {@endtemplate}
abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints> extends RenderObjectWidget {
/// Creates a widget that defers its building until layout.
///
......@@ -74,15 +86,22 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
assert(widget != newWidget);
super.update(newWidget);
assert(widget == newWidget);
renderObject.updateCallback(_layout);
renderObject.markNeedsLayout();
// Force the callback to be called, even if the layout constraints are the
// same, because the logic in the callback might have changed.
renderObject.markNeedsBuild();
}
@override
void performRebuild() {
// This gets called if markNeedsBuild() is called on us.
// That might happen if, e.g., our builder uses Inherited widgets.
renderObject.markNeedsLayout();
// Force the callback to be called, even if the layout constraints are the
// same. This is because that callback may depend on the updated widget
// configuration, or an inherited widget.
renderObject.markNeedsBuild();
super.performRebuild(); // Calls widget.updateRenderObject (a no-op in this case).
}
......@@ -168,10 +187,42 @@ mixin RenderConstrainedLayoutBuilder<ConstraintType extends Constraints, ChildTy
markNeedsLayout();
}
/// Invoke the layout callback.
void layoutAndBuildChild() {
bool _needsBuild = true;
/// Marks this layout builder as needing to rebuild.
///
/// The layout build rebuilds automatically when layout constraints change.
/// However, we must also rebuild when the widget updates, e.g. after
/// [State.setState], or [State.didChangeDependencies], even when the layout
/// constraints remain unchanged.
///
/// See also:
///
/// * [ConstrainedLayoutBuilder.builder], which is called during the rebuild.
void markNeedsBuild() {
// Do not call the callback directly. It must be called during the layout
// phase, when parent constraints are available. Calling `markNeedsLayout`
// will cause it to be called at the right time.
_needsBuild = true;
markNeedsLayout();
}
// The constraints that were passed to this class last time it was laid out.
// These constraints are compared to the new constraints to determine whether
// [ConstrainedLayoutBuilder.builder] needs to be called.
Constraints _previousConstraints;
/// Invoke the callback supplied via [updateCallback].
///
/// Typically this results in [ConstrainedLayoutBuilder.builder] being called
/// during layout.
void rebuildIfNecessary() {
assert(_callback != null);
invokeLayoutCallback(_callback);
if (_needsBuild || constraints != _previousConstraints) {
_previousConstraints = constraints;
_needsBuild = false;
invokeLayoutCallback(_callback);
}
}
}
......@@ -183,11 +234,13 @@ mixin RenderConstrainedLayoutBuilder<ConstraintType extends Constraints, ChildTy
/// the child's intrinsic size. The [LayoutBuilder]'s final size will match its
/// child's size.
///
/// {@macro flutter.widgets.layoutBuilder.builderFunctionInvocation}
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=IYDVcriKjsw}
///
/// If the child should be smaller than the parent, consider wrapping the child
/// in an [Align] widget. If the child might want to be bigger, consider
/// wrapping it in a [SingleChildScrollView].
/// wrapping it in a [SingleChildScrollView] or [OverflowBox].
///
/// See also:
///
......@@ -241,7 +294,7 @@ class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin<Ren
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
layoutAndBuildChild();
rebuildIfNecessary();
if (child != null) {
child.layout(constraints, parentUsesSize: true);
size = constraints.constrain(child.size);
......
......@@ -19,6 +19,7 @@ typedef SliverLayoutWidgetBuilder = Widget Function(BuildContext context, Sliver
/// The [SliverLayoutBuilder]'s final [SliverGeometry] will match the [SliverGeometry]
/// of its child.
///
/// {@macro flutter.widgets.layoutBuilder.builderFunctionInvocation}
///
/// See also:
///
......@@ -52,7 +53,7 @@ class _RenderSliverLayoutBuilder extends RenderSliver with RenderObjectWithChild
@override
void performLayout() {
layoutAndBuildChild();
rebuildIfNecessary();
child?.layout(constraints, parentUsesSize: true);
geometry = child?.geometry ?? SliverGeometry.zero;
}
......
......@@ -587,4 +587,111 @@ void main() {
await tester.pump();
expect(hitCounts, const <int> [0, 0, 0]);
});
testWidgets('LayoutBuilder does not call builder when layout happens but layout constraints do not change', (WidgetTester tester) async {
int builderInvocationCount = 0;
Future<void> pumpTestWidget(Size size) async {
await tester.pumpWidget(
// Center is used to give the SizedBox the power to determine constraints for LayoutBuilder
Center(
child: SizedBox.fromSize(
size: size,
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
builderInvocationCount += 1;
return _LayoutSpy();
}),
),
),
);
}
await pumpTestWidget(const Size(10, 10));
final _RenderLayoutSpy spy = tester.renderObject(find.byType(_LayoutSpy));
// The child is laid out once the first time.
expect(spy.performLayoutCount, 1);
expect(spy.performResizeCount, 1);
// The initial `pumpWidget` will trigger `performRebuild`, asking for
// builder invocation.
expect(builderInvocationCount, 1);
// Invalidate the layout without chaning the constraints.
tester.renderObject(find.byType(LayoutBuilder)).markNeedsLayout();
// The second pump will not go through the `performRebuild` or `update`, and
// only judge the need for builder invocation based on constraints, which
// didn't change, so we don't expect any counters to go up.
await tester.pump();
expect(builderInvocationCount, 1);
expect(spy.performLayoutCount, 1);
expect(spy.performResizeCount, 1);
// Cause the `update` to be called (but not `performRebuild`), triggering
// builder invocation.
await pumpTestWidget(const Size(10, 10));
expect(builderInvocationCount, 2);
// The spy does not invalidate its layout on widget update, so no
// layout-related methods should be called.
expect(spy.performLayoutCount, 1);
expect(spy.performResizeCount, 1);
// Have the child request layout and verify that the child gets laid out
// despite layout constraints remaining constant.
spy.markNeedsLayout();
await tester.pump();
// Builder is not invoked. This was a layout-only pump with the same parent
// constraints.
expect(builderInvocationCount, 2);
// Expect performLayout to be called.
expect(spy.performLayoutCount, 2);
// performResize should not be called because the spy sets sizedByParent,
// and the constraints did not change.
expect(spy.performResizeCount, 1);
// Change the parent size, triggering constraint change.
await pumpTestWidget(const Size(20, 20));
// We should see everything invoked once.
expect(builderInvocationCount, 3);
expect(spy.performLayoutCount, 3);
expect(spy.performResizeCount, 2);
});
}
class _LayoutSpy extends LeafRenderObjectWidget {
@override
LeafRenderObjectElement createElement() => _LayoutSpyElement(this);
@override
RenderObject createRenderObject(BuildContext context) => _RenderLayoutSpy();
}
class _LayoutSpyElement extends LeafRenderObjectElement {
_LayoutSpyElement(LeafRenderObjectWidget widget) : super(widget);
}
class _RenderLayoutSpy extends RenderBox {
int performLayoutCount = 0;
int performResizeCount = 0;
@override
bool get sizedByParent => true;
@override
void performResize() {
performResizeCount += 1;
size = constraints.biggest;
}
@override
void performLayout() {
performLayoutCount += 1;
}
}
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