-
Notifications
You must be signed in to change notification settings - Fork 30.4k
Fix DropdownMenu throwing TextEditingController disposed error on select
#139385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6b8bc17
e35986f
a03462c
875669b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -348,35 +348,54 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { | |
| final GlobalKey _leadingKey = GlobalKey(); | ||
| late List<GlobalKey> buttonItemKeys; | ||
| final MenuController _controller = MenuController(); | ||
| late final TextEditingController _textEditingController; | ||
| late bool _enableFilter; | ||
| late List<DropdownMenuEntry<T>> filteredEntries; | ||
| List<Widget>? _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<GlobalKey>.generate(filteredEntries.length, (int index) => GlobalKey()); | ||
| _menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled); | ||
|
|
||
| final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> 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<T> 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<T> extends State<DropdownMenu<T>> { | |
| if (oldWidget.initialSelection != widget.initialSelection) { | ||
| final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> 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<T> extends State<DropdownMenu<T>> { | |
|
|
||
| List<Widget> _buildButtons( | ||
| List<DropdownMenuEntry<T>> filteredEntries, | ||
| TextEditingController textEditingController, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Part of the problem here was that the text editing controller that was passed in was being captured by the closure, rather than being accessed new each time, so it would capture an instance that was no longer current. There's no need to pass it in, since it's a member of the state class. |
||
| TextDirection textDirection, | ||
| { int? focusedIndex, bool enableScrollToHighlight = true} | ||
| ) { | ||
|
|
@@ -519,9 +538,10 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { | |
| 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<T> extends State<DropdownMenu<T>> { | |
| 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<T> extends State<DropdownMenu<T>> { | |
| 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<T> extends State<DropdownMenu<T>> { | |
| } | ||
| } | ||
|
|
||
| final List<Widget> menu = _buildButtons(filteredEntries, _textEditingController, textDirection, focusedIndex: currentHighlight); | ||
| final List<Widget> menu = _buildButtons(filteredEntries, textDirection, focusedIndex: currentHighlight); | ||
|
|
||
| final TextStyle? effectiveTextStyle = widget.textStyle ?? theme.textStyle ?? defaults.textStyle; | ||
|
|
||
|
|
@@ -670,9 +688,10 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> { | |
| if (currentHighlight != null) { | ||
| final DropdownMenuEntry<T> 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 { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The old code wasn't handling the case where the widget got a new controller later on. It would just ignore any controller that was set later, and continue using the one it originally constructed.