I am currently working on a Flutter project and I am trying to create a searchable dropdown using the CustomDropdown
widget. The issue I am facing is that the filteredItems
variable is updating correctly when I change the search filter text, but the ListView
within the dropdown does not update to display the filtered items inside PopupMenuItem
.
import 'package:flutter/material.dart';
class CustomDropdown extends StatefulWidget {
final String labelText;
final List<String> items;
final ValueChanged<String?> onChanged;
final TextEditingController dropdownTextFieldController;
const CustomDropdown({
Key? key,
required this.labelText,
required this.items,
required this.onChanged,
required this.dropdownTextFieldController,
}) : super(key: key);
@override
CustomDropdownState createState() => CustomDropdownState();
}
class CustomDropdownState extends State<CustomDropdown> {
TextEditingController searchController = TextEditingController();
List<String> filteredItems = [];
@override
void initState() {
super.initState();
filteredItems = widget.items;
}
void filterItems(String query) {
setState(() {
filteredItems = widget.items
.where((item) => item.toLowerCase().contains(query.toLowerCase()))
.toList();
});
}
void setSelectedValue(String? value) {
widget.dropdownTextFieldController.text = value ?? '';
}
void _showDropdown(BuildContext context) {
final RenderBox textFieldRenderBox =
context.findRenderObject() as RenderBox;
showMenu(
context: context,
position: RelativeRect.fromLTRB(
textFieldRenderBox.localToGlobal(Offset.zero).dx,
textFieldRenderBox.localToGlobal(Offset.zero).dy +
textFieldRenderBox.size.height,
textFieldRenderBox.localToGlobal(Offset.zero).dx +
textFieldRenderBox.size.width,
textFieldRenderBox.localToGlobal(Offset.zero).dy +
textFieldRenderBox.size.height +
10,
),
items: <PopupMenuEntry<String>>[
_buildSearchField(),
if (filteredItems.isNotEmpty) _buildFilteredItems(),
],
).then((value) {
if (value != null) {
widget.dropdownTextFieldController.text = value;
widget.onChanged(value);
}
});
}
PopupMenuItem<String> _buildSearchField() {
return PopupMenuItem<String>(
child: SizedBox(
width: 300,
child: TextField(
controller: searchController,
decoration: const InputDecoration(
hintText: 'Search...',
suffixIcon: Icon(Icons.search),
),
onChanged: (query) {
filterItems(query);
},
),
),
);
}
PopupMenuItem<String> _buildFilteredItems() {
return PopupMenuItem<String>(
child: SizedBox(
height: 200,
width: 300,
child: ListView.builder(
itemCount: filteredItems.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(filteredItems[index]),
onTap: () {
widget.dropdownTextFieldController.text = filteredItems[index];
widget.onChanged(filteredItems[index]);
Navigator.pop(context);
},
);
},
),
),
);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.labelText,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 10),
TextField(
readOnly: true,
onTap: () {
_showDropdown(context);
},
controller: widget.dropdownTextFieldController,
decoration: InputDecoration(
hintText: "Select an item",
suffixIcon: IconButton(
icon: const Icon(Icons.arrow_drop_down),
onPressed: () {
_showDropdown(context);
},
),
),
),
],
);
}
}
class CustomDropDown {
Widget simpleDropDown(
bool loading,
String label,
List<String> items,
TextEditingController controller,
Function? function, {
bool showSearch = false,
}) {
return SizedBox(
width: 300,
child: CustomDropdown(
labelText: label,
items: items,
onChanged: (value) => function?.call(value),
dropdownTextFieldController: controller,
),
);
}
}
class CustomDropDown {
Widget simpleDropDown(
bool loading,
String label,
List<String> items,
TextEditingController controller,
Function? function, {
bool showSearch = false,
}) {
return SizedBox(
width: 300,
child: CustomDropdown(
labelText: label,
items: items,
onChanged: (value) => function?.call(value),
dropdownTextFieldController: controller,
),
);
}
}
class Components {
Widget customdd() {
return CustomDropDown().simpleDropDown(
isCompanyLoading,
'Company',
listOfCompany,
companyController,
comIdDd,
);
}
}
Use StatefulBuilder
widget to achieve this thing.
First make a widget function that return PopupMenuItem<String>
.
PopupMenuItem<String> _newPopupMenuItem() {
return PopupMenuItem<String>(
child: SizedBox(
height: 300,
width: 300,
child: StatefulBuilder(
builder: (context, setState) {
return Column(
children: <Widget>[
TextField(
controller: searchController,
decoration: const InputDecoration(
hintText: 'Search...',
suffixIcon: Icon(Icons.search),
),
onChanged: (query) {
setState(() {
filteredItems = widget.items.where((item) => item.toLowerCase().contains(query.toLowerCase())).toList();
});
},
),
if (filteredItems.isNotEmpty)
PopupMenuItem<String>(
child: SizedBox(
height: 200,
width: 300,
child: ListView.builder(
itemCount: filteredItems.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(filteredItems[index]),
onTap: () {
widget.dropdownTextFieldController.text = filteredItems[index];
widget.onChanged(filteredItems[index]);
Navigator.pop(context);
},
);
},
),
),
),
],
);
},
),
),
);
}
then use them into _showDropdown()
, like below:
showMenu(
...
items: <PopupMenuEntry<String>>[
// _buildSearchField(),
// if (filteredItems.isNotEmpty) _buildFilteredItems(),
_newPopupMenuItem()
],
...