Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make sure LayoutBuilder rebuild in an inactive route #154681

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

LongCatIsLooong
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong commented Sep 5, 2024

Prompted by #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: #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

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: scrolling Viewports, list views, slivers, etc. labels Sep 5, 2024
@LongCatIsLooong LongCatIsLooong force-pushed the layout-builder-always-rebuild branch from f264ff6 to 1adbbda Compare September 5, 2024 23:02
@LongCatIsLooong

This comment was marked as off-topic.

@LongCatIsLooong LongCatIsLooong marked this pull request as ready for review September 6, 2024 00:20
// This ensures that the layout callback will be run even if an ancestor
// chooses to not lay out this subtree (for example, OverlayEntries with
// `maintainState` set to true).
owner?._nodesNeedingLayout.add(this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my own understanding: Wouldn't this case child nodes to potentially also get layed out unnecessary?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or is the idea that since the contrains haven't changed, the layout builder is pretty much guaranteed to pass the same constraints to its child and layout would short-circuit right there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the render children may also do layout now which can be unnecessary. But I think (at least for RenderTheater) if you rebuild a widget subtree in an inactive maintainState OverlayEntry, relayout boundaries in the render subtree are still operational and call performLayout as usual?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the reason that we don't want to find the closest boundary because we are not sure whether between that and here may skips the layout?

Is it possible to short circuit the layout pass and directly marksNeedsBuild of the subtree if this is in active route? I am a little concern about removing the check that layout can start at non layout boundary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to short circuit the layout pass and directly marksNeedsBuild of the subtree if this is in active route? I am a little concern about removing the check that layout can start at non layout boundary.

I am not aware of any other ways to detect whether a LayoutBuilders layout has been skipped, other than adding its node to the dirtly layout list.

If the parent skips layout, then it doesn't care about the size of this render object so this render object is effectively a relayout boundary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I can't think of a better way to do this. especially we only want to rebuild during layout phase .

Comment on lines 4211 to 4212
// The initial value of this flag must be set to true to prevent the layout
// callback from being called when the tree has never been laid out.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this comment: _needsRebuild doesn't seem to be used as a guard for runLayoutCallback. And also, why do we not want to run the callback during the first time the tree is laid out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This RenderObject will be added to the PipelineOwner's dirty list, but if its ancestors were never laid out we can't run the layout callback because the constraints are not known yet. Changed the class documentation to

/// A mixin for [RenderObject] subclasses with a layout callback. The mixin
/// guarantees the layout callback will be called even if this [RenderObject]
/// skips doing layout, unless the [RenderObject] has never been laid out and
/// does not have valid [constraints].

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new explanation sounds good to me, but you may have forgotten to push the change up to github.

@@ -850,6 +850,52 @@ void main() {
expect(paintCount, 3);
expect(mostRecentOffset, const Offset(60, 60));
});

testWidgets('LayoutBuilder in a subtree that skips layout does not rebuild during the initial treewalk', (WidgetTester tester) async {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why this is?

auto-submit bot pushed a commit that referenced this pull request Sep 13, 2024
Fixes #154060

The error message doesn't make sense to me since one can call `setState` during the idle phase, and I'm not sure what this is guarding against without the right error message.

In the case of #154060 the layout builder was never laid out:
```
��child 1: _RenderLayoutBuilder#7c319 NEEDS-LAYOUT NEEDS-PAINT
�   creator: LayoutBuilder � _BodyBuilder � MediaQuery �
�     LayoutId-[<_ScaffoldSlot.body>] � CustomMultiChildLayout �
�     _ActionsScope � Actions � AnimatedBuilder � DefaultTextStyle �
�     AnimatedDefaultTextStyle � _InkFeatures-[GlobalKey#1f6eb ink
�     renderer] � NotificationListener<LayoutChangedNotification> � �
�   parentData: offset=Offset(0.0, 0.0); id=_ScaffoldSlot.body
�   constraints: MISSING
�   size: MISSING
```
So #154681 doesn't really fix #154060 since the layout callback cannot be run without a set of valid constraints. 

Before the `BuildScope` change all `_inDirtyList` flags were unset after the `BuildOwner` finishes rebuilding the widget tree, so `LayoutBuilder._inDirtyLst` is always set to false after a frame even for layout builders that were never laid out.
With the `BuildScope` change, `LayoutBuilder` has its own `BuildScope` which is only flushed after LayoutBuilder gets its constraints.
@Piinks
Copy link
Contributor

Piinks commented Sep 24, 2024

(PR Triage): FYI there are some merge conflicts here. :)

@chunhtai chunhtai self-requested a review October 1, 2024 22:14
Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@@ -2446,7 +2446,6 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
@pragma('vm:notify-debugger-on-exception')
void _layoutWithoutResize() {
assert(_needsLayout);
assert(_relayoutBoundary == this);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of remove this, can it add a || this is RenderObjectWithLayoutCallbackMixin

// This ensures that the layout callback will be run even if an ancestor
// chooses to not lay out this subtree (for example, OverlayEntries with
// `maintainState` set to true).
owner?._nodesNeedingLayout.add(this);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I can't think of a better way to do this. especially we only want to rebuild during layout phase .

@Piinks
Copy link
Contributor

Piinks commented Oct 15, 2024

(Flyby comment from triage) This has some merge conflicts now.

@chunhtai
Copy link
Contributor

@LongCatIsLooong said they will come back to this pr once they finished Full keyboard access pr

@LongCatIsLooong LongCatIsLooong force-pushed the layout-builder-always-rebuild branch from b3f906c to 5786841 Compare December 12, 2024 23:46
@LongCatIsLooong
Copy link
Contributor Author

(requested another review since I remember I spoke with @goderbauer about the issue).

@flutter-dashboard
Copy link

This pull request executed golden file tests, but it has not been updated in a while (20+ days). Test results from Gold expire after as many days, so this pull request will need to be updated with a fresh commit in order to get results from Gold.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@Piinks
Copy link
Contributor

Piinks commented Mar 11, 2025

(Triage) Hey @LongCatIsLooong, is this change still something you'd like to return to?

@LongCatIsLooong
Copy link
Contributor Author

(Triage) Hey @LongCatIsLooong, is this change still something you'd like to return to?

Yeah I'm planning to get to this PR tomorrow

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM other than the updates and test fixes that this PR needs.

@@ -4108,6 +4107,64 @@ mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject
}
}

/// A mixin for [RenderObject] subclasses with a layout callback. The mixin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Break into a new paragraph after the first sentence here.

Comment on lines +4152 to +4153
/// Informs the framework that the layout callback has been updated and must
/// be invoked again.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Consider explaining the difference between this and markNeedsLayout here. Also maybe add a "see also" for this method in the docs of markNeedsLayout?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants