Skip to content

Add/remove overlay child RenderObject from the tree in attach/detach#186564

Merged
auto-submit[bot] merged 15 commits into
flutter:masterfrom
LongCatIsLooong:overlayportal-active
May 29, 2026
Merged

Add/remove overlay child RenderObject from the tree in attach/detach#186564
auto-submit[bot] merged 15 commits into
flutter:masterfrom
LongCatIsLooong:overlayportal-active

Conversation

@LongCatIsLooong
Copy link
Copy Markdown
Contributor

@LongCatIsLooong LongCatIsLooong commented May 15, 2026

Fixes #180569
Fixes #177693

  • Moves _RenderDeferredLayoutBox from Element.activate/Element.deactivate to RenderObject.attach/RenderObject.detach.
  • Makes _addDeferredChild/_removeDeferredChild reentrant.

Before this OverlayPortalElement.deactivate removes the overlay child render object from Overlay. This is fine but in OverlayPortalElement.activate the OverlayPortal's render object has not reattach yet so the RO mutation check fails because it couldn't find the ancestor actively doing layout for the overlay child render object, when it tries to re-attach.

Pre-launch Checklist

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

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label May 15, 2026
@github-actions github-actions Bot added the framework flutter/packages/flutter repository. See also f: labels. label May 15, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors RenderObject mutation checks and OverlayPortal lifecycle management. It introduces _debugClosestMutationRoot to streamline mutation permission logic and replaces boolean flags with a counter in _RenderTheater to handle deferred child updates more robustly. Additionally, _RenderLayoutSurrogateProxyBox now manages its own attachment lifecycle. Feedback includes a potential regression where a field assignment was replaced by an assertion in _OverlayPortalElement.insertRenderObjectChild, along with minor typo corrections in documentation comments.

Comment on lines +2427 to +2428
assert(renderObject._deferredLayoutChild == child);
slot._addChild(child as _RenderDeferredLayoutBox);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The assignment to renderObject._deferredLayoutChild was removed and replaced with an assert. However, insertRenderObjectChild is the primary point where the parent element receives the child's RenderObject. If this field is not set before this call, the assert will fail. Furthermore, the logic in attach and detach in _RenderLayoutSurrogateProxyBox depends on this field being correctly populated. Unless the assignment was moved to a location not shown in this diff (like a new setter or a different part of the element lifecycle), this appears to be a regression.

      renderObject._deferredLayoutChild = child as _RenderDeferredLayoutBox;
      slot._addChild(child as _RenderDeferredLayoutBox);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Actually the suggested change is more robust if we change the lifecycle of this in the future. But I think doing it during the construction of the render object makes the lifecycle of the variable easier to understand.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added a comment.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe the comment could be a little more explicit for findability? e.g

// _deferredLayoutChild is assigned in _DeferredLayout.createRenderObject.

Comment thread packages/flutter/lib/src/rendering/object.dart Outdated
Comment thread packages/flutter/lib/src/widgets/overlay.dart Outdated
@github-actions github-actions Bot removed the CICD Run CI/CD label May 15, 2026
@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label May 15, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label May 16, 2026
@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label May 16, 2026

bool _debugMutationsLocked = false;

// Returns the closest ancestor that determines whether this RenderObject is
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the same exact logic as before just restructured it a little bit.

@chunhtai
Copy link
Copy Markdown
Contributor

in OverlayPortalElement.activate the OverlayPortal's render object has not reattach yet so the RO mutation check fails

This seems like will be a problem even if this is first build, how does this not crash every time?

@LongCatIsLooong
Copy link
Copy Markdown
Contributor Author

LongCatIsLooong commented May 18, 2026

in OverlayPortalElement.activate the OverlayPortal's render object has not reattach yet so the RO mutation check fails

This seems like will be a problem even if this is first build, how does this not crash every time?

activate is not called on mount, it's only called during global key reparenting (if that's what you meant)?

@github-actions github-actions Bot removed the CICD Run CI/CD label May 18, 2026
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label May 18, 2026
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
child._parent = this;
if (attached) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This breaks abstracting of attached. If we later update the attached getter to take more thing into account, this may break.

