Skip to main content

flutter_autocomplete 0.0.13 copy "flutter_autocomplete: ^0.0.13" to clipboard
flutter_autocomplete: ^0.0.13 copied to clipboard

A reusable Flutter Autocomplete widget inspired by MUI's React Autocomplete.

flutter_autocomplete #

Mobile-first autocomplete for Flutter with focused constructors:

  • AutocompleteField.single<T>()
  • AutocompleteField.multiple<T>()
  • AutocompleteField.async<T>()
  • AutocompleteField.asyncMultiple<T>()

The package is built for touch/virtual-keyboard UX, chip-based multiple mode, async loading, creatable options, visual grouping, and popup placement that stays stable through scrolling and viewport changes.

Installation #

dependencies:
  flutter_autocomplete: ^0.0.13
import 'package:flutter_autocomplete/flutter_autocomplete.dart';

Quick start #

AutocompleteField<String>.single(
  options: const ['Apple', 'Banana', 'Cherry'],
  getOptionLabel: (option) => option,
  decoration: const InputDecoration(
    labelText: 'Fruit',
    border: OutlineInputBorder(),
  ),
)

What's New In 0.0.13 #

  • AutocompleteBehaviorConfig.clearSelectionOnSelect turns a single-select field into a transient picker: each pick is reported through onChanged and the field returns to an empty state instead of displaying the selected label. See section 17 for the full flow.
  • Selecting an option no longer reopens the popup when the selection callback pushes a route, such as a loader dialog, and that route is dismissed.

Previously, in 0.0.12: creatable fields gained submit-driven creation through the keyboard action button, and every constructor gained textInputAction and onSubmitted.

Demo #

1) Single select (primitives) 2) Single select (objects) 3) Multiple chips
Single select demo Single select objects demo Multiple chips demo
4) Creatable 5) Grouped options 6) Async search-as-type
Creatable demo Grouped options demo Async search demo
7) Async combobox (load once) 8) Async pagination 9) Form validation
Async combobox demo Async pagination demo Form validation demo

Constructor cheat sheet #

Constructor Selection Data source Typical use
single One value Local list Plain dropdown-like autocomplete
multiple Many values Local list Chips/tag picker
async One value API/DB Search-as-type or combobox
asyncMultiple Many values API/DB Remote-backed chip picker

Use case cookbook #

1) Single select with primitive options #

AutocompleteField<String>.single(
  options: const ['Open', 'In Progress', 'Done'],
  value: status,
  onChanged: (value) => setState(() => status = value),
  getOptionLabel: (option) => option,
)

2) Single select with objects + custom equality #

Use isOptionEqualToValue when object identity may differ.

AutocompleteField<User>.single(
  options: users,
  value: selectedUser,
  onChanged: (value) => setState(() => selectedUser = value),
  getOptionLabel: (user) => user.fullName,
  isOptionEqualToValue: (option, value) => option.id == value.id,
)

3) Multiple chips with fixed values #

AutocompleteField<String>.multiple(
  options: const ['Owner', 'Reviewer', 'Approver', 'Observer'],
  values: selectedRoles,
  onChanged: (values) => setState(() => selectedRoles = values),
  getOptionLabel: (option) => option,
  chipConfig: const AutocompleteChipConfig<String>(
    fixedValues: ['Owner'],
    limitTags: 3,
    showHiddenCountChip: true,
  ),
  behaviorConfig: const AutocompleteBehaviorConfig(
    closeOnSelect: false,
    clearInputOnSelect: true,
  ),
  selectionConfig: const AutocompleteSelectionConfig<String>(
    selectedBackgroundColor: Color(0xFFE8F5E9),
    unselectedBackgroundColor: Color(0xFFFFF8E1),
  ),
)

4) Creatable options #

