Skip to content

Commit 263bc45

Browse files
gabrimaticvictorsanni
authored andcommitted
Fix owner-mismatch crash for OverlayPortal inside a deferred-mount parent (e.g. Table) (flutter#185793)
## Issue Fixes flutter#174133 (and the duplicate flutter#180337). Placing a widget that hosts an `OverlayPortal` — most visibly `Slider`, but anything that calls `OverlayPortalController.show()` before its `OverlayPortal` is mounted — inside a `Table`/`TableRow` crashes during mount with: ``` 'package:flutter/src/rendering/object.dart': Failed assertion: line 2138 pos 12: 'child.owner == owner': is not true. flutter#2 RenderObject.redepthChild flutter#3 _RenderDeferredLayoutBox.redepthChildren overlay.dart:2576 flutter#4 RenderObject.redepthChild flutter#5 RenderObject.adoptChild flutter#6 _RenderTheater._addDeferredChild overlay.dart:1312 flutter#7 _OverlayEntryLocation._addChild overlay.dart:2185 flutter#8 _OverlayPortalElement.insertRenderObjectChild overlay.dart:2447 ... flutter#24 _TableElement.mount.<anonymous closure>... table.dart:303 ``` This is a regression introduced between Flutter 3.32 and 3.35. ## Root cause `Table` defers calling `adoptChild` on its `RenderObject` children until *every* row has been mounted (`_TableElement.insertRenderObjectChild` is a no-op while `_doingMountOrUpdate`; the children are wired in later by `_updateRenderObjectChildren`). Meanwhile `_OverlayPortalElement.mount` runs `super.mount` (which "attaches" the layout surrogate to its parent — but Table swallows it) and then immediately mounts the overlay child, which causes the deferred-layout box to be adopted by the `_RenderTheater`. The theater is attached, so the deferred box receives an owner — *while the layout surrogate has none yet*. When the theater calls `redepthChild(deferredBox)`, the deferred box's `redepthChildren` unconditionally calls `_layoutSurrogate.redepthChild(this)`, which trips the `child.owner == owner` assertion across that owner boundary. ## Fix `_RenderDeferredLayoutBox.redepthChildren` now skips the cross-redepth when the surrogate is not yet attached, mirroring the existing guard in `_RenderLayoutSurrogateProxyBox.redepthChildren`: ```dart if (_layoutSurrogate.attached) { _layoutSurrogate.redepthChild(this); } ``` The depth invariant is restored when the surrogate is finally adopted by its parent: `_RenderLayoutSurrogateProxyBox.redepthChildren` already calls `redepthChild(_deferredLayoutChild)` once that child becomes attached. ## Tests Adds a regression test in `test/widgets/table_test.dart` placing an `OverlayPortal` whose controller is `.show()`'d before mount (mirroring `Slider`) inside a `TableRow`, asserting that no exception is thrown and the overlay child is present. Without this fix the new test reproduces the assertion. With this fix `flutter test test/widgets/table_test.dart`, `test/widgets/overlay_portal_test.dart`, `test/widgets/overlay_test.dart`, `test/widgets/raw_tooltip_test.dart`, `test/widgets/overlay_layout_builder_test.dart`, and `test/material/slider_test.dart` all pass. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I added new tests to check the change I am making. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo --------- Co-authored-by: Victor Sanni <[email protected]>
1 parent f4821b5 commit 263bc45

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2573,7 +2573,13 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox
25732573

25742574
@override
25752575
void redepthChildren() {
2576-
_layoutSurrogate.redepthChild(this);
2576+
// The layout surrogate can be adopted after this box enters the theater.
2577+
// Until then, the surrogate has no owner and cannot redepth this child.
2578+
// Once the surrogate is adopted, its own [redepthChildren] will restore the
2579+
// depth invariant.
2580+
if (_layoutSurrogate.attached) {
2581+
_layoutSurrogate.redepthChild(this);
2582+
}
25772583
super.redepthChildren();
25782584
}
25792585

‎packages/flutter/test/widgets/overlay_portal_test.dart‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,59 @@ void main() {
10401040
verifyTreeIsClean();
10411041
});
10421042

1043+
// Regression test for https://github.com/flutter/flutter/issues/174133.
1044+
// [Table] defers adopting its render-object children until every row has been
1045+
// mounted. An [OverlayPortal] cell can mount its overlay child in that window,
1046+
// before the portal's layout-surrogate render object has been adopted by its
1047+
// parent.
1048+
testWidgets('OverlayPortal child inside a TableRow does not crash', (WidgetTester tester) async {
1049+
// Exercise the pre-mount show path while the layout surrogate is still
1050+
// waiting to be adopted by its parent.
1051+
final controller = OverlayPortalController()..show();
1052+
const overlayKey = Key('overlay-child');
1053+
late final OverlayEntry overlayEntry;
1054+
addTearDown(
1055+
() => overlayEntry
1056+
..remove()
1057+
..dispose(),
1058+
);
1059+
1060+
await tester.pumpWidget(
1061+
Directionality(
1062+
textDirection: TextDirection.ltr,
1063+
child: Overlay(
1064+
initialEntries: <OverlayEntry>[
1065+
overlayEntry = OverlayEntry(
1066+
builder: (BuildContext context) {
1067+
return Table(
1068+
children: <TableRow>[
1069+
TableRow(
1070+
children: <Widget>[
1071+
OverlayPortal(
1072+
controller: controller,
1073+
overlayChildBuilder: (BuildContext context) => const Align(
1074+
alignment: Alignment.topLeft,
1075+
child: SizedBox(key: overlayKey, width: 10, height: 10),
1076+
),
1077+
child: const SizedBox(width: 10, height: 10),
1078+
),
1079+
],
1080+
),
1081+
],
1082+
);
1083+
},
1084+
),
1085+
],
1086+
),
1087+
),
1088+
);
1089+
expect(tester.takeException(), isNull);
1090+
expect(find.byKey(overlayKey), findsOneWidget);
1091+
// Confirm the overlay child actually completed layout. The depth-invariant
1092+
// restoration must hold for the deferred-layout box to be laid out.
1093+
expect(tester.getSize(find.byKey(overlayKey)), const Size(10, 10));
1094+
});
1095+
10431096
testWidgets('adding/removing overlay child does not redirty overlay more than once', (
10441097
WidgetTester tester,
10451098
) async {

0 commit comments

Comments
 (0)