Searchable Dropdown Form Field
A customizable, generic Flutter dropdown with local or asynchronous search,
form validation, dialog and bottom-sheet presentation, and full Material
InputDecoration support.
Features
- Generic values with
DropdownItem<T> - Fast local filtering with optional custom matching
- Debounced asynchronous search for APIs and databases
- Material dialog or modal bottom-sheet presentation
Formvalidation, saving, and auto-validation- Controlled values through
valueandonChanged - Clear button and selected-item checkmark
- Custom value equality for model objects
- Custom field, popup-item, title, search-input, and empty-state widgets
- Theme-aware styling with no runtime dependency beyond Flutter
Installation
Add the package to your pubspec.yaml:
dependencies:
searchable_dropdown_form_field: ^0.1.1
Then import it:
import 'package:searchable_dropdown_form_field/searchable_dropdown_form_field.dart';
Basic usage
The default behavior opens a searchable Material dialog. Existing synchronous
dropdowns only need an item list and an onChanged callback.
String? selectedCountry;
CustomSearchableDropdown<String>(
value: selectedCountry,
hintText: 'Choose a country',
dialogTitle: 'Select a country',
items: const [
DropdownItem(label: 'Bangladesh', value: 'BD'),
DropdownItem(label: 'India', value: 'IN'),
DropdownItem(label: 'Nepal', value: 'NP'),
],
onChanged: (value) {
setState(() => selectedCountry = value);
},
);
Form validation
CustomSearchableDropdown extends FormField<T>, so it works with normal
Flutter forms.
final formKey = GlobalKey<FormState>();
Form(
key: formKey,
child: CustomSearchableDropdown<String>(
items: countries,
hintText: 'Country',
validator: (value) => value == null ? 'Please select a country' : null,
onSaved: (value) => selectedCountry = value,
),
);
Field and popup customization
The closed field and popup search input use separate InputDecoration
objects. Text styles, popup shape, padding, icons, and builders can also be
configured independently.
CustomSearchableDropdown<String>(
value: selectedCountry,
items: countries,
hintText: 'Choose a country',
decoration: InputDecoration(
labelText: 'Country',
prefixIcon: const Icon(Icons.public),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
searchDecoration: InputDecoration(
hintText: 'Type a country name or code',
prefixIcon: const Icon(Icons.travel_explore),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
textStyle: const TextStyle(fontWeight: FontWeight.w600),
dialogTitleStyle: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
searchTextStyle: const TextStyle(fontSize: 16),
itemTextStyle: const TextStyle(fontSize: 16),
dialogShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
showClearButton: true,
showSelectedCheckmark: true,
onChanged: (value) => setState(() => selectedCountry = value),
);
For complete control, use fieldBuilder, popupItemBuilder,
dialogTitleWidget, or emptyResultWidget.
Custom search matching
Local search matches item labels case-insensitively by default. Use
searchMatcher to search additional fields such as country codes.
CustomSearchableDropdown<String>(
items: countries,
searchMatcher: (item, query) {
final search = query.toLowerCase();
return item.label.toLowerCase().contains(search) ||
item.value.toLowerCase().contains(search);
},
onChanged: (value) => setState(() => selectedCountry = value),
);
Asynchronous search
Use asyncItemsLoader when results come from an API or database. Calls are
debounced, stale responses are ignored, and loading, error, retry, and empty
states are handled automatically. The selected async item's label remains
visible after the popup closes, even when the original items list is empty.
CustomSearchableDropdown<User>(
value: selectedUser,
items: const [],
hintText: 'Search users',
popupMode: DropdownPopupMode.bottomSheet,
searchDebounceDuration: const Duration(milliseconds: 300),
asyncItemsLoader: (query) async {
final users = await api.searchUsers(query);
return users
.map((user) => DropdownItem(label: user.name, value: user))
.toList();
},
valueEquals: (first, second) => first.id == second.id,
onChanged: (user) => setState(() => selectedUser = user),
);
Popup presentation
Dialog presentation is the default:
popupMode: DropdownPopupMode.dialog
For a mobile-friendly modal sheet:
popupMode: DropdownPopupMode.bottomSheet
Selection controls
CustomSearchableDropdown<String>(
value: selectedCountry,
items: countries,
showClearButton: true,
clearIcon: const Icon(Icons.close),
showSelectedCheckmark: true,
selectedIcon: const Icon(Icons.check_circle),
onCleared: () => debugPrint('Selection cleared'),
onChanged: (value) => setState(() => selectedCountry = value),
);
Main options
| Option | Purpose |
|---|---|
items |
Local dropdown items. Pass an empty list for async-only search. |
value |
Currently selected value. |
onChanged |
Called after selecting or clearing a value. |
validator / onSaved |
Standard FormField<T> integration. |
decoration |
Decoration for the closed dropdown field. |
searchDecoration |
Decoration for the popup search input. |
searchMatcher |
Custom local filtering logic. |
asyncItemsLoader |
Loads items asynchronously for a query. |
valueEquals |
Compares model values that do not implement equality. |
popupMode |
Displays a dialog or modal bottom sheet. |
showClearButton |
Allows the current selection to be cleared. |
fieldBuilder |
Builds custom closed-field content. |
popupItemBuilder |
Builds custom popup item content. |
See the complete example for synchronous and asynchronous dropdowns.
Libraries
- searchable_dropdown_form_field
- A searchable, generic dropdown form field for Flutter.