app_select_fields 0.0.5 copy "app_select_fields: ^0.0.5" to clipboard
app_select_fields: ^0.0.5 copied to clipboard

Material 3 single- and multi-select form fields for Flutter. Opens a draggable bottom sheet with search and lazy pagination — themeable, form-integrated, no extra dependencies.

app_select_fields #

pub package pub points pub likes CI License: MIT SDK: Flutter Platform

A Flutter dropdown / select field package: labelled, form-integrated single-select and multi-select inputs that open a draggable, root-level bottom sheet picker with built-in search, pagination and server-search hooks. Styled from the ambient Material 3 Theme by default — no configuration required, and no extra dependencies.

Useful as a Material dropdown alternative, multi-select chip picker, tag selector, or searchable option list in any Form.

Both fields closed AppSingleSelect sheet open AppMultiSelect sheet open

Picking options in the bottom sheet

Features #

  • AppSingleSelect<T> — radio-style picker, optional "Clear" action.
  • AppMultiSelect<T> — checkbox picker with a live selection-count chip.
  • Built-in search box, with optional debounced server-search callback.
  • Optional lazy pagination (onLoadMore, hasMore, loadingMore).
  • Works inside a Form via the standard validator API.
  • AppSelectStyle to override radii, animation timing, text styles and the built-in English copy (search hint, button labels, "N selected").

Usage #

import 'package:app_select_fields/app_select_fields.dart';

AppSingleSelect<String>(
  label: 'Country',
  hint: 'Select a country',
  options: const [
    SelectOption(label: 'United States', value: 'US'),
    SelectOption(label: 'United Kingdom', value: 'UK'),
  ],
  value: selectedCountry,
  onChanged: (value) => setState(() => selectedCountry = value),
);

AppMultiSelect<String>(
  label: 'Tags',
  options: const [
    SelectOption(label: 'Urgent', value: 'urgent'),
    SelectOption(label: 'Follow up', value: 'follow_up'),
  ],
  values: selectedTags,
  onChanged: (values) => setState(() => selectedTags = values),
);

Backend lazy loading / virtual scroll #

For large or server-backed option lists, pass onLoadMore (infinite scroll, fired near the end of the list) and/or onSearchChanged (debounced server-search). The sheet's list is already a virtualized ListView.builder, so only visible rows are built regardless of list size — you just feed it pages as they arrive:

AppSingleSelect<String>(
  label: 'City',
  options: _cityOptions, // grows as pages arrive
  value: _city,
  hasMore: _cityHasMore,
  loadingMore: _cityLoadingMore,
  onLoadMore: () => _fetchNextPage(), // append results, update hasMore
  onSearchChanged: (query) => _fetchFirstPage(query), // replace options
  onChanged: (value) => setState(() => _city = value),
);

See example/ for a runnable demo of both widgets, including a fake paginated "backend" for AppSingleSelect and a custom AppSelectStyle.

Custom decoration, styles & templates #

Every field on both widgets below is optional. inputDecorationStyle gets the package's fully-resolved InputDecoration to tweak or replace; inputValueStyle/hintStyle/inputLabelStyle override individual text styles (each takes precedence over the equivalent AppSelectStyle field); selectedInputTemplate and optionTemplate replace the default Text with a custom widget, both called with the option's label and value:

AppSingleSelect<String>(
  label: 'Country',
  options: _countries, // SelectOption<String>, value is a country code
  value: selectedCountry,
  onChanged: (value) => setState(() => selectedCountry = value),
  inputLabelStyle: const TextStyle(fontWeight: FontWeight.bold, color: Colors.indigo),
  hintStyle: const TextStyle(fontStyle: FontStyle.italic, color: Colors.grey),
  inputValueStyle: const TextStyle(fontWeight: FontWeight.w600),
  inputDecorationStyle: (decoration) => decoration.copyWith(prefixIcon: const Icon(Icons.public)),
  // Trigger field's selected-value display:
  selectedInputTemplate: (context, label, value) => Row(
    mainAxisSize: MainAxisSize.min,
    children: [Text(flagEmoji(value)), const SizedBox(width: 8), Text(label)],
  ),
  // Each row in the option sheet:
  optionTemplate: (context, label, value) => Row(
    children: [Text(flagEmoji(value)), const SizedBox(width: 12), Text(label)],
  ),
  // The sheet's Cancel button — the only action button on a single-select
  // sheet, since tapping an option confirms immediately:
  cancelButtonLabel: 'Dismiss',
  cancelButtonStyle: OutlinedButton.styleFrom(foregroundColor: Colors.indigo),
);

