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
  • Form validation, saving, and auto-validation
  • Controlled values through value and onChanged
  • 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),
);

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(),
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.

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.

Libraries

searchable_dropdown_form_field
A searchable, generic dropdown form field for Flutter.