diff --git a/packages/flutter/lib/src/widgets/layout_builder.dart b/packages/flutter/lib/src/widgets/layout_builder.dart index d7e489bc05..df503e720c 100644 --- a/packages/flutter/lib/src/widgets/layout_builder.dart +++ b/packages/flutter/lib/src/widgets/layout_builder.dart @@ -47,6 +47,28 @@ abstract class ConstrainedLayoutBuilder exte /// The builder must not return null. final Widget Function(BuildContext context, ConstraintType constraints) builder; + /// Whether [builder] needs to be called again even if the layout constraints + /// are the same. + /// + /// When this widget's configuration is updated, the [builder] callback most + /// likely needs to be called to build this widget's child. However, + /// subclasses may provide ways in which the widget can be updated without + /// needing to rebuild the child. Such subclasses can use this method to tell + /// the framework when the child widget should be rebuilt. + /// + /// When this method is called by the framework, the newly configured widget + /// is asked if it requires a rebuild, and it is passed the old widget as a + /// parameter. + /// + /// See also: + /// + /// * [State.setState] and [State.didUpdateWidget], which talk about widget + /// configuration changes and how they're triggered. + /// * [Element.update], the method that actually updates the widget's + /// configuration. + @protected + bool updateShouldRebuild(covariant ConstrainedLayoutBuilder oldWidget) => true; + // updateRenderObject is redundant with the logic in the LayoutBuilderElement below. } @@ -81,13 +103,14 @@ class _LayoutBuilderElement extends RenderOb @override void update(ConstrainedLayoutBuilder newWidget) { assert(widget != newWidget); + final ConstrainedLayoutBuilder oldWidget = widget as ConstrainedLayoutBuilder; super.update(newWidget); assert(widget == newWidget); renderObject.updateCallback(_layout); - // 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(); + if (newWidget.updateShouldRebuild(oldWidget)) { + renderObject.markNeedsBuild(); + } } @override diff --git a/packages/flutter/test/widgets/layout_builder_test.dart b/packages/flutter/test/widgets/layout_builder_test.dart index 18542fcd1a..8728162292 100644 --- a/packages/flutter/test/widgets/layout_builder_test.dart +++ b/packages/flutter/test/widgets/layout_builder_test.dart @@ -699,6 +699,139 @@ void main() { await pumpTestWidget(const Size(10.0, 10.0)); expect(childSize, const Size(10.0, 10.0)); }); + + testWidgetsWithLeakTracking('LayoutBuilder will only invoke builder if updateShouldRebuild returns true', (WidgetTester tester) async { + int buildCount = 0; + int paintCount = 0; + Offset? mostRecentOffset; + void handleChildWasPainted(Offset extraOffset) { + paintCount++; + mostRecentOffset = extraOffset; + } + + Future pumpWidget(String text, double offsetPercentage) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SizedBox( + width: 100, + height: 100, + child: _SmartLayoutBuilder( + text: text, + offsetPercentage: offsetPercentage, + onChildWasPainted: handleChildWasPainted, + builder: (BuildContext context, BoxConstraints constraints) { + buildCount++; + return Text(text); + }, + ), + ), + ), + ), + ); + } + + await pumpWidget('aaa', 0.2); + expect(find.text('aaa'), findsOneWidget); + expect(buildCount, 1); + expect(paintCount, 1); + expect(mostRecentOffset, const Offset(20, 20)); + await pumpWidget('aaa', 0.4); + expect(find.text('aaa'), findsOneWidget); + expect(buildCount, 1); + expect(paintCount, 2); + expect(mostRecentOffset, const Offset(40, 40)); + await pumpWidget('bbb', 0.6); + expect(find.text('aaa'), findsNothing); + expect(find.text('bbb'), findsOneWidget); + expect(buildCount, 2); + expect(paintCount, 3); + expect(mostRecentOffset, const Offset(60, 60)); + }); +} + +class _SmartLayoutBuilder extends ConstrainedLayoutBuilder { + const _SmartLayoutBuilder({ + required this.text, + required this.offsetPercentage, + required this.onChildWasPainted, + required super.builder, + }); + + final String text; + final double offsetPercentage; + final _OnChildWasPaintedCallback onChildWasPainted; + + @override + bool updateShouldRebuild(_SmartLayoutBuilder oldWidget) { + // Because this is a private widget and thus local to this file, we know + // that only the [text] property affects the builder; the other properties + // only affect painting. + return text != oldWidget.text; + } + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderSmartLayoutBuilder( + offsetPercentage: offsetPercentage, + onChildWasPainted: onChildWasPainted, + ); + } + + @override + void updateRenderObject(BuildContext context, _RenderSmartLayoutBuilder renderObject) { + renderObject + ..offsetPercentage = offsetPercentage + ..onChildWasPainted = onChildWasPainted; + } +} + +typedef _OnChildWasPaintedCallback = void Function(Offset extraOffset); + +class _RenderSmartLayoutBuilder extends RenderProxyBox + with RenderConstrainedLayoutBuilder { + _RenderSmartLayoutBuilder({ + required double offsetPercentage, + required this.onChildWasPainted, + }) : _offsetPercentage = offsetPercentage; + + double _offsetPercentage; + double get offsetPercentage => _offsetPercentage; + set offsetPercentage(double value) { + if (value != _offsetPercentage) { + _offsetPercentage = value; + markNeedsPaint(); + } + } + + _OnChildWasPaintedCallback onChildWasPainted; + + @override + bool get sizedByParent => true; + + @override + Size computeDryLayout(BoxConstraints constraints) { + return constraints.biggest; + } + + @override + void performLayout() { + rebuildIfNecessary(); + child?.layout(constraints); + } + + @override + void paint(PaintingContext context, Offset offset) { + if (child != null) { + final Offset extraOffset = Offset( + size.width * offsetPercentage, + size.height * offsetPercentage, + ); + context.paintChild(child!, offset + extraOffset); + onChildWasPainted(extraOffset); + } + } } class _LayoutSpy extends LeafRenderObjectWidget {