Skip to content

Commit 777bfea

Browse files
sstasi95navaronbrackedkwingsmt
authored
fix PopupMenuButton unmounted exception when updating position (#166412)
<!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> This PR fixes an exception thrown when trying to update an unmounted PopupMenuButton's position, which can happen when a layout change is triggered during PopupMenuButton's pop animation (see issue's attached video). A workaround is to set `popUpAnimationStyle: AnimationStyle.noAnimation`. This PR fixes it by returning the last known position if the button is unmounted. Exception thrown:` FlutterError (This widget has been unmounted, so the State no longer has a context (and should be considered defunct). Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.)` Code that causes the exception: `final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);` Fixes #163477 Tested both on stable (3.29.2) and master (a0b1b32) ## 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], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] 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 --------- Co-authored-by: Navaron Bracke <[email protected]> Co-authored-by: Tong Mu <[email protected]>
1 parent 5285cbb commit 777bfea

2 files changed

Lines changed: 76 additions & 1 deletion

File tree

packages/flutter/lib/src/material/popup_menu.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1568,7 +1568,17 @@ class PopupMenuButton<T> extends StatefulWidget {
15681568
/// of your button state.
15691569
class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
15701570
bool _isMenuExpanded = false;
1571+
RelativeRect? _lastPosition;
1572+
15711573
RelativeRect _positionBuilder(BuildContext _, BoxConstraints constraints) {
1574+
if (!mounted) {
1575+
// When the route is displayed, the `_positionBuilder` closure is stored.
1576+
// Even after the button has been unmounted and the context becomes invalid,
1577+
// the route might keep displaying, and `_positionBuilder` must continue to
1578+
// work in that case.
1579+
return _lastPosition ?? RelativeRect.fromSize(Rect.zero, constraints.biggest);
1580+
}
1581+
15721582
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
15731583
final RenderBox button = context.findRenderObject()! as RenderBox;
15741584
final RenderBox overlay =
@@ -1598,7 +1608,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
15981608
Offset.zero & overlay.size,
15991609
);
16001610

1601-
return position;
1611+
return _lastPosition = position;
16021612
}
16031613

16041614
/// A method to show a popup menu with the items supplied to

packages/flutter/test/material/popup_menu_test.dart

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2863,6 +2863,71 @@ void main() {
28632863
expect(mediaQueryPadding, EdgeInsets.zero);
28642864
});
28652865

2866+
// Regression test for https://github.com/flutter/flutter/issues/163477
2867+
testWidgets("PopupMenu's overlay can be rebuilt even when the button is unmounted", (
2868+
WidgetTester tester,
2869+
) async {
2870+
final GlobalKey buttonKey = GlobalKey();
2871+
2872+
late StateSetter setState;
2873+
bool showButton = true;
2874+
2875+
Widget widget({required Size viewSize}) {
2876+
return Center(
2877+
child: SizedBox(
2878+
width: viewSize.width,
2879+
height: viewSize.height,
2880+
child: MaterialApp(
2881+
home: Material(
2882+
child: StatefulBuilder(
2883+
builder: (BuildContext context, StateSetter innerSetState) {
2884+
setState = innerSetState;
2885+
return showButton
2886+
? PopupMenuButton<int>(
2887+
key: buttonKey,
2888+
popUpAnimationStyle: const AnimationStyle(
2889+
reverseDuration: Duration(milliseconds: 400),
2890+
),
2891+
itemBuilder: (BuildContext context) {
2892+
return <PopupMenuEntry<int>>[
2893+
PopupMenuItem<int>(value: 1, child: const Text('ACTION'), onTap: () {}),
2894+
];
2895+
},
2896+
)
2897+
: Container();
2898+
},
2899+
),
2900+
),
2901+
),
2902+
),
2903+
);
2904+
}
2905+
2906+
// Pump a button
2907+
await tester.pumpWidget(widget(viewSize: const Size(500, 500)));
2908+
2909+
// Tap the button to show the menu
2910+
await tester.tap(find.byKey(buttonKey));
2911+
await tester.pumpAndSettle();
2912+
expect(find.text('ACTION'), findsOne);
2913+
expect(find.byKey(buttonKey), findsOne);
2914+
2915+
// Hide the button. The menu still shows since it's placed on a separate route.
2916+
setState(() {
2917+
showButton = false;
2918+
});
2919+
await tester.pump();
2920+
expect(find.text('ACTION'), findsOne);
2921+
expect(find.byKey(buttonKey), findsNothing);
2922+
2923+
// Resize the view, causing the menu to rebuild. Before the fix, this
2924+
// rebuild would lead to a crash, because it relies on context of the button,
2925+
// which has been unmounted.
2926+
await tester.pumpWidget(widget(viewSize: const Size(300, 300)));
2927+
2928+
expect(tester.takeException(), isNull);
2929+
});
2930+
28662931
group('feedback', () {
28672932
late FeedbackTester feedback;
28682933

0 commit comments

Comments
 (0)