mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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 (a0b1b325341) ## 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 <brackenavaron@gmail.com> Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com>
This commit is contained in:
parent
5285cbb750
commit
777bfea286
@ -1568,7 +1568,17 @@ class PopupMenuButton<T> extends StatefulWidget {
|
||||
/// of your button state.
|
||||
class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
|
||||
bool _isMenuExpanded = false;
|
||||
RelativeRect? _lastPosition;
|
||||
|
||||
RelativeRect _positionBuilder(BuildContext _, BoxConstraints constraints) {
|
||||
if (!mounted) {
|
||||
// When the route is displayed, the `_positionBuilder` closure is stored.
|
||||
// Even after the button has been unmounted and the context becomes invalid,
|
||||
// the route might keep displaying, and `_positionBuilder` must continue to
|
||||
// work in that case.
|
||||
return _lastPosition ?? RelativeRect.fromSize(Rect.zero, constraints.biggest);
|
||||
}
|
||||
|
||||
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||
final RenderBox button = context.findRenderObject()! as RenderBox;
|
||||
final RenderBox overlay =
|
||||
@ -1598,7 +1608,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
|
||||
Offset.zero & overlay.size,
|
||||
);
|
||||
|
||||
return position;
|
||||
return _lastPosition = position;
|
||||
}
|
||||
|
||||
/// A method to show a popup menu with the items supplied to
|
||||
|
||||
@ -2863,6 +2863,71 @@ void main() {
|
||||
expect(mediaQueryPadding, EdgeInsets.zero);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/163477
|
||||
testWidgets("PopupMenu's overlay can be rebuilt even when the button is unmounted", (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final GlobalKey buttonKey = GlobalKey();
|
||||
|
||||
late StateSetter setState;
|
||||
bool showButton = true;
|
||||
|
||||
Widget widget({required Size viewSize}) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: viewSize.width,
|
||||
height: viewSize.height,
|
||||
child: MaterialApp(
|
||||
home: Material(
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter innerSetState) {
|
||||
setState = innerSetState;
|
||||
return showButton
|
||||
? PopupMenuButton<int>(
|
||||
key: buttonKey,
|
||||
popUpAnimationStyle: const AnimationStyle(
|
||||
reverseDuration: Duration(milliseconds: 400),
|
||||
),
|
||||
itemBuilder: (BuildContext context) {
|
||||
return <PopupMenuEntry<int>>[
|
||||
PopupMenuItem<int>(value: 1, child: const Text('ACTION'), onTap: () {}),
|
||||
];
|
||||
},
|
||||
)
|
||||
: Container();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Pump a button
|
||||
await tester.pumpWidget(widget(viewSize: const Size(500, 500)));
|
||||
|
||||
// Tap the button to show the menu
|
||||
await tester.tap(find.byKey(buttonKey));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('ACTION'), findsOne);
|
||||
expect(find.byKey(buttonKey), findsOne);
|
||||
|
||||
// Hide the button. The menu still shows since it's placed on a separate route.
|
||||
setState(() {
|
||||
showButton = false;
|
||||
});
|
||||
await tester.pump();
|
||||
expect(find.text('ACTION'), findsOne);
|
||||
expect(find.byKey(buttonKey), findsNothing);
|
||||
|
||||
// Resize the view, causing the menu to rebuild. Before the fix, this
|
||||
// rebuild would lead to a crash, because it relies on context of the button,
|
||||
// which has been unmounted.
|
||||
await tester.pumpWidget(widget(viewSize: const Size(300, 300)));
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
group('feedback', () {
|
||||
late FeedbackTester feedback;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user