AutocompleteField<String>.multiple(
  options: const ['bug', 'feature', 'blocked'],
  values: tags,
  onChanged: (values) => setState(() => tags = values),
  getOptionLabel: (option) => option,
  creatableConfig: AutocompleteCreatableConfig<String>(
    createOption: (input) => input.trim().toLowerCase(),
    createLabel: (input) => 'Create tag "$input"',
  ),
)

Values can be created two ways:

  • tapping the Create tag "..." row in the popup;
  • submitting the input with the keyboard action button (Ok/Done on a virtual keyboard, Enter on a hardware one).

Submit-driven creation runs the same flow as the row, so createOption, clearInputOnCreate, minInputLength, and shouldShowCreateOption all apply. Nothing is created when the typed text already matches an option or the current selection.

The popup shows the create row only when no regular options are visible, while submit is not restricted that way: submitting fl while flutter is listed creates fl. Use shouldShowCreateOption to narrow creatable inputs, or createOnSubmit: false to keep creation tap-only:

creatableConfig: AutocompleteCreatableConfig<String>(
  createOption: (input) => input.trim().toLowerCase(),
  createOnSubmit: false,
),

Every constructor also accepts textInputAction to pick the keyboard action button and onSubmitted to observe submissions. onSubmitted runs after package-managed submit handling, whether or not a value was created.

AutocompleteField<String>.single(
  options: fruits,
  getOptionLabel: (option) => option,
  textInputAction: TextInputAction.done,
  onSubmitted: (input) => debugPrint('submitted: $input'),
)

5) Grouping (visual only) #

Grouping changes popup rendering only.

AutocompleteField<City>.single(
  options: cities,
  getOptionLabel: (city) => city.name,
  groupingConfig: AutocompleteGroupingConfig<City>(
    groupBy: (city) => city.country,
    sortGroups: true,
    stickyHeaders: true,
  ),
)

6) Async search-as-type #

AutocompleteField<String>.async(
  asyncConfig: AutocompleteAsyncConfig<String>(
    optionsBuilder: repository.searchCities,
    debounceDuration: const Duration(milliseconds: 250),
    minQueryLength: 2,
    reloadOnQueryChange: true,
  ),
  getOptionLabel: (option) => option,
)

Async fields display the complete list returned by optionsBuilder by default, so remote filtering, ranking, and alias matching are preserved.

7) Async combobox (load once, then local filtering) #

Useful when backend returns a bounded dataset.

AutocompleteField<String>.async(
  asyncConfig: AutocompleteAsyncConfig<String>(
    optionsBuilder: repository.fetchAllCities,
    loadOnFocus: true,
    reloadOnQueryChange: false,
    loadOnlyOnce: true,
    searchOnEmptyQuery: false,
    debounceDuration: Duration.zero,
  ),
  filterConfig: const AutocompleteFilterConfig<String>(enabled: true),
  getOptionLabel: (option) => option,
)

8) Async multiple (load once) #

AutocompleteField<String>.asyncMultiple(
  asyncConfig: AutocompleteAsyncConfig<String>(
    optionsBuilder: repository.fetchAllLabels,
    loadOnFocus: true,
    reloadOnQueryChange: false,
    loadOnlyOnce: true,
    searchOnEmptyQuery: false,
  ),
  filterConfig: const AutocompleteFilterConfig<String>(enabled: true),
  values: selectedLabels,
  onChanged: (values) => setState(() => selectedLabels = values),
  getOptionLabel: (option) => option,
  behaviorConfig: const AutocompleteBehaviorConfig(
    closeOnSelect: false,
    clearInputOnSelect: true,
  ),
)

9) External patching from parent/form state #

AutocompleteField<String>.asyncMultiple(
  asyncConfig: AutocompleteAsyncConfig<String>(
    optionsBuilder: repository.fetchAllLabels,
    loadOnFocus: true,
    reloadOnQueryChange: false,
    loadOnlyOnce: true,
  ),
  values: selectedLabels,
  onChanged: (values) {
    setState(() {
      selectedLabels
        ..clear()
        ..addAll(values);
    });
  },
  getOptionLabel: (option) => option,
)

// Later, patch externally without changing the widget key:
setState(() {
  selectedLabels
    ..clear()
    ..addAll(['Urgent', 'Backend']);
});

10) Async pagination #

AutocompleteField<String>.async(
  asyncConfig: AutocompleteAsyncConfig<String>(
    optionsBuilder: (_) async => const [],
    loadOnFocus: true,
    paginationConfig: AutocompleteAsyncPaginationConfig<String>(
      pageSize: 20,
      optionsPageBuilder: (query, page, pageSize) {
        return repository.fetchPage(query: query, page: page, size: pageSize);
      },
      showEndOfListIndicator: true,
    ),
  ),
  getOptionLabel: (option) => option,
)

11) Form validation and save #

final formKey = GlobalKey<FormState>();

Form(
  key: formKey,
  child: Column(
    children: [
      AutocompleteField<String>.single(
        options: const ['Low', 'Medium', 'High'],
        getOptionLabel: (option) => option,
        validator: (value) => value == null ? 'Priority is required' : null,
        onSaved: (value) => priority = value,
      ),
      AutocompleteField<String>.multiple(
        options: const ['UI', 'Backend', 'QA'],
        getOptionLabel: (option) => option,
        validator: (values) {
          if (values == null || values.isEmpty) {
            return 'Select at least one team';
          }
          return null;
        },
        onSaved: (values) => teams = values ?? <String>[],
      ),
    ],
  ),
)

12) Selected-value start adornment #

Use startAdornmentBuilder when you want a leading widget for the selected value while still keeping the real TextField mounted. This works well for async single fields that need to refocus, reload, or keep showing loading state.

AutocompleteField<String>.async(
  asyncConfig: AutocompleteAsyncConfig<String>(
    optionsBuilder: repository.fetchAllLabels,
    loadOnFocus: true,
    reloadOnQueryChange: false,
    loadOnlyOnce: true,
  ),
  value: selectedLabel,
  onChanged: (value) => setState(() => selectedLabel = value),
  getOptionLabel: (option) => option,
  renderingConfig: AutocompleteRenderingConfig<String>(
    startAdornmentBuilder: (context, value, label) {
      return Padding(
        padding: const EdgeInsets.only(right: 8),
        child: Chip(label: Text(label)),
      );
    },
  ),
)

13) Custom text style #

Use textStyle when you want to style the input text for one field only without changing the app-wide Theme.

AutocompleteField<String>.single(
  options: const ['Apple', 'Banana', 'Cherry'],
  value: selectedFruit,
  onChanged: (value) => setState(() => selectedFruit = value),
  getOptionLabel: (option) => option,
  textStyle: const TextStyle(
    fontSize: 18,
    fontWeight: FontWeight.w700,
    color: Colors.deepOrange,
  ),
  decoration: const InputDecoration(
    labelText: 'Styled fruit',
    border: OutlineInputBorder(),
  ),
)

14) Custom rendering #

AutocompleteField<String>.multiple(
  options: const ['Apple', 'Banana', 'Cherry'],
  getOptionLabel: (option) => option,
  renderingConfig: AutocompleteRenderingConfig<String>(
    optionBuilder: (context, option) {
      return ListTile(
        title: Text(option.label),
        trailing: option.isSelected
            ? const Icon(Icons.check, size: 18)
            : null,
      );
    },
  ),
)

15) Disabled options #

AutocompleteField<String>.single(
  options: const ['Open', 'Closed', 'Archived'],
  getOptionLabel: (option) => option,
  isOptionDisabled: (option) => option == 'Archived',
)

16) Disabled vs read-only interaction states #

Use enabled: false when the field should be disabled and excluded from normal form interaction. Use readOnly: true when it should keep enabled styling but block edits, popup selection changes, clearing, and chip deletion.

