Unverified Commit 37da62a6 authored by Todd Volkert's avatar Todd Volkert Committed by GitHub

Add ConstrainedLayoutBuilder.updateShouldRebuild() (#136691)

This method controls whether the builder needs to be called again again even if the layout constraints are the same.

By default, the builder will always be called when the widget is updated because the logic in the callback might have changed. However, there are cases where subclasses of ConstrainedLayoutBuilder know that certain property updates only affect paint and not build. In these cases, we lack a way of expressing that the builder callback is not needed -- and we end up doing superfluous work.

This PR gives subclasses the ability to know exactly when the callback needs to be called and when it can be skipped.
parent 9e04246c
...@@ -47,6 +47,28 @@ abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints> exte ...@@ -47,6 +47,28 @@ abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints> exte
/// The builder must not return null. /// The builder must not return null.
final Widget Function(BuildContext context, ConstraintType constraints) builder; 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<ConstraintType> oldWidget) => true;
// updateRenderObject is redundant with the logic in the LayoutBuilderElement below. // updateRenderObject is redundant with the logic in the LayoutBuilderElement below.
} }
...@@ -81,13 +103,14 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb ...@@ -81,13 +103,14 @@ class _LayoutBuilderElement<ConstraintType extends Constraints> extends RenderOb
@override @override
void update(ConstrainedLayoutBuilder<ConstraintType> newWidget) { void update(ConstrainedLayoutBuilder<ConstraintType> newWidget) {
assert(widget != newWidget); assert(widget != newWidget);
final ConstrainedLayoutBuilder<ConstraintType> oldWidget = widget as ConstrainedLayoutBuilder<ConstraintType>;
super.update(newWidget); super.update(newWidget);
assert(widget == newWidget); assert(widget == newWidget);
renderObject.updateCallback(_layout); renderObject.updateCallback(_layout);
// Force the callback to be called, even if the layout constraints are the if (newWidget.updateShouldRebuild(oldWidget)) {
// same, because the logic in the callback might have changed. renderObject.markNeedsBuild();
renderObject.markNeedsBuild(); }
} }
@override @override
......
...@@ -699,6 +699,139 @@ void main() { ...@@ -699,6 +699,139 @@ void main() {
await pumpTestWidget(const Size(10.0, 10.0)); await pumpTestWidget(const Size(10.0, 10.0));
expect(childSize, 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<void> 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<BoxConstraints> {
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<BoxConstraints, RenderBox> {
_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 { class _LayoutSpy extends LeafRenderObjectWidget {
......
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