I think we should revert this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah you mean someone overrides attached so it becomes something like super.attached && someOtherCondition? That is most likely not legal because the framework assumes that every render object in the tree must be attached to the same owner and every node must be attached when they're in the tree. So basically attached is synonymous with owner != null.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I guess we should mark attached @nonVirtual?

Copy link
Copy Markdown
Contributor Author

@LongCatIsLooong LongCatIsLooong May 18, 2026

Choose a reason for hiding this comment

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

Or do you mean at some point we may change the framework so we can mark the entire tree detached despite the nodes have an owner, or mark the entire tree attached when the nodes don't have an owner?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

both, but mainly for us.

we can mark the entire tree detached despite the nodes have an owner, or mark the entire tree attached when the nodes don't have an owner?

right

Copy link
Copy Markdown
Contributor Author

@LongCatIsLooong LongCatIsLooong May 20, 2026

Choose a reason for hiding this comment

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

Undid the change. I think still we should add @nonVirtual to attached to make sure it's interchangeable with owner != null, since currently it's illegal to change the definition of attached (the framework assumes attached means owner != null or _owner != null and checks _owner instead of attached most of the time).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not safe though.

Part of a reason why semantics compiling is hard to refactor is that they made a bunch of assumption based on mix match different property state, and I have to try my best to create a bunch of getter to make it more manageable.

I can totally imagine we later introduce addition logic to the getter that can mess up the things if the code assume _ower != null

Copy link
Copy Markdown
Contributor Author

@LongCatIsLooong LongCatIsLooong May 22, 2026

Choose a reason for hiding this comment

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

attached literally means wether this RenderObject is attached to an owner. Today it has to be synonymous with owner != null or you'll likely get an assert when calling addChild / dropChild (I think it is an intended invariant that all RenderObjects in the tree must be attached). If someone really wants to override the behavior of attached, they should override owner instead (IOW owner is going to be the single source of truth and attached is a convenience getter).

I don't see how we would want to change attached to account for some other flags, but I think we should consider changing the owner getter instead if we ever want to so attached and owner != null still mean the same thing (or introduce new flags to represent the new possible states) to avoid breaking existing code.

Copy link
Copy Markdown
Contributor

@chunhtai chunhtai May 22, 2026

Choose a reason for hiding this comment

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

The case I imagine will be something like

bool get attached {
  if (inSomeWeirdLimboState) {
     return false;
  }
  return owner != null;
}

It is not the first time we have cases where we have to keep render object dangling in certain lifecycle phase without moving it to another lifecycle phase

I think we should consider changing the owner getter

you meant even through renderobject has _owner, but we still return null base on condition? I know we do that in some places, but it always feels a bit weird to me

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ideally we don't have to touch neither attached or owner if we have to introduce a different lifecycle state (like offscreen or something if that's what you meant). The semantics of these two are already broadly accepted and changing them will probably break existing code. We can introduce a new source of truth (e.g., lifecycleState) and have owner and attached both reflect that. I find it confusing if attached needs to account for something that's not owner since the documentation literally says this means if this RO is attached to an owner (unless there is an example of inSomeWeirdLimboState?).

// Detaches the deferred child if this node is being detached, but only if the theater isn't
// already detached (so the deferred child will be detached by the theater).
if (_deferredLayoutChild case final deferredChild? when deferredChild.theater.attached) {
overlayLocation!._deactivate(deferredChild);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

_deactivate and _active should probably be named to _attach and _detach

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I couldn't come up with better names so I didn't change them. The method calls dropChild so its not regular detach. Added documentation to _deactivate to explain why it is so named.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

hmm _detachFromLayoutParent sounds more descriptive than _detach or _deactivate, WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

went with _detachFromLayoutSurrogate to match the class name.

}());
}

bool _didDeactivate = false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this also need a better name

@chunhtai
Copy link
Copy Markdown
Contributor

chunhtai commented May 18, 2026

activate is not called on mount, it's only called during global key reparenting (if that's what you meant)?

you are right, I forgot this part, so basically global key reparent a opened overlayportal will just crash 100% of the time?

@LongCatIsLooong
Copy link
Copy Markdown
Contributor Author

activate is not called on mount, it's only called during global key reparenting (if that's what you meant)?

you are right, I forgot this part, so basically global key reparent a opened overlayportal will just crash 100% of the time?

