Skip to content

Commit 324fdb6

Browse files
authored
LayoutBuilder: skip calling builder when constraints are the same (flutter#55414)
Avoid calling `builder` in `ConstrainedLayoutBuilder` when layout constraints are the same. [Design doc](flutter.dev/go/layout-builder-optimization). ## Related Issues Fixes flutter#6469
1 parent f865ac7 commit 324fdb6

File tree

4 files changed

+177
-8
lines changed

4 files changed

+177
-8
lines changed

dev/integration_tests/flutter_gallery/lib/gallery/backdrop.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,14 @@ class _BackdropState extends State<Backdrop> with SingleTickerProviderStateMixin
215215
value: 1.0,
216216
vsync: this,
217217
);
218+
_controller.addStatusListener((AnimationStatus status) {
219+
setState(() {
220+
// This is intentionally left empty. The state change itself takes
221+
// place inside the AnimationController, so there's nothing to update.
222+
// All we want is for the widget to rebuild and read the new animation
223+
// state from the AnimationController.
224+
});
225+
});
218226
_frontOpacity = _controller.drive(_frontOpacityTween);
219227
}
220228

packages/flutter/lib/src/widgets/layout_builder.dart

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ typedef LayoutWidgetBuilder = Widget Function(BuildContext context, BoxConstrain
1717
/// function at layout time and provides the constraints that this widget should
1818
/// adhere to. This is useful when the parent constrains the child's size and layout,
1919
/// and doesn't depend on the child's intrinsic size.
20+
///
21+
/// {@template flutter.widgets.layoutBuilder.builderFunctionInvocation}
22+
/// The [builder] function is called in the following situations:
23+
///
24+
/// * The first time the widget is laid out.
25+
/// * When the parent widget passes different layout constraints.
26+
/// * When the parent widget updates this widget.
27+
/// * When the depedencies that the [builder] function subscribes to change.
28+
///
29+
/// The [builder] function is _not_ called during layout if the parent passes
30+
/// the same constraints repeatedly.
31+
/// {@endtemplate}
2032
abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints> extends RenderObjectWidget {
2133
/// Creates a widget that defers its building until layout.
2234
///
@@ -74,15 +86,22 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
7486
assert(widget != newWidget);
7587
super.update(newWidget);
7688
assert(widget == newWidget);
89+
7790
renderObject.updateCallback(_layout);
78-
renderObject.markNeedsLayout();
91+
// Force the callback to be called, even if the layout constraints are the
92+
// same, because the logic in the callback might have changed.
93+
renderObject.markNeedsBuild();
7994
}
8095

8196
@override
8297
void performRebuild() {
8398
// This gets called if markNeedsBuild() is called on us.
8499
// That might happen if, e.g., our builder uses Inherited widgets.
85-
renderObject.markNeedsLayout();
100+
101+
// Force the callback to be called, even if the layout constraints are the
102+
// same. This is because that callback may depend on the updated widget
103+
// configuration, or an inherited widget.
104+
renderObject.markNeedsBuild();
86105
super.performRebuild(); // Calls widget.updateRenderObject (a no-op in this case).
87106
}
88107

@@ -168,10 +187,42 @@ mixin RenderConstrainedLayoutBuilder<ConstraintType extends Constraints, ChildTy
168187
markNeedsLayout();
169188
}
170189

171-
/// Invoke the layout callback.
172-
void layoutAndBuildChild() {
190+
bool _needsBuild = true;
191+
192+
/// Marks this layout builder as needing to rebuild.
193+
///
194+
/// The layout build rebuilds automatically when layout constraints change.
195+
/// However, we must also rebuild when the widget updates, e.g. after
196+
/// [State.setState], or [State.didChangeDependencies], even when the layout
197+
/// constraints remain unchanged.
198+
///
199+
/// See also:
200+
///
201+
/// * [ConstrainedLayoutBuilder.builder], which is called during the rebuild.
202+
void markNeedsBuild() {
203+
// Do not call the callback directly. It must be called during the layout
204+
// phase, when parent constraints are available. Calling `markNeedsLayout`
205+
// will cause it to be called at the right time.
206+
_needsBuild = true;
207+
markNeedsLayout();
208+
}
209+
210+
// The constraints that were passed to this class last time it was laid out.
211+
// These constraints are compared to the new constraints to determine whether
212+
// [ConstrainedLayoutBuilder.builder] needs to be called.
213+
Constraints _previousConstraints;
214+
215+
/// Invoke the callback supplied via [updateCallback].
216+
///
217+
/// Typically this results in [ConstrainedLayoutBuilder.builder] being called
218+
/// during layout.
219+
void rebuildIfNecessary() {
173220
assert(_callback != null);
174-
invokeLayoutCallback(_callback);
221+
if (_needsBuild || constraints != _previousConstraints) {
222+
_previousConstraints = constraints;
223+
_needsBuild = false;
224+
invokeLayoutCallback(_callback);
225+
}
175226
}
176227
}
177228

