searchable_dropdown_form_field 0.2.0
searchable_dropdown_form_field: ^0.2.0 copied to clipboard
A customizable, searchable Flutter dropdown with form validation and generic value support.
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.2.0
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>.async(
value: selectedUser,
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),
);
Infinite pagination #
Use asyncPageLoader for large datasets. Page numbers start at 1, the next
page loads automatically near the end of the list, and returning an empty list
stops pagination. No pagination controller is required.
CustomSearchableDropdown<User>.async(
popupMode: DropdownPopupMode.bottomSheet,
asyncPageLoader: (query, page) async {
final users = await api.searchUsers(query: query, page: page);
return users
.map((user) => DropdownItem(label: user.name, value: user))
.toList(); // Return [] on the final page.
},
valueEquals: (first, second) => first.id == second.id,
onChanged: (user) => setState(() => selectedUser = user),
);
Use either asyncItemsLoader or asyncPageLoader, not both. Search debounce,
first-page loading, next-page loading, stale responses, and retry behavior are
managed internally.
Async errors and retry #
The default error state includes a Retry button. Customize it only when needed:
asyncErrorBuilder: (context, error, retry) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Could not load results: $error'),
TextButton(onPressed: retry, child: const Text('Try again')),
],
),
loadingWidget: const CircularProgressIndicator(),
Popup list customization #
CustomSearchableDropdown<String>(
items: countries,
listPadding: const EdgeInsets.symmetric(vertical: 8),
separatorBuilder: (context, index) => const Divider(height: 1),
listPhysics: const BouncingScrollPhysics(),
itemPadding: const EdgeInsets.symmetric(horizontal: 16),
popupWidth: 420,
onChanged: (value) => setState(() => selectedCountry = value),
);
An optional popupScrollController is also available when the surrounding app
needs to observe or control the popup list.
Accessibility and keyboard navigation #
Keyboard support works automatically while the popup search field is focused:
- ↓ and ↑ highlight items.
- Enter selects the highlighted item.
- Escape closes the popup.
Dropdowns, search fields, list items, selection state, and async errors expose
screen-reader semantics. Use semanticLabel and searchSemanticLabel when the
visible labels do not provide enough context.
Bottom sheets automatically resize and animate above the software keyboard; no
extra MediaQuery or padding code is required.
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. |
asyncPageLoader |
Loads numbered pages and enables automatic infinite scrolling. |
asyncErrorBuilder |
Customizes async errors while retaining the supplied retry callback. |
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. |
listPadding / separatorBuilder |
Customizes popup list spacing and separators. |
semanticLabel |
Provides a custom screen-reader label for the field. |
See the complete example for synchronous and asynchronous dropdowns.