I have created a custom SearchDropdown
widget in Flutter, which includes a TextFormField
and a dropdown list of items. When the text field gains or loses focus, the dropdown list should appear or disappear respectively. When an item in the dropdown list is tapped, it should be selected, and the dropdown should hide.
Currently, I am using Future.delayed(const Duration(milliseconds: 700))
in the _handleFocusChange
method to delay hiding the dropdown. This is because when a user taps (selects) an item from the dropdown list, only _handleFocusChange
is called, and the tap is not registered immediately. By using a delay, I can handle both the tap and the focus change. However, this approach feels hacky and unreliable.
How can I handle the focus change and item selection in the dropdown without using Future.delayed(const Duration(milliseconds: 700))
in _handleFocusChange
? What is the best practice to achieve this functionality in Flutter?
Here are the relevant parts of my code:
class SearchDropdown extends StatefulWidget {
final List<SearchDropdownItem> items;
final Function(String) onTextChanged;
final Function(dynamic) onItemSelected;
final String selectedTextValue;
final String? Function(String?)? validator;
final String? headerText;
const SearchDropdown({
super.key,
required this.items,
required this.onTextChanged,
required this.onItemSelected,
required this.selectedTextValue,
this.headerText,
this.validator,
});
@override
_SearchDropdownState createState() => _SearchDropdownState();
}
class _SearchDropdownState extends State<SearchDropdown> {
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
bool _isFocused = false;
bool _isShowDropdown = false;
OverlayEntry? _overlayEntry;
final LayerLink _layerLink = LayerLink();
void _onTextChanged() {
widget.onTextChanged(_controller.text);
}
void _clearText() {
_controller.clear();
_onTextChanged();
_handleItemSelected("", null);
}
void _handleItemSelected(String title, dynamic value) {
_controller.text = title;
widget.onItemSelected(value);
_focusNode.unfocus(); // Remove focus when item is selected
setState(() {
_isShowDropdown = false; // Hide the dropdown when item is selected
});
}
@override
void initState() {
super.initState();
_controller.text = widget.selectedTextValue;
_controller.addListener(_onTextChanged);
_focusNode.addListener(_handleFocusChange);
}
void _handleFocusChange() {
if (_focusNode.hasFocus) {
if (mounted) {
setState(() {
_isFocused = true;
_isShowDropdown = true;
});
}
} else {
Future.delayed(const Duration(milliseconds: 700), () {
if (mounted && !_focusNode.hasFocus) {
setState(() {
_isFocused = false;
_isShowDropdown = false;
});
}
});
}
}
void _showDropdown() {
if (_overlayEntry != null) {
_overlayEntry!.remove();
}
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry!);
}
void _removeDropdown() {
if (_overlayEntry != null) {
_overlayEntry!.remove();
_overlayEntry = null;
}
}
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject() as RenderBox;
var size = renderBox.size;
return OverlayEntry(
builder: (context) => Positioned(
width: size.width,
child: CompositedTransformFollower(
link: _layerLink,
showWhenUnlinked: false,
offset: Offset(0, size.height),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
border: Border.all(color: Theme.of(context).colorScheme.onSurface),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(UIConstants.radius),
bottomRight: Radius.circular(UIConstants.radius),
),
),
child: Material(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(UIConstants.radius),
bottomRight: Radius.circular(UIConstants.radius),
),
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200, minHeight: 0),
child: ListView(
shrinkWrap: true,
padding: EdgeInsets.zero,
children: widget.items.map((item) {
return ListTile(
title: Text(item.title),
onTap: () {
_handleItemSelected(item.title, item.value);
},
);
}).toList(),
),
),
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
if (_isShowDropdown) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_showDropdown();
});
} else {
WidgetsBinding.instance.addPostFrameCallback((_) {
_removeDropdown();
});
}
return CompositedTransformTarget(
link: _layerLink,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.headerText != null) ...[
Text(widget.headerText ?? "", style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 5),
],
TextFormField(
controller: _controller,
focusNode: _focusNode,
validator: widget.validator,
.
.
.
.
.
Any help or suggestions would be greatly appreciated!
Johnny is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
2