Not necessarily. The OverlayPortal needs to be in a LayoutBuilder because the mutation checks are only performed during layout. Also the overlay child needs to be marked as dirty after it is detached from the tree (See the test).

@github-actions github-actions Bot removed the CICD Run CI/CD label May 18, 2026
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label May 18, 2026
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label May 18, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label May 20, 2026
@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label May 20, 2026
Comment thread packages/flutter/lib/src/widgets/overlay.dart Outdated
Comment thread packages/flutter/lib/src/widgets/overlay.dart Outdated
Comment on lines +2427 to +2428
assert(renderObject._deferredLayoutChild == child);
slot._addChild(child as _RenderDeferredLayoutBox);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe the comment could be a little more explicit for findability? e.g

// _deferredLayoutChild is assigned in _DeferredLayout.createRenderObject.

Comment thread packages/flutter/lib/src/widgets/overlay.dart Outdated
@github-actions github-actions Bot removed the CICD Run CI/CD label May 22, 2026
@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label May 22, 2026
Copy link
Copy Markdown
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, just some question about the code comments

/// Undoes _detachFromLayoutSurrogate by adding the given `child` back to the
/// `_theater`.
///
/// This is called when the OverlayPortal is activated.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should the is activated => is attached?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The only case this can happen is when the element reactivates I think (and this method is not always called on attach, unless _didDetachDeferredChild is true meaning the OverlayPortalElement has been deactivated but the Overlay is not).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same for _detachFromLayoutSurrogate: it's only called if OverlayPortalElement is deactivating but the Overlay is still in the tree.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have to admit that I did not consider the case where the Overlay and OverlayPortal are both reparenting (but separately), in which case the detaching/reattaching won't happen. But that should work too because the detach/attach shenanigans are just for maintaining the "every RO in the tree must be attached" invariant and does not have to guarantee the target Overlay is still the overlay we're targeting. When OverlayPortal rebuilds after activation it will move the overlay child to the correct overlay.

/// Removes the given `child` from the `_theater` but keeps it in the child list
/// (unlike `_removeChild`).
///
/// This is typically called when the [OverlayPortal] deactivates. Since every
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same deactivates => right before detached

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think deactivate is more accurate (for reasons stated in the comment above).

@victorsanni
Copy link
Copy Markdown
Contributor

Should #177693 also be added to this PR description so it is tracked here? Not sure if merging this PR will close that issue.

@LongCatIsLooong LongCatIsLooong added the autosubmit Merge PR when tree becomes green via auto submit App label May 28, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label May 28, 2026
@auto-submit
Copy link
Copy Markdown
Contributor

auto-submit Bot commented May 28, 2026

autosubmit label was removed for flutter/flutter/186564, because The base commit of the PR is older than 7 days and can not be merged. Please merge the latest changes from the main into this branch and resubmit the PR.

@github-actions github-actions Bot removed the CICD Run CI/CD label May 28, 2026
@LongCatIsLooong LongCatIsLooong added the autosubmit Merge PR when tree becomes green via auto submit App label May 28, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label May 28, 2026
@auto-submit
Copy link
Copy Markdown
Contributor

auto-submit Bot commented May 28, 2026

auto label is removed for flutter/flutter/186564, Failed to enqueue flutter/flutter/186564 with HTTP 400: Pull request Required status check "Merge Queue Guard" is expected..

@LongCatIsLooong LongCatIsLooong added CICD Run CI/CD autosubmit Merge PR when tree becomes green via auto submit App labels May 28, 2026
@auto-submit auto-submit Bot added this pull request to the merge queue May 29, 2026
Merged via the queue into flutter:master with commit becfd1a May 29, 2026
106 of 107 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label May 29, 2026
@davidhicks980
Copy link
Copy Markdown
Contributor

davidhicks980 commented May 29, 2026

Should #187162 also be added to the PR description?

auto-submit Bot pushed a commit to flutter/packages that referenced this pull request May 29, 2026
flutter/flutter@e70534d...b05a9d7