AppMultiSelect supports the same six properties, plus displaySelectedCount (default true) to hide the sheet's "N selected" summary chip. Its selectedInputTemplate is called once per selected option and the results are laid out in a Wrap in the trigger, in place of the default comma-joined text — falling back to that text once the selection count passes maxSelectedLabel:

AppMultiSelect<String>(
  label: 'Tags',
  options: _tags,
  values: selectedTags,
  onChanged: (values) => setState(() => selectedTags = values),
  displaySelectedCount: false, // hide the "N selected" chip in the sheet
  selectedInputTemplate: (context, label, value) => Chip(label: Text(label)), // one per selected tag
  optionTemplate: (context, label, value) => Row(
    children: [Icon(Icons.circle, size: 10, color: tagColor(value)), const SizedBox(width: 10), Text(label)],
  ),
  // The sheet's Cancel / confirm (OK) buttons:
  cancelButtonLabel: 'Dismiss',
  cancelButtonStyle: FilledButton.styleFrom(backgroundColor: Colors.grey.shade200),
  confirmButtonLabel: 'Apply',
  confirmButtonStyle: FilledButton.styleFrom(backgroundColor: Colors.teal),
);

AppSingleSelect<T> properties #

Property Type Default Description
label String required Text shown above the field.
options List<SelectOption<T>> required Options offered in the sheet.
hint String? null Placeholder shown while nothing is selected.
value T? null Currently selected value; the field is fully controlled by the caller.
onChanged ValueChanged<T?>? null Called with the newly picked value when the sheet closes.
enabled bool true Whether the field accepts interaction.
allowClear bool false Shows a Clear button beside Cancel while value is non-null; calls onChanged(null).
loading bool false Shows a spinner in place of the chevron and disables the field.
required bool false Adds a red * to the label. Purely visual — pair with errorText/validator.
errorText String? null Caller-driven validation message; takes precedence over validator.
validator String? Function(T?)? null Form validator run against the selected value.
hasMore bool false Whether a further page of options exists server-side. Ignored unless onLoadMore is set.
loadingMore bool false Shows a loading row while the next page is fetched. Ignored unless onLoadMore is set.
onLoadMore VoidCallback? null Fires when the sheet's list is scrolled past ~85%. Enables lazy pagination.
onSearchChanged ValueChanged<String>? null Debounced (~400ms) server-search callback; disables local filtering when set.
displayLabel bool true Whether label renders above the field, e.g. when a surrounding layout already names it.
style AppSelectStyle AppSelectStyle() Visual/copy overrides — see below.
noRecordWidget Widget? null Shown centered in the sheet's list area when options is empty. Defaults to a "No Records Found !" message.
inputDecorationStyle InputDecoration Function(InputDecoration)? null Customizes the trigger's resolved InputDecoration.
inputValueStyle TextStyle? null Overrides the selected value's text style. Takes precedence over AppSelectStyle.inputValueStyle.
hintStyle TextStyle? null Overrides the placeholder's text style. Takes precedence over AppSelectStyle.hintStyle.
inputLabelStyle TextStyle? null Overrides the field label's text style. Takes precedence over AppSelectStyle.inputLabelStyle.
errorTextStyle TextStyle? null Overrides the validation message's text style. Takes precedence over AppSelectStyle.errorTextStyle.
selectedInputTemplate Widget Function(BuildContext, String label, T value)? null Builds the trigger's selected-value display in place of the default Text.
optionTemplate Widget Function(BuildContext, String label, T value)? null Builds each row's content in the option sheet in place of the default Text.
cancelButtonLabel String? null Overrides the sheet's Cancel button label. Takes precedence over AppSelectStyle.cancelLabel.
cancelButtonStyle ButtonStyle? null Overrides the sheet's Cancel button style.

AppMultiSelect<T> properties #

