diff --git a/packages/material_ui/lib/src/dropdown.dart b/packages/material_ui/lib/src/dropdown.dart index 7f0c8eb67fe..389632b3219 100644 --- a/packages/material_ui/lib/src/dropdown.dart +++ b/packages/material_ui/lib/src/dropdown.dart @@ -15,6 +15,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -1588,8 +1589,23 @@ class _DropdownButtonState extends State> with WidgetsBindi final Orientation newOrientation = _getOrientation(context); _lastOrientation ??= newOrientation; if (newOrientation != _lastOrientation) { - _removeDropdownRoute(); _lastOrientation = newOrientation; + // The open menu's layout is computed for the previous orientation, so it + // must be dismissed. Removing the route mutates the Navigator, which is + // not allowed during build, so defer the dismissal until after the + // current frame when we are in the persistent callbacks phase. + // See https://github.com/flutter/flutter/issues/171011 + if (_dropdownRoute != null) { + if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { + WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) { + if (mounted) { + _removeDropdownRoute(); + } + }, debugLabel: 'DropdownButton.dismissOnOrientationChange'); + } else { + _removeDropdownRoute(); + } + } } // The width of the button and the menu are defined by the widest diff --git a/packages/material_ui/pending_changelogs/change_187366_dropdown_orientation_crash.yaml b/packages/material_ui/pending_changelogs/change_187366_dropdown_orientation_crash.yaml new file mode 100644 index 00000000000..271b95ecf6d --- /dev/null +++ b/packages/material_ui/pending_changelogs/change_187366_dropdown_orientation_crash.yaml @@ -0,0 +1,3 @@ +changelog: | + - Fixes a crash when a DropdownButton menu is open inside a dialog and the device orientation changes. +version: patch diff --git a/packages/material_ui/test/dropdown_test.dart b/packages/material_ui/test/dropdown_test.dart index 1e8d2d6b245..f66e38b4f2e 100644 --- a/packages/material_ui/test/dropdown_test.dart +++ b/packages/material_ui/test/dropdown_test.dart @@ -1375,6 +1375,65 @@ void main() { }, ); + testWidgets( + 'Dropdown menu opened inside a dialog is dismissed on orientation change without throwing', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/171011 + addTearDown(tester.view.reset); + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(800, 600); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return Center( + child: ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + content: DropdownButton( + value: 1, + onChanged: (int? value) {}, + items: const >[ + DropdownMenuItem(value: 1, child: Text('1')), + DropdownMenuItem(value: 2, child: Text('2')), + DropdownMenuItem(value: 3, child: Text('3')), + ], + ), + ); + }, + ); + }, + child: const Text('Open dialog'), + ), + ); + }, + ), + ), + ), + ); + + // Open the dialog, then open the dropdown menu inside it. + await tester.tap(find.text('Open dialog')); + await tester.pumpAndSettle(); + await tester.tap(find.byType(DropdownButton)); + await tester.pumpAndSettle(); + expect(find.byType(ListView), findsOneWidget); + + // Rotate the device (simulate by swapping the physical size dimensions). + tester.view.physicalSize = const Size(600, 800); + await tester.pumpAndSettle(); + + // The route should be dismissed without mutating the Navigator during build. + expect(tester.takeException(), isNull); + expect(find.byType(ListView), findsNothing); + }, + ); + testWidgets('Semantics Tree contains only selected element', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget(buildFrame(onChanged: onChanged));