- Fixes _DropdownMenuState leaking text controller (#146571)

This commit is contained in:
Dimil Kalathiya 2024-04-14 20:29:43 +05:30 committed by GitHub
parent 266cdf0b1e
commit 1002ce4b03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 20 additions and 18 deletions

View file

@ -434,13 +434,15 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
double? leadingPadding; double? leadingPadding;
bool _menuHasEnabledItem = false; bool _menuHasEnabledItem = false;
TextEditingController? _localTextEditingController; TextEditingController? _localTextEditingController;
TextEditingController get _textEditingController {
return widget.controller ?? (_localTextEditingController ??= TextEditingController());
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (widget.controller != null) {
_localTextEditingController = widget.controller;
} else {
_localTextEditingController = TextEditingController();
}
_enableFilter = widget.enableFilter; _enableFilter = widget.enableFilter;
filteredEntries = widget.dropdownMenuEntries; filteredEntries = widget.dropdownMenuEntries;
buttonItemKeys = List<GlobalKey>.generate(filteredEntries.length, (int index) => GlobalKey()); buttonItemKeys = List<GlobalKey>.generate(filteredEntries.length, (int index) => GlobalKey());
@ -448,7 +450,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection); final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection);
if (index != -1) { if (index != -1) {
_textEditingController.value = TextEditingValue( _localTextEditingController?.value = TextEditingValue(
text: filteredEntries[index].label, text: filteredEntries[index].label,
selection: TextSelection.collapsed(offset: filteredEntries[index].label.length), selection: TextSelection.collapsed(offset: filteredEntries[index].label.length),
); );
@ -458,8 +460,10 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
@override @override
void dispose() { void dispose() {
if (widget.controller == null) {
_localTextEditingController?.dispose(); _localTextEditingController?.dispose();
_localTextEditingController = null; _localTextEditingController = null;
}
super.dispose(); super.dispose();
} }
@ -469,8 +473,8 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
if (oldWidget.controller != widget.controller) { if (oldWidget.controller != widget.controller) {
if (widget.controller != null) { if (widget.controller != null) {
_localTextEditingController?.dispose(); _localTextEditingController?.dispose();
_localTextEditingController = null;
} }
_localTextEditingController = widget.controller ?? TextEditingController();
} }
if (oldWidget.enableSearch != widget.enableSearch) { if (oldWidget.enableSearch != widget.enableSearch) {
if (!widget.enableSearch) { if (!widget.enableSearch) {
@ -489,7 +493,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
if (oldWidget.initialSelection != widget.initialSelection) { if (oldWidget.initialSelection != widget.initialSelection) {
final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection); final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection);
if (index != -1) { if (index != -1) {
_textEditingController.value = TextEditingValue( _localTextEditingController?.value = TextEditingValue(
text: filteredEntries[index].label, text: filteredEntries[index].label,
selection: TextSelection.collapsed(offset: filteredEntries[index].label.length), selection: TextSelection.collapsed(offset: filteredEntries[index].label.length),
); );
@ -601,7 +605,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
trailingIcon: entry.trailingIcon, trailingIcon: entry.trailingIcon,
onPressed: entry.enabled onPressed: entry.enabled
? () { ? () {
_textEditingController.value = TextEditingValue( _localTextEditingController?.value = TextEditingValue(
text: entry.label, text: entry.label,
selection: TextSelection.collapsed(offset: entry.label.length), selection: TextSelection.collapsed(offset: entry.label.length),
); );
@ -630,7 +634,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
currentHighlight = (currentHighlight! - 1) % filteredEntries.length; currentHighlight = (currentHighlight! - 1) % filteredEntries.length;
} }
final String currentLabel = filteredEntries[currentHighlight!].label; final String currentLabel = filteredEntries[currentHighlight!].label;
_textEditingController.value = TextEditingValue( _localTextEditingController?.value = TextEditingValue(
text: currentLabel, text: currentLabel,
selection: TextSelection.collapsed(offset: currentLabel.length), selection: TextSelection.collapsed(offset: currentLabel.length),
); );
@ -649,7 +653,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
currentHighlight = (currentHighlight! + 1) % filteredEntries.length; currentHighlight = (currentHighlight! + 1) % filteredEntries.length;
} }
final String currentLabel = filteredEntries[currentHighlight!].label; final String currentLabel = filteredEntries[currentHighlight!].label;
_textEditingController.value = TextEditingValue( _localTextEditingController?.value = TextEditingValue(
text: currentLabel, text: currentLabel,
selection: TextSelection.collapsed(offset: currentLabel.length), selection: TextSelection.collapsed(offset: currentLabel.length),
); );
@ -661,7 +665,7 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
currentHighlight = null; currentHighlight = null;
controller.close(); controller.close();
} else { // close to open } else { // close to open
if (_textEditingController.text.isNotEmpty) { if (_localTextEditingController!.text.isNotEmpty) {
_enableFilter = false; _enableFilter = false;
} }
controller.open(); controller.open();
@ -677,14 +681,14 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context); final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context);
if (_enableFilter) { if (_enableFilter) {
filteredEntries = filter(widget.dropdownMenuEntries, _textEditingController); filteredEntries = filter(widget.dropdownMenuEntries, _localTextEditingController!);
} }
if (widget.enableSearch) { if (widget.enableSearch) {
if (widget.searchCallback != null) { if (widget.searchCallback != null) {
currentHighlight = widget.searchCallback!.call(filteredEntries, _textEditingController.text); currentHighlight = widget.searchCallback!.call(filteredEntries, _localTextEditingController!.text);
} else { } else {
currentHighlight = search(filteredEntries, _textEditingController); currentHighlight = search(filteredEntries, _localTextEditingController!);
} }
if (currentHighlight != null) { if (currentHighlight != null) {
scrollToHighlight(); scrollToHighlight();
@ -750,12 +754,12 @@ class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
enableInteractiveSelection: canRequestFocus(), enableInteractiveSelection: canRequestFocus(),
textAlignVertical: TextAlignVertical.center, textAlignVertical: TextAlignVertical.center,
style: effectiveTextStyle, style: effectiveTextStyle,
controller: _textEditingController, controller: _localTextEditingController,
onEditingComplete: () { onEditingComplete: () {
if (currentHighlight != null) { if (currentHighlight != null) {
final DropdownMenuEntry<T> entry = filteredEntries[currentHighlight!]; final DropdownMenuEntry<T> entry = filteredEntries[currentHighlight!];
if (entry.enabled) { if (entry.enabled) {
_textEditingController.value = TextEditingValue( _localTextEditingController?.value = TextEditingValue(
text: entry.label, text: entry.label,
selection: TextSelection.collapsed(offset: entry.label.length), selection: TextSelection.collapsed(offset: entry.label.length),
); );

View file

@ -8,11 +8,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_testing/leak_tracker_testing.dart';
void main() { void main() {
// TODO(polina-c): _DropdownMenuState should not be used after disposal, https://github.com/flutter/flutter/issues/145622 [leaks-to-clean]
LeakTesting.settings = LeakTesting.settings.withIgnoredAll();
const String longText = 'one two three four five six seven eight nine ten eleven twelve'; const String longText = 'one two three four five six seven eight nine ten eleven twelve';
final List<DropdownMenuEntry<TestMenu>> menuChildren = <DropdownMenuEntry<TestMenu>>[]; final List<DropdownMenuEntry<TestMenu>> menuChildren = <DropdownMenuEntry<TestMenu>>[];
@ -2070,6 +2067,7 @@ void main() {
}, },
); );
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
addTearDown(controller.dispose);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(