Skip to content

Commit 884e011

Browse files
Make sure LayoutBuilder rebuild in an inactive route (#154681)
Prompted by flutter/flutter#154060: widgets should always rebuild even when off-screen. The ancestor widget could be trying to pass down information that is not related to the UI state, or trying to pause video playback. Widgets with global keys should also always rebuild to make sure the widget tree is consistent in terms of global keys. ~Also prevents unnecessary repaints: flutter/flutter#106306 (comment) This works by adding `_RenderLayoutBuilder` as a relayout boundary in the dirtly layout list so the layout callback always gets a chance to run if marked dirty. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent 5e18e59 commit 884e011

File tree

6 files changed

+243
-35
lines changed

6 files changed

+243
-35
lines changed

packages/flutter/lib/src/rendering/object.dart

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2608,7 +2608,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
26082608
@pragma('vm:notify-debugger-on-exception')
26092609
void _layoutWithoutResize() {
26102610
assert(_needsLayout);
2611-
assert(_relayoutBoundary == this);
2611+
assert(_relayoutBoundary == this || this is RenderObjectWithLayoutCallbackMixin);
26122612
RenderObject? debugPreviousActiveLayout;
26132613
assert(!_debugMutationsLocked);
26142614
assert(!_doingThisLayoutWithCallback);
@@ -4132,6 +4132,77 @@ mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject
41324132
}
41334133
}
41344134

