From 2f5ffe08705b650396e7338395e4ab12426b12e1 Mon Sep 17 00:00:00 2001 From: Shivam Gupta Date: Thu, 10 Sep 2026 11:30:40 +0530 Subject: [PATCH 1/3] [material_ui] Fix DropdownButton crash on orientation change while menu is open Defer Navigator.removeRoute until after the current frame when orientation changes during build. This ports the reviewed fix from flutter/flutter#187366. Fixes https://github.com/flutter/flutter/issues/171011 --- packages/material_ui/lib/src/dropdown.dart | 18 +++++- ...nge_187366_dropdown_orientation_crash.yaml | 3 + packages/material_ui/test/dropdown_test.dart | 59 +++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 packages/material_ui/pending_changelogs/change_187366_dropdown_orientation_crash.yaml diff --git a/packages/material_ui/lib/src/dropdown.dart b/packages/material_ui/lib/src/dropdown.dart index 7f0c8eb67fea..389632b3219c 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 000000000000..271b95ecf6d8 --- /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 1e8d2d6b2451..f66e38b4f2e8 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)); From 355216878c8e2ace45f66b203bcdd7713c104db2 Mon Sep 17 00:00:00 2001 From: Shivam Gupta Date: Mon, 14 Sep 2026 15:34:57 +0530 Subject: [PATCH 2/3] [material_ui] Rename orientation-change regression test Use DropdownButton in the test name so it is easier to find when those tests move to dropdown_button_test.dart. --- packages/material_ui/test/dropdown_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/material_ui/test/dropdown_test.dart b/packages/material_ui/test/dropdown_test.dart index f66e38b4f2e8..2b99a0d23b2a 100644 --- a/packages/material_ui/test/dropdown_test.dart +++ b/packages/material_ui/test/dropdown_test.dart @@ -1376,7 +1376,7 @@ void main() { ); testWidgets( - 'Dropdown menu opened inside a dialog is dismissed on orientation change without throwing', + '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); From e4c9b23d8c8a352f943ad10ad9e6b48924a9f058 Mon Sep 17 00:00:00 2001 From: Shivam Gupta Date: Mon, 14 Sep 2026 20:26:55 +0530 Subject: [PATCH 3/3] [material_ui] Move orientation-change test to dropdown_button_test.dart Place the regression test in its own DropdownButton test file so it does not conflict with the upcoming dropdown_test.dart split. --- .../test/dropdown_button_test.dart | 69 +++++++++++++++++++ packages/material_ui/test/dropdown_test.dart | 59 ---------------- 2 files changed, 69 insertions(+), 59 deletions(-) create mode 100644 packages/material_ui/test/dropdown_button_test.dart 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 000000000000..f04fc8ee258a --- /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); + }, + ); +} diff --git a/packages/material_ui/test/dropdown_test.dart b/packages/material_ui/test/dropdown_test.dart index 2b99a0d23b2a..1e8d2d6b2451 100644 --- a/packages/material_ui/test/dropdown_test.dart +++ b/packages/material_ui/test/dropdown_test.dart @@ -1375,65 +1375,6 @@ 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); - }, - ); - testWidgets('Semantics Tree contains only selected element', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget(buildFrame(onChanged: onChanged));