@@ -183,11 +234,13 @@ mixin RenderConstrainedLayoutBuilder<ConstraintType extends Constraints, ChildTy
183234
/// the child's intrinsic size. The [LayoutBuilder]'s final size will match its
184235
/// child's size.
185236
///
237+
/// {@macro flutter.widgets.layoutBuilder.builderFunctionInvocation}
238+
///
186239
/// {@youtube 560 315 https://www.youtube.com/watch?v=IYDVcriKjsw}
187240
///
188241
/// If the child should be smaller than the parent, consider wrapping the child
189242
/// in an [Align] widget. If the child might want to be bigger, consider
190-
/// wrapping it in a [SingleChildScrollView].
243+
/// wrapping it in a [SingleChildScrollView] or [OverflowBox].
191244
///
192245
/// See also:
193246
///
@@ -241,7 +294,7 @@ class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin<Ren
241294
@override
242295
void performLayout() {
243296
final BoxConstraints constraints = this.constraints;
244-
layoutAndBuildChild();
297+
rebuildIfNecessary();
245298
if (child != null) {
246299
child.layout(constraints, parentUsesSize: true);
247300
size = constraints.constrain(child.size);

packages/flutter/lib/src/widgets/sliver_layout_builder.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ typedef SliverLayoutWidgetBuilder = Widget Function(BuildContext context, Sliver
1919
/// The [SliverLayoutBuilder]'s final [SliverGeometry] will match the [SliverGeometry]
2020
/// of its child.
2121
///
22+
/// {@macro flutter.widgets.layoutBuilder.builderFunctionInvocation}
2223
///
2324
/// See also:
2425
///
@@ -52,7 +53,7 @@ class _RenderSliverLayoutBuilder extends RenderSliver with RenderObjectWithChild
5253

5354
@override
5455
void performLayout() {
55-
layoutAndBuildChild();
56+
rebuildIfNecessary();
5657
child?.layout(constraints, parentUsesSize: true);
5758
geometry = child?.geometry ?? SliverGeometry.zero;
5859
}

packages/flutter/test/widgets/layout_builder_test.dart

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,4 +587,111 @@ void main() {
587587
await tester.pump();
588588
expect(hitCounts, const <int> [0, 0, 0]);
589589
});
590+
591+
testWidgets('LayoutBuilder does not call builder when layout happens but layout constraints do not change', (WidgetTester tester) async {
592+
int builderInvocationCount = 0;
593+
594+
Future<void> pumpTestWidget(Size size) async {
595+
await tester.pumpWidget(
596+
// Center is used to give the SizedBox the power to determine constraints for LayoutBuilder
597+
Center(
598+
child: SizedBox.fromSize(
599+
size: size,
600+
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
601+
builderInvocationCount += 1;
602+
return _LayoutSpy();
603+
}),
604+
),
605+
),
606+
);
607+
}
608+
609+
await pumpTestWidget(const Size(10, 10));
610+
611+
final _RenderLayoutSpy spy = tester.renderObject(find.byType(_LayoutSpy));
612+
613+
// The child is laid out once the first time.
614+
expect(spy.performLayoutCount, 1);
615+
expect(spy.performResizeCount, 1);
616+
617+
// The initial `pumpWidget` will trigger `performRebuild`, asking for
618+
// builder invocation.
619+
expect(builderInvocationCount, 1);
620+
621+
// Invalidate the layout without chaning the constraints.
622+
tester.renderObject(find.byType(LayoutBuilder)).markNeedsLayout();
623+
624+
// The second pump will not go through the `performRebuild` or `update`, and
625+
// only judge the need for builder invocation based on constraints, which
626+
// didn't change, so we don't expect any counters to go up.
627+
await tester.pump();
628+
expect(builderInvocationCount, 1);
629+
expect(spy.performLayoutCount, 1);
630+
expect(spy.performResizeCount, 1);
631+
632+
// Cause the `update` to be called (but not `performRebuild`), triggering
633+
// builder invocation.
634+
await pumpTestWidget(const Size(10, 10));
635+
expect(builderInvocationCount, 2);
636+
637+
// The spy does not invalidate its layout on widget update, so no
638+
// layout-related methods should be called.
639+
expect(spy.performLayoutCount, 1);
640+
expect(spy.performResizeCount, 1);
641+
642+
// Have the child request layout and verify that the child gets laid out
643+
// despite layout constraints remaining constant.
644+
spy.markNeedsLayout();
645+
await tester.pump();
646+
647+
// Builder is not invoked. This was a layout-only pump with the same parent
648+
// constraints.
649+
expect(builderInvocationCount, 2);
650+
651+
// Expect performLayout to be called.
652+
expect(spy.performLayoutCount, 2);
653+
654+
// performResize should not be called because the spy sets sizedByParent,
655+
// and the constraints did not change.
656+
expect(spy.performResizeCount, 1);
657+
658+
// Change the parent size, triggering constraint change.
659+
await pumpTestWidget(const Size(20, 20));
660+
661+
// We should see everything invoked once.
662+
expect(builderInvocationCount, 3);
663+
expect(spy.performLayoutCount, 3);
664+
expect(spy.performResizeCount, 2);
665+
});
666+
}
667+
668+
class _LayoutSpy extends LeafRenderObjectWidget {
669+
@override
670+
LeafRenderObjectElement createElement() => _LayoutSpyElement(this);
671+
672+
@override
673+
RenderObject createRenderObject(BuildContext context) => _RenderLayoutSpy();
674+
}
675+
676+
class _LayoutSpyElement extends LeafRenderObjectElement {
677+
_LayoutSpyElement(LeafRenderObjectWidget widget) : super(widget);
678+
}
679+
680+
class _RenderLayoutSpy extends RenderBox {
681+
int performLayoutCount = 0;
682+
int performResizeCount = 0;
683+
684+
@override
685+
bool get sizedByParent => true;
686+
687+
@override
688+
void performResize() {
689+
performResizeCount += 1;
690+
size = constraints.biggest;
691+
}
692+
693+
@override
694+
void performLayout() {
695+
performLayoutCount += 1;
696+
}
590697
}

0 commit comments

Comments
 (0)