diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart index b97f319ea4..431fdd2c18 100644 --- a/packages/flutter/lib/src/material/dropdown_menu.dart +++ b/packages/flutter/lib/src/material/dropdown_menu.dart @@ -348,18 +348,20 @@ class _DropdownMenuState extends State> { final GlobalKey _leadingKey = GlobalKey(); late List buttonItemKeys; final MenuController _controller = MenuController(); - late final TextEditingController _textEditingController; late bool _enableFilter; late List> filteredEntries; List? _initialMenu; int? currentHighlight; double? leadingPadding; bool _menuHasEnabledItem = false; + TextEditingController? _localTextEditingController; + TextEditingController get _textEditingController { + return widget.controller ?? (_localTextEditingController ??= TextEditingController()); + } @override void initState() { super.initState(); - _textEditingController = widget.controller ?? TextEditingController(); _enableFilter = widget.enableFilter; filteredEntries = widget.dropdownMenuEntries; buttonItemKeys = List.generate(filteredEntries.length, (int index) => GlobalKey()); @@ -367,16 +369,33 @@ class _DropdownMenuState extends State> { final int index = filteredEntries.indexWhere((DropdownMenuEntry entry) => entry.value == widget.initialSelection); if (index != -1) { - _textEditingController.text = filteredEntries[index].label; - _textEditingController.selection = - TextSelection.collapsed(offset: _textEditingController.text.length); + _textEditingController.value = TextEditingValue( + text: filteredEntries[index].label, + selection: TextSelection.collapsed(offset: filteredEntries[index].label.length), + ); } refreshLeadingPadding(); } + @override + void dispose() { + if (_localTextEditingController != null) { + debugPrint('Disposing of $_textEditingController'); + } + _localTextEditingController?.dispose(); + _localTextEditingController = null; + super.dispose(); + } + @override void didUpdateWidget(DropdownMenu oldWidget) { super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + if (widget.controller != null) { + _localTextEditingController?.dispose(); + _localTextEditingController = null; + } + } if (oldWidget.enableSearch != widget.enableSearch) { if (!widget.enableSearch) { currentHighlight = null; @@ -394,9 +413,10 @@ class _DropdownMenuState extends State> { if (oldWidget.initialSelection != widget.initialSelection) { final int index = filteredEntries.indexWhere((DropdownMenuEntry entry) => entry.value == widget.initialSelection); if (index != -1) { - _textEditingController.text = filteredEntries[index].label; - _textEditingController.selection = - TextSelection.collapsed(offset: _textEditingController.text.length); + _textEditingController.value = TextEditingValue( + text: filteredEntries[index].label, + selection: TextSelection.collapsed(offset: filteredEntries[index].label.length), + ); } } } @@ -463,7 +483,6 @@ class _DropdownMenuState extends State> { List _buildButtons( List> filteredEntries, - TextEditingController textEditingController, TextDirection textDirection, { int? focusedIndex, bool enableScrollToHighlight = true} ) { @@ -519,9 +538,10 @@ class _DropdownMenuState extends State> { trailingIcon: entry.trailingIcon, onPressed: entry.enabled ? () { - textEditingController.text = entry.label; - textEditingController.selection = - TextSelection.collapsed(offset: textEditingController.text.length); + _textEditingController.value = TextEditingValue( + text: entry.label, + selection: TextSelection.collapsed(offset: entry.label.length), + ); currentHighlight = widget.enableSearch ? i : null; widget.onSelected?.call(entry.value); } @@ -535,37 +555,43 @@ class _DropdownMenuState extends State> { return result; } - void handleUpKeyInvoke(_) => setState(() { - if (!_menuHasEnabledItem || !_controller.isOpen) { - return; - } - _enableFilter = false; - currentHighlight ??= 0; - currentHighlight = (currentHighlight! - 1) % filteredEntries.length; - while (!filteredEntries[currentHighlight!].enabled) { + void handleUpKeyInvoke(_) { + setState(() { + if (!_menuHasEnabledItem || !_controller.isOpen) { + return; + } + _enableFilter = false; + currentHighlight ??= 0; currentHighlight = (currentHighlight! - 1) % filteredEntries.length; - } - final String currentLabel = filteredEntries[currentHighlight!].label; - _textEditingController.text = currentLabel; - _textEditingController.selection = - TextSelection.collapsed(offset: _textEditingController.text.length); - }); + while (!filteredEntries[currentHighlight!].enabled) { + currentHighlight = (currentHighlight! - 1) % filteredEntries.length; + } + final String currentLabel = filteredEntries[currentHighlight!].label; + _textEditingController.value = TextEditingValue( + text: currentLabel, + selection: TextSelection.collapsed(offset: currentLabel.length), + ); + }); + } - void handleDownKeyInvoke(_) => setState(() { - if (!_menuHasEnabledItem || !_controller.isOpen) { - return; - } - _enableFilter = false; - currentHighlight ??= -1; - currentHighlight = (currentHighlight! + 1) % filteredEntries.length; - while (!filteredEntries[currentHighlight!].enabled) { + void handleDownKeyInvoke(_) { + setState(() { + if (!_menuHasEnabledItem || !_controller.isOpen) { + return; + } + _enableFilter = false; + currentHighlight ??= -1; currentHighlight = (currentHighlight! + 1) % filteredEntries.length; - } - final String currentLabel = filteredEntries[currentHighlight!].label; - _textEditingController.text = currentLabel; - _textEditingController.selection = - TextSelection.collapsed(offset: _textEditingController.text.length); - }); + while (!filteredEntries[currentHighlight!].enabled) { + currentHighlight = (currentHighlight! + 1) % filteredEntries.length; + } + final String currentLabel = filteredEntries[currentHighlight!].label; + _textEditingController.value = TextEditingValue( + text: currentLabel, + selection: TextSelection.collapsed(offset: currentLabel.length), + ); + }); + } void handlePressed(MenuController controller) { if (controller.isOpen) { @@ -580,18 +606,10 @@ class _DropdownMenuState extends State> { setState(() {}); } - @override - void dispose() { - if (widget.controller == null) { - _textEditingController.dispose(); - } - super.dispose(); - } - @override Widget build(BuildContext context) { final TextDirection textDirection = Directionality.of(context); - _initialMenu ??= _buildButtons(widget.dropdownMenuEntries, _textEditingController, textDirection, enableScrollToHighlight: false); + _initialMenu ??= _buildButtons(widget.dropdownMenuEntries, textDirection, enableScrollToHighlight: false); final DropdownMenuThemeData theme = DropdownMenuTheme.of(context); final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context); @@ -610,7 +628,7 @@ class _DropdownMenuState extends State> { } } - final List menu = _buildButtons(filteredEntries, _textEditingController, textDirection, focusedIndex: currentHighlight); + final List menu = _buildButtons(filteredEntries, textDirection, focusedIndex: currentHighlight); final TextStyle? effectiveTextStyle = widget.textStyle ?? theme.textStyle ?? defaults.textStyle; @@ -670,9 +688,10 @@ class _DropdownMenuState extends State> { if (currentHighlight != null) { final DropdownMenuEntry entry = filteredEntries[currentHighlight!]; if (entry.enabled) { - _textEditingController.text = entry.label; - _textEditingController.selection = - TextSelection.collapsed(offset: _textEditingController.text.length); + _textEditingController.value = TextEditingValue( + text: entry.label, + selection: TextSelection.collapsed(offset: entry.label.length), + ); widget.onSelected?.call(entry.value); } } else { diff --git a/packages/flutter/test/material/dropdown_menu_test.dart b/packages/flutter/test/material/dropdown_menu_test.dart index 27b0e9dafc..e95fe1b10b 100644 --- a/packages/flutter/test/material/dropdown_menu_test.dart +++ b/packages/flutter/test/material/dropdown_menu_test.dart @@ -1805,6 +1805,109 @@ void main() { await tester.pump(); checkExpectedHighlight(searchResult: 'Read', otherItems: ['All', 'Unread']); }); + + testWidgetsWithLeakTracking('onSelected gets called when a selection is made in a nested menu', + (WidgetTester tester) async { + int selectionCount = 0; + + final ThemeData themeData = ThemeData(); + final List> menuWithDisabledItems = >[ + const DropdownMenuEntry(value: TestMenu.mainMenu0, label: 'Item 0'), + ]; + + await tester.pumpWidget(MaterialApp( + theme: themeData, + home: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { + return Scaffold( + body: MenuAnchor( + menuChildren: [ + DropdownMenu( + dropdownMenuEntries: menuWithDisabledItems, + onSelected: (_) { + setState(() { + selectionCount++; + }); + }, + ), + ], + builder: (BuildContext context, MenuController controller, Widget? widget) { + return IconButton( + icon: const Icon(Icons.smartphone_rounded), + onPressed: () { + controller.open(); + }, + ); + }, + ), + ); + }), + )); + + // Open the first menu + await tester.tap(find.byType(IconButton)); + await tester.pump(); + // Open the dropdown menu + await tester.tap(find.byType(DropdownMenu)); + await tester.pump(); + + final Finder item1 = find.widgetWithText(MenuItemButton, 'Item 0').last; + await tester.tap(item1); + await tester.pumpAndSettle(); + + expect(selectionCount, 1); + }); + + testWidgetsWithLeakTracking('When onSelected is called and menu is closed, no textEditingController exception is thrown', + (WidgetTester tester) async { + int selectionCount = 0; + + final ThemeData themeData = ThemeData(); + final List> menuWithDisabledItems = >[ + const DropdownMenuEntry(value: TestMenu.mainMenu0, label: 'Item 0'), + ]; + + await tester.pumpWidget(MaterialApp( + theme: themeData, + home: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { + return Scaffold( + body: MenuAnchor( + menuChildren: [ + DropdownMenu( + dropdownMenuEntries: menuWithDisabledItems, + onSelected: (_) { + setState(() { + selectionCount++; + }); + }, + ), + ], + builder: (BuildContext context, MenuController controller, Widget? widget) { + return IconButton( + icon: const Icon(Icons.smartphone_rounded), + onPressed: () { + controller.open(); + }, + ); + }, + ), + ); + }), + )); + + // Open the first menu + await tester.tap(find.byType(IconButton)); + await tester.pump(); + // Open the dropdown menu + await tester.tap(find.byType(DropdownMenu)); + await tester.pump(); + + final Finder item1 = find.widgetWithText(MenuItemButton, 'Item 0').last; + await tester.tap(item1); + await tester.pumpAndSettle(); + + expect(selectionCount, 1); + expect(tester.takeException(), isNull); + }); } enum TestMenu {