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_button_test.dart b/packages/material_ui/test/dropdown_button_test.dart new file mode 100644 index 00000000000..f04fc8ee258 --- /dev/null +++ b/packages/material_ui/test/dropdown_button_test.dart @@ -0,0 +1,69 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:material_ui/material_ui.dart'; + +void main() { + testWidgets( + 'DropdownButton 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); + }, + ); +}