Property Type Default Description
label String required Text shown above the field.
options List<SelectOption<T>> required Options offered in the sheet.
hint String? null Placeholder shown while nothing is selected.
values List<T> [] Currently selected values; the field is fully controlled by the caller.
onChanged ValueChanged<List<T>>? null Called with the confirmed values when the sheet's OK button is tapped.
maxSelectedLabel int? null Once the selected count exceeds this, the trigger shows "N selected" instead of the joined labels.
enabled bool true Whether the field accepts interaction.
errorText String? null Caller-driven validation message; takes precedence over validator.
validator String? Function(List<T>?)? null Form validator run against the selected values.
style AppSelectStyle AppSelectStyle() Visual/copy overrides — see below.
noRecordWidget Widget? null Shown centered in the sheet's list area when options is empty. Defaults to a "No Records Found !" message.
displaySelectedCount bool true Whether the sheet's "N selected" summary chip shows above the option list.
inputDecorationStyle InputDecoration Function(InputDecoration)? null Customizes the trigger's resolved InputDecoration.
inputValueStyle TextStyle? null Overrides the selected values' text style. Takes precedence over AppSelectStyle.inputValueStyle.
hintStyle TextStyle? null Overrides the placeholder's text style. Takes precedence over AppSelectStyle.hintStyle.
inputLabelStyle TextStyle? null Overrides the field label's text style. Takes precedence over AppSelectStyle.inputLabelStyle.
errorTextStyle TextStyle? null Overrides the validation message's text style. Takes precedence over AppSelectStyle.errorTextStyle.
selectedInputTemplate Widget Function(BuildContext, String label, T value)? null Builds each selected option's display, laid out in a Wrap in the trigger, in place of the default joined text. Falls back to the joined text once the count passes maxSelectedLabel.
optionTemplate Widget Function(BuildContext, String label, T value)? null Builds each row's content in the option sheet in place of the default Text.
cancelButtonLabel String? null Overrides the sheet's Cancel button label. Takes precedence over AppSelectStyle.cancelLabel.
cancelButtonStyle ButtonStyle? null Overrides the sheet's Cancel button style.
confirmButtonLabel String? null Overrides the sheet's confirm (OK) button label. Takes precedence over AppSelectStyle.okLabel.
confirmButtonStyle ButtonStyle? null Overrides the sheet's confirm (OK) button style.

SelectOption<T> #

Property Type Description
label String Human-readable text shown in the list and trigger field.
value T The underlying value reported back via onChanged.

AppSelectStyle #

Every field is optional and falls back to a Material 3 default or an English string.

Property Type Default Description
borderRadius double 12 Corner radius of the trigger field's border.
sheetBorderRadius double 20 Corner radius of the sheet's top corners.
chevronAnimationDuration Duration 250ms How long the trigger's chevron takes to flip. Skipped when reduce-motion is on.
checkAnimationDuration Duration 250ms How long a radio dot / checkbox takes to animate. Skipped when reduce-motion is on.
inputLabelStyle TextStyle? textTheme.labelMedium Overrides the caption above the field. Overridden by the widget's own inputLabelStyle.
hintStyle TextStyle? textTheme.bodyMedium Overrides the placeholder text style. Overridden by the widget's own hintStyle.
inputValueStyle TextStyle? textTheme.bodyMedium Overrides the selected value's text style. Overridden by the widget's own inputValueStyle.
inputDecorationStyle InputDecoration Function(InputDecoration)? null Customizes the trigger's resolved InputDecoration. Overridden by the widget's own inputDecorationStyle.
searchHint String 'Search' Placeholder for the sheet's search field.
cancelLabel String 'Cancel' Label of the button that closes the sheet without changes.
clearLabel String 'Clear' Label of the button that clears the current selection.
okLabel String 'OK' Label of the button that confirms a multi-select's choices.
cancelButtonStyle ButtonStyle? null Style of the Cancel button. Overridden by the widget's own cancelButtonStyle.
selectedCountLabel String Function(int)? '$count selected' Builds the "N selected" summary text.
errorTextStyle TextStyle? small colorScheme.error caption Overrides the validation message's text style. Overridden by the widget's own errorTextStyle.

Every field the widgets also expose directly (inputLabelStyle, hintStyle, inputValueStyle, inputDecorationStyle, cancelButtonStyle, errorTextStyle) can be set on either the widget or style — the widget-level one wins when both are set, so style is a good place for values shared across many fields while the widget-level params stay for one-off overrides:

AppSingleSelect<String>(
  label: 'Country',
  options: _countries,
  value: selectedCountry,
  onChanged: (value) => setState(() => selectedCountry = value),
  style: const AppSelectStyle(
    inputLabelStyle: TextStyle(fontWeight: FontWeight.bold, color: Colors.indigo),
    hintStyle: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey),
    inputValueStyle: TextStyle(fontWeight: FontWeight.w600),
  ),
);

Author #

Maintained by @TheTourist1502.

2
likes
160
points
208
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Material 3 single- and multi-select form fields for Flutter. Opens a draggable bottom sheet with search and lazy pagination — themeable, form-integrated, no extra dependencies.

Repository (GitHub)
View/report issues

Topics

#dropdown #select #form #bottom-sheet #material

License

MIT (license)

Dependencies

flutter

More

Packages that depend on app_select_fields