4135+
/// A mixin for managing [RenderObject] with a [layoutCallback], which will be
4136+
/// invoked during this [RenderObject]'s layout process if scheduled using
4137+
/// [scheduleLayoutCallback].
4138+
///
4139+
/// A layout callback is typically a callback that mutates the [RenderObject]'s
4140+
/// render subtree during the [RenderObject]'s layout process. When an ancestor
4141+
/// [RenderObject] chooses to skip laying out this [RenderObject] in its
4142+
/// [performLayout] implementation (for example, for performance reasons, an
4143+
/// [Overlay] may skip laying out an offstage [OverlayEntry] while keeping it in
4144+
/// the tree), normally the [layoutCallback] will not be invoked because the
4145+
/// [layout] method will not be called. This can be undesirable when the
4146+
/// [layoutCallback] involves rebuilding dirty widgets (most notably, the
4147+
/// [LayoutBuilder] widget). Unlike render subtrees, typically all dirty widgets
4148+
/// (even off-screen ones) in a widget tree must be rebuilt. This mixin makes
4149+
/// sure once scheduled, the [layoutCallback] method will be invoked even if it's
4150+
/// skipped by an ancestor [RenderObject], unless this [RenderObject] has never
4151+
/// been laid out.
4152+
///
4153+
/// Subclasses must not invoke the layout callback directly. Instead, call
4154+
/// [runLayoutCallback] in the [performLayout] implementation.
4155+
///
4156+
/// See also:
4157+
///
4158+
/// * [LayoutBuilder] and [SliverLayoutBuilder], which use the mixin.
4159+
mixin RenderObjectWithLayoutCallbackMixin on RenderObject {
4160+
// The initial value of this flag must be set to true to prevent the layout
4161+
// callback from being scheduled when the subtree has never been laid out (in
4162+
// which case the `constraints` or any other layout information is unknown).
4163+
bool _needsRebuild = true;
4164+
4165+
/// The layout callback to be invoked during [performLayout].
4166+
///
4167+
/// This method should not be invoked directly. Instead, call
4168+
/// [runLayoutCallback] in the [performLayout] implementation. This callback
4169+
/// will be invoked using [invokeLayoutCallback].
4170+
@visibleForOverriding
4171+
void layoutCallback();
4172+
4173+
/// Invokes [layoutCallback] with [invokeLayoutCallback].
4174+
///
4175+
/// This method must be called in [performLayout], typically as early as
4176+
/// possible before any layout work is done, to avoid re-dirtying any child
4177+
/// [RenderObject]s.
4178+
@mustCallSuper
4179+
void runLayoutCallback() {
4180+
assert(debugDoingThisLayout);
4181+
invokeLayoutCallback((_) => layoutCallback());
4182+
_needsRebuild = false;
4183+
}
4184+
4185+
/// Informs the framework that the layout callback has been updated and must be
4186+
/// invoked again when this [RenderObject] is ready for layout, even when an
4187+
/// ancestor [RenderObject] chooses to skip laying out this render subtree.
4188+
@mustCallSuper
4189+
void scheduleLayoutCallback() {
4190+
if (_needsRebuild) {
4191+
assert(debugNeedsLayout);
4192+
return;
4193+
}
4194+
_needsRebuild = true;
4195+
// This ensures that the layout callback will be run even if an ancestor
4196+
// chooses to not lay out this subtree (for example, obstructed OverlayEntries
4197+
// with `maintainState` set to true), to maintain the widget tree integrity
4198+
// (making sure global keys are unique, for example).
4199+
owner?._nodesNeedingLayout.add(this);
4200+
// In an active tree, markNeedsLayout is needed to inform the layout boundary
4201+
// that its child size may change.
4202+
super.markNeedsLayout();
4203+
}
4204+
}
4205+
41354206
/// Parent data to support a doubly-linked list of children.
41364207
///
41374208
/// The children can be traversed using [nextSibling] or [previousSibling],

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

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ abstract class AbstractLayoutBuilder<LayoutInfoType> extends RenderObjectWidget
9292
///
9393
/// The [builder] function is _not_ called during layout if the parent passes
9494
/// the same constraints repeatedly.
95+
///
96+
/// In the event that an ancestor skips the layout of this subtree so the
97+
/// constraints become outdated, the `builder` rebuilds with the last known
98+
/// constraints.
9599
/// {@endtemplate}
96100
abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints>
97101
extends AbstractLayoutBuilder<ConstraintType> {
@@ -133,7 +137,7 @@ class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement {
133137
SchedulerPhase.persistentCallbacks => false,
134138
};
135139
if (!deferMarkNeedsLayout) {
136-
renderObject.markNeedsLayout();
140+
renderObject.scheduleLayoutCallback();
137141
return;
138142
}
139143
_deferredCallbackScheduled = true;
@@ -145,7 +149,7 @@ class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement {
145149
// This method is only called when the render tree is stable, if the Element
146150
// is deactivated it will never be reincorporated back to the tree.
147151
if (mounted) {
148-
renderObject.markNeedsLayout();
152+
renderObject.scheduleLayoutCallback();
149153
}
150154
}
151155

@@ -180,7 +184,7 @@ class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement {
180184
renderObject._updateCallback(_rebuildWithConstraints);
181185
if (newWidget.updateShouldRebuild(oldWidget)) {
182186
_needsBuild = true;
183-
renderObject.markNeedsLayout();
187+
renderObject.scheduleLayoutCallback();
184188
}
185189
}
186190

@@ -190,7 +194,7 @@ class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement {
190194
// to performRebuild since this call already does what performRebuild does,
191195
// So the element is clean as soon as this method returns and does not have
192196
// to be added to the dirty list or marked as dirty.
193-
renderObject.markNeedsLayout();
197+
renderObject.scheduleLayoutCallback();
194198
_needsBuild = true;
195199
}
196200

@@ -202,14 +206,14 @@ class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement {
202206
// Force the callback to be called, even if the layout constraints are the
203207
// same. This is because that callback may depend on the updated widget
204208
// configuration, or an inherited widget.
205-
renderObject.markNeedsLayout();
209+
renderObject.scheduleLayoutCallback();
206210
_needsBuild = true;
207211
super.performRebuild(); // Calls widget.updateRenderObject (a no-op in this case).
208212
}
209213

210214
@override
211215
void unmount() {
212-
renderObject._updateCallback(null);
216+
renderObject._callback = null;
213217
super.unmount();
214218
}
215219

@@ -295,39 +299,36 @@ class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement {
295299
/// Generic mixin for [RenderObject]s created by an [AbstractLayoutBuilder] with
296300
/// the the same `LayoutInfoType`.
297301
///
298-
/// Provides a [rebuildIfNecessary] method that should be called at layout time,
299-
/// typically in [RenderObject.performLayout]. The method invokes
300-
/// [AbstractLayoutBuilder]'s builder callback if needed.
302+
/// Provides a [layoutCallback] implementation which, if needed, invokes
303+
/// [AbstractLayoutBuilder]'s builder callback.
301304
///
302305
/// Implementers must provide a [layoutInfo] implementation that is safe to
303-
/// access in [rebuildIfNecessary], which is typically called in [performLayout].
306+
/// access in [layoutCallback], which is called in [performLayout].
304307
mixin RenderAbstractLayoutBuilderMixin<LayoutInfoType, ChildType extends RenderObject>
305-
on RenderObjectWithChildMixin<ChildType> {
308+
on RenderObjectWithChildMixin<ChildType>, RenderObjectWithLayoutCallbackMixin {
306309
LayoutCallback<Constraints>? _callback;
307310

308311
/// Change the layout callback.
309-
void _updateCallback(LayoutCallback<Constraints>? value) {
312+
void _updateCallback(LayoutCallback<Constraints> value) {
310313
if (value == _callback) {
311314
return;
312315
}
313316
_callback = value;
314-
markNeedsLayout();
317+
scheduleLayoutCallback();
315318
}
316319

317-
/// Invoke the builder callback supplied via [AbstractLayoutBuilder] and
320+
/// Invokes the builder callback supplied via [AbstractLayoutBuilder] and
318321
/// rebuilds the [AbstractLayoutBuilder]'s widget tree, if needed.
319322
///
320-
/// No work will be done if [layoutInfo] has not changed since the last time
321-
/// this method was called, and [AbstractLayoutBuilder.updateShouldRebuild]
323+
/// No further work will be done if [layoutInfo] has not changed since the last
324+
/// time this method was called, and [AbstractLayoutBuilder.updateShouldRebuild]
322325
/// returned `false` when the widget was rebuilt.
323326
///
324327
/// This method should typically be called as soon as possible in the class's
325328
/// [performLayout] implementation, before any layout work is done.
326-
@protected
327-
void rebuildIfNecessary() {
328-
assert(_callback != null);
329-
invokeLayoutCallback(_callback!);
330-
}
329+
@visibleForOverriding
330+
@override
331+
void layoutCallback() => _callback!(constraints);
331332

332333
/// The information to invoke the [AbstractLayoutBuilder.builder] callback with.
333334
///
@@ -381,6 +382,7 @@ class LayoutBuilder extends ConstrainedLayoutBuilder<BoxConstraints> {
381382
class _RenderLayoutBuilder extends RenderBox
382383
with
383384
RenderObjectWithChildMixin<RenderBox>,
385+
RenderObjectWithLayoutCallbackMixin,
384386
RenderAbstractLayoutBuilderMixin<BoxConstraints, RenderBox> {
385387
@override
386388
double computeMinIntrinsicWidth(double height) {
@@ -433,7 +435,7 @@ class _RenderLayoutBuilder extends RenderBox
433435
@override
434436
void performLayout() {
435437
final BoxConstraints constraints = this.constraints;
436-
rebuildIfNecessary();
438+
runLayoutCallback();
437439
if (child != null) {
438440
child!.layout(constraints, parentUsesSize: true);
439441
size = constraints.constrain(child!.size);

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2632,7 +2632,10 @@ class _OverlayChildLayoutBuilder extends AbstractLayoutBuilder<OverlayChildLayou
26322632
// Additionally, like RenderDeferredLayoutBox, this RenderBox also uses the Stack
26332633
// layout algorithm so developers can use the Positioned widget.
26342634
class _RenderLayoutBuilder extends RenderProxyBox
2635-
with _RenderTheaterMixin, RenderAbstractLayoutBuilderMixin<OverlayChildLayoutInfo, RenderBox> {
2635+
with
2636+
_RenderTheaterMixin,
2637+
RenderObjectWithLayoutCallbackMixin,
2638+
RenderAbstractLayoutBuilderMixin<OverlayChildLayoutInfo, RenderBox> {
26362639
@override
26372640
Iterable<RenderBox> _childrenInPaintOrder() {
26382641
final RenderBox? child = this.child;
@@ -2709,19 +2712,25 @@ class _RenderLayoutBuilder extends RenderProxyBox
27092712
return OverlayChildLayoutInfo._((overlayPortalSize, paintTransform, size));
27102713
}
27112714

2715+
@override
2716+
@visibleForOverriding
2717+
void layoutCallback() {
2718+
_layoutInfo = _computeNewLayoutInfo();
2719+
super.layoutCallback();
2720+
}
2721+
27122722
int? _callbackId;
27132723
@override
27142724
void performLayout() {
2715-
// The invokeLayoutCallback allows arbitrary access to the sizes of
2716-
// RenderBoxes the we know that have finished doing layout.
2717-
invokeLayoutCallback((_) => _layoutInfo = _computeNewLayoutInfo());
2718-
rebuildIfNecessary();
2725+
runLayoutCallback();
2726+
if (child case final RenderBox child?) {
2727+
layoutChild(child, constraints);
2728+
}
27192729
assert(_callbackId == null);
27202730
_callbackId ??= SchedulerBinding.instance.scheduleFrameCallback(
27212731
_frameCallback,
27222732
scheduleNewFrame: false,
27232733
);
2724-
layoutChild(child!, constraints);
27252734
}
27262735

27272736
// This RenderObject is a child of _RenderDeferredLayouts which in turn is a

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class SliverLayoutBuilder extends ConstrainedLayoutBuilder<SliverConstraints> {
3737
class _RenderSliverLayoutBuilder extends RenderSliver
3838
with
3939
RenderObjectWithChildMixin<RenderSliver>,
40+
RenderObjectWithLayoutCallbackMixin,
4041
RenderAbstractLayoutBuilderMixin<SliverConstraints, RenderSliver> {
4142
@override
4243
double childMainAxisPosition(RenderObject child) {
@@ -50,7 +51,7 @@ class _RenderSliverLayoutBuilder extends RenderSliver
5051

5152
@override
5253
void performLayout() {
53-
rebuildIfNecessary();
54+
runLayoutCallback();
5455
child?.layout(constraints, parentUsesSize: true);
5556
geometry = child?.geometry ?? SliverGeometry.zero;
5657
}

0 commit comments

Comments
 (0)