2026-05-29 engine-flutter-autoroll@skia.org Roll Skia from 47155534833e to d9d6b440c4e7 (1 revision) (flutter/flutter#187301)
2026-05-29 engine-flutter-autoroll@skia.org Roll Skia from f93ed13d77fb to 47155534833e (4 revisions) (flutter/flutter#187291)
2026-05-29 kevmoo@users.noreply.github.com [web_ui] Optimize skwasm text layout and path decoding to eliminate dynamic boxing churn under Wasm (flutter/flutter#186978)
2026-05-29 engine-flutter-autoroll@skia.org Roll Fuchsia Linux SDK from SBpmmPxqx3lAvGojE... to jMR_VXQi07kAk8vbR... (flutter/flutter#187279)
2026-05-29 burak.karahan@mail.ru Remove Material import from sliver tree rendering test (flutter/flutter#187000)
2026-05-29 bdero@google.com [Impeller] Remove the y_coord_scale Y-flip plumbing (flutter/flutter#187224)
2026-05-29 31859944+LongCatIsLooong@users.noreply.github.com Add/remove overlay child RenderObject from the tree in `attach`/`detach` (flutter/flutter#186564)
2026-05-29 engine-flutter-autoroll@skia.org Roll Skia from fcfe5975c945 to f93ed13d77fb (4 revisions) (flutter/flutter#187273)
2026-05-28 evanwall@buffalo.edu Handle complex RSE rendering in the uber SDF pipeline (flutter/flutter#186434)
2026-05-28 engine-flutter-autoroll@skia.org Roll Dart SDK from 082191101fcc to 683322426411 (2 revisions) (flutter/flutter#187270)
2026-05-28 mvincentong@gmail.com Clarify route transition animations (flutter/flutter#186552)
2026-05-28 116356835+AbdeMohlbi@users.noreply.github.com document that the default Key is null and explain proper usage in list diffing (flutter/flutter#185197)
2026-05-28 srawlins@google.com [flutter_tools] Use super parameters in missed spots (flutter/flutter#186197)
2026-05-28 mr_nadeem_iqbal@yahoo.com docs: Document MediaQueryData.alwaysUse24HourFormat on macOS, Windows, Linux, web (#160664) (flutter/flutter#186642)
2026-05-28 engine-flutter-autoroll@skia.org Roll Skia from 5493e4c144cd to fcfe5975c945 (3 revisions) (flutter/flutter#187256)
2026-05-28 30870216+gaaclarke@users.noreply.github.com Shares opengles golden context (flutter/flutter#187243)
2026-05-28 jason-simmons@users.noreply.github.com Update the Curl CIPD package in .ci.yaml to version 8.20.0 (flutter/flutter#187133)
2026-05-28 737941+loic-sharma@users.noreply.github.com Improve SizedBox's docs (flutter/flutter#187208)
2026-05-28 bdero@google.com [Impeller] Support instanced rendering across all backends (flutter/flutter#186653)
2026-05-28 43054281+camsim99@users.noreply.github.com [Android] Reset system UI visibility flags when setting edge-to-edge mode (flutter/flutter#187207)
2026-05-28 engine-flutter-autoroll@skia.org Roll Skia from a38708fb7926 to 5493e4c144cd (7 revisions) (flutter/flutter#187241)
2026-05-28 30870216+gaaclarke@users.noreply.github.com Turned on impeller by default on macos (flutter/flutter#186546)
2026-05-28 mvincentong@gmail.com Clarify lazy scroll extent docs (flutter/flutter#186864)
2026-05-28 mdebbar@google.com [web] Fix WebParagraph locales test (flutter/flutter#186813)
2026-05-28 engine-flutter-autoroll@skia.org Roll Packages from 4b424d7 to 10cbdc5 (3 revisions) (flutter/flutter#187238)
2026-05-28 engine-flutter-autoroll@skia.org Roll Dart SDK from f3db7b7d9801 to 082191101fcc (8 revisions) (flutter/flutter#187235)
2026-05-28 engine-flutter-autoroll@skia.org Roll Skia from 32acea791248 to a38708fb7926 (1 revision) (flutter/flutter#187221)
2026-05-28 bdero@google.com [Flutter GPU] Add r32Float and remove Apple-only XR pixel formats (flutter/flutter#187069)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC boetger@google.com,stuartmorgan@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tooltip in Alertidalog causes exception with OrientationBuilder OverlayPortal crashes on Hot Reload when reparented inside a LayoutBuilder

5 participants