AutocompleteField<String>.multiple(
  options: const ['Apple', 'Banana', 'Cherry'],
  values: const ['Apple', 'Banana'],
  onChanged: (_) {},
  readOnly: true,
  getOptionLabel: (option) => option,
  decoration: const InputDecoration(
    labelText: 'Locked fruit list',
    helperText: 'Users can review the values, but cannot change them.',
    border: OutlineInputBorder(),
  ),
)

17) Transient picker (do not keep the selection) #

Use clearSelectionOnSelect when the field is an action dispatcher rather than a value holder: search, pick, act on the pick, and leave the field empty for the next search.

AutocompleteField<Employee>.async(
  asyncConfig: AutocompleteAsyncConfig<Employee>(
    optionsBuilder: repository.searchEmployees,
  ),
  getOptionLabel: (employee) => employee.fullName,
  behaviorConfig: const AutocompleteBehaviorConfig(
    clearSelectionOnSelect: true,
  ),
  onChanged: (employee) {
    if (employee == null) {
      return;
    }
    setState(() => _assignees = [..._assignees, employee]);
  },
  decoration: const InputDecoration(
    labelText: 'Add assignee',
    border: OutlineInputBorder(),
  ),
)

What happens on each pick:

  • onChanged fires once with the picked option;
  • the selected value and the input text are discarded, so no label stays in the field and no option renders as selected;
  • the popup closes, because closeOnSelect defaults to true;
  • form state is reset too, so validator and onSaved observe null instead of the transient pick;
  • the reset emits no extra onChanged(null), so the callback never sees a spurious clear.

Because nothing stays selected, the same option can be picked repeatedly. Add blurOnSelect: true to also dismiss the keyboard after each pick, or closeOnSelect: false to keep the popup open for rapid repeated picks.

Running async work in onChanged is supported, including pushing a modal loader or confirmation route. Dismissing that route restores focus to the field, and the popup the selection closed stays closed instead of reopening.

Do not feed the picked option back through value. A parent that stores the pick and re-asserts it as value is treated as external patching (see section 9) and wins over the automatic reset, leaving the label in the field. Act on the pick inside onChanged and keep value unset, or use it only to seed an initial selection.

The flag applies to single-selection modes (.single and .async). Multiple mode renders selections as chips and ignores it.

Async behavior reference #

AutocompleteAsyncConfig gives these common patterns:

  • Search-as-type: reloadOnQueryChange: true; each builder response is shown unchanged by default.
  • Load on focus: loadOnFocus: true
  • One request only: loadOnlyOnce: true
  • Ignore whitespace-only input: searchOnEmptyQuery: false
  • Local filtering after first load: reloadOnQueryChange: false plus AutocompleteFilterConfig(enabled: true)

Useful config groups #

  • AutocompleteBehaviorConfig: focus/open/close/clear behavior, including clearSelectionOnSelect for transient pickers that must not retain a value.
  • AutocompleteFilterConfig: opt into client-side matching for async fields, customize matching, or set enabled: false to show source options unchanged.
  • AutocompleteSelectionConfig: keep selected rows visible, customize selected and unselected row backgrounds, and control indicator behavior.
  • AutocompleteChipConfig: chip layout, fixed values, hidden count, max chip area height.
  • AutocompletePopupConfig: popup size/surface styling.
  • AutocompleteRenderingConfig: option/selected/loading/empty custom builders.

Example app #

A full runnable showcase exists in example/lib/main.dart.

It includes:

  1. Single and multiple local constructors.
  2. Creatable and grouped flows.
  3. Async search-as-type.
  4. Async load-once combobox and async multiple.
  5. Async pagination.
  6. Form validation and save flows.
  7. Selected-value start adornment examples.
  8. Per-field input text styling examples.
  9. Transient picker that does not retain the selection.
  10. Disabled and read-only interaction state examples.

Accessibility and platform scope #

  • Uses Flutter text/chip/tap semantics.
  • Validation errors are rendered via InputDecoration.
  • Mobile-first behavior.
2
likes
160
points
398
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

A reusable Flutter Autocomplete widget inspired by MUI's React Autocomplete.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_autocomplete