multiselect_field 2.3.0 copy "multiselect_field: ^2.3.0" to clipboard
multiselect_field: ^2.3.0 copied to clipboard

A flexible dropdown field supporting single/multiple selection modes, styles, titles, etc


MultiSelectField #

multiselect_field

License: MIT text_field_validation Dart 3 Flutter 3.10

MultiSelectField is a custom implementation of a multi-select field for Flutter applications. This library provides a flexible and highly configurable solution for projects that require native multi-selection, real-time text filtering, and more advanced features.

Features #

  • 4 display modes: Standard (dropdown), Chip (compact), BottomSheet (modal), and Drawer (panel).
  • Native multi-selection: Single or multiple selection without additional packages.
  • closeOnSelect: Automatically close the menu after selection (all variants).
  • onChanged callback: Simple callback that fires only on user interaction, never on default data.
  • FieldWidth: Control field width — fit content, fixed pixels, or full width (Standard variant).
  • iconSpacing: Configurable gap between label and dropdown icon (Standard variant).
  • Advanced features: Real-time text filtering, select all, group titles, chips display, programmatic control via MultiSelectKeyStore.
  • Independence: Zero third-party dependencies.

Library #

Check out the library on pub.dev.

Installation #

Add the dependency to your pubspec.yaml file:

dependencies:
  multiselect_field: ^2.3.0

Then, install the dependencies using:

flutter pub get

Or #

flutter pub add multiselect_field

Usage #

Basic Example #

import 'package:multiselect_field/multiselect_field.dart';

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiSelectField<Car>(
      data: () => [
        Choice<Car>(null, 'Ferrari'),  // Group title
        Choice<Car>('2', '488 GTB', metadata: Car(103, 27.500, 2015)),
        Choice<Car>('3', '458 Italia', metadata: Car(99, 22.000, 2009)),
        Choice<Car>('4', 'Portofino', metadata: Car(105, 31.000, 2017)),
        Choice<Car>('5', 'California T', metadata: Car(102, 25.000, 2016)),
        Choice<Car>('6', 'F8 Tributo', metadata: Car(104, 30.000, 2019)),
      ],
      onSelect: (selectedItems, isFromDefault) {
        print(selectedItems.map((item) => item.value).toList());
      },
      useTextFilter: true,
    );
  }
}

Properties #

  • data: List<Choice<T>> Function(). Function that returns a list of Choice elements for selection.
  • onSelect: Function(List<Choice<T>> ChoiceList). Callback invoked when items are selected.
  • title: Widget Function(bool isEmpty)?. Optional widget that displays a title, depending on whether the selection is empty.
  • footer: Widget?. Optional widget displayed at the bottom.
  • singleSelectWidget: Widget Function(Choice<T> ChoiceList)?. Optional widget for displaying a single selected item.
  • multiSelectWidget: Widget Function(Choice<T> ChoiceList)?. Optional widget for displaying multiple selected items.
  • defaultData: List<Choice<T>>?. Optional function that returns the default list of selected items.
  • singleSelection: bool. Defines if the widget should allow only a single selection. Defaults to false.
  • useTextFilter: bool. Enables or disables real-time text filtering.
  • decoration: Decoration?. Custom decoration for the widget.
  • padding: EdgeInsetsGeometry?. Defines the internal padding of the widget.
  • textStyleSingleSelection: TextStyle?. Text style for single selection.
  • labelBuilder: Widget Function(String label)?. Optional builder that fully overrides how the static label is rendered. See Custom label rendering.
  • scrollbarConfig: ScrollbarConfig. Modify the size, color, margins, etc.

Custom label rendering #

When staticLabel: true, the field shows label regardless of the current selection. By default this is a plain Text(label). Pass labelBuilder to fully control how it renders, or use the included MultiSelectLabel widget for common presets via the LabelType enum.

MultiSelectField<String>(
  label: 'NUI Marketplace North America',
  staticLabel: true,
  singleSelection: true,
  labelBuilder: (label) => MultiSelectLabel(
    label: label,
    type: LabelType.wrap,      // see LabelType options below
    maxLines: 2,
    style: Theme.of(context).textTheme.titleSmall,
  ),
  data: () => choices,
)

LabelType options:

  • LabelType.line (default): single line, no wrap.
  • LabelType.wrap: wraps up to maxLines (default 2) with ellipsis. Width collapses to the longest rendered line so trailing widgets (e.g. dropdown arrows) sit right next to the text instead of being pushed to the parent's max width.
  • LabelType.overflow: single line truncated with ellipsis when the parent constrains width.

You can also supply a fully custom widget when the presets do not fit:

labelBuilder: (label) => Row(
  mainAxisSize: MainAxisSize.min,
  children: [
    const Icon(Icons.filter_alt, size: 14),
    const SizedBox(width: 4),
    Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
  ],
),

Advanced Example with scrollbarConfig #

MultiSelectField<String>(
  data: () => [
      Choice(key: 'apple', value: 'Apple'),
      Choice(key: 'banana', value: 'Banana'),
      Choice(key: 'orange', value: 'Orange'),
    ],
    scrollbarConfig: ScrollbarConfig(
      visible: true,
      themeData: ScrollbarThemeData(
      thickness: WidgetStateProperty.all(10.0),
      thumbColor: WidgetStateProperty.all(Colors.orange),
      trackColor: WidgetStateProperty.all(Colors.grey.withValues(alpha: 0.2)),
      radius: const Radius.circular(5.0),
      thumbVisibility: WidgetStateProperty.all(true),
      trackVisibility: WidgetStateProperty.all(true),
    ),
  ),
  defaultData: [Choice(key: 'banana', value: 'Banana')],
  ///[isFromDefault] Helps to know if current selected element is from default data or not.
  onSelect: (selectedItems, isFromDefaultData) {
    // Update selection state
  },
  title: (isEmpty) => Text(isEmpty ? 'Select a fruit' : 'Selected fruits'),
  singleSelection: false,
  useTextFilter: true,
  decoration: BoxDecoration(
    border: Border.all(color: Colors.blue),
    borderRadius: BorderRadius.circular(5),
  ),
  padding: EdgeInsets.all(10),
  multiSelectWidget: (item) => Chip(
    label: Text(item.value),
    onDeleted: () {
      // Remove selected item
    },
  ),
);

Chip Variant - Compact Dropdown #

Use MultiSelectField.chip() for space-constrained areas like filter bars:

MultiSelectField<String>.chip(
  label: 'Status',
  chipSize: ChipSize.small,
  chipStyle: ChipStyle.withColor(Colors.blue),
  singleSelection: true,
  data: () => [
    Choice(null, 'Priority'),  // Group title
    Choice('high', 'High'),
    Choice('medium', 'Medium'),
    Choice('low', 'Low'),
  ],
  onSelect: (selected, _) {
    print(selected.map((c) => c.key).toList());
  },
)

ChipSize Options

ChipSize.extraSmall  // Minimal footprint
ChipSize.small       // Compact
ChipSize.medium      // Default
ChipSize.large       // Prominent
ChipSize.extraLarge  // Maximum visibility

// Or create custom sizes:
const mySize = ChipSize(
  fontSize: 12,
  iconSize: 16,
  padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
  borderRadius: 12,
  spacing: 4,
);

Custom Menu Content

MultiSelectField<String>.chip(
  label: 'Date',
  menuContent: Column(
    children: [
      ListTile(title: Text('Today')),
      ListTile(title: Text('This week')),
      Divider(),
      CalendarDatePicker(...),
    ],
  ),
)

Bottom Sheet Variant #

Use MultiSelectField.bottomSheet() to open selections in a modal bottom sheet:

MultiSelectField<String>.bottomSheet(
  label: 'Categories',
  data: () => [
    Choice(null, 'Fruits'),  // Group title
    Choice('apple', 'Apple'),
    Choice('banana', 'Banana'),
    Choice(null, 'Vegetables'),
    Choice('carrot', 'Carrot'),
  ],
  bottomSheetStyle: const BottomSheetStyle(
    maxHeightFraction: 0.5,
    showDragHandle: true,
    borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
  ),
  menuHeader: const Padding(
    padding: EdgeInsets.all(16),
    child: Text('Pick your items', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
  ),
  onSelect: (selected, _) {
    print(selected.map((c) => c.value).toList());
  },
)

BottomSheetStyle Options

const BottomSheetStyle(
  maxHeightFraction: 0.6,     // 60% of screen height (default)
  fixedHeight: 400,           // Fixed height in pixels (overrides fraction)
  backgroundColor: Colors.white,
  barrierColor: Colors.black54,
  borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
  showDragHandle: true,       // Default: true
  dragHandleColor: Colors.grey,
  dragHandleWidth: 40,
)

Search in BottomSheet

Enable text filtering inside the bottom sheet with useTextFilter. Use searchMinHeight to control the minimum height of the filtered area:

MultiSelectField<String>.bottomSheet(
  label: 'Categories',
  data: () => choices,
  useTextFilter: true,
  searchMinHeight: 400,  // Ensures enough space for filtered results
  onSelect: (selected, _) => print(selected),
)

Drawer Variant #

Two modes: Scaffold drawer (with scaffoldKey) or Overlay drawer (without it).

Scaffold Mode

Place MultiSelectField.drawer() inside your Scaffold's drawer. Open/close it from anywhere with MultiSelectKeyStore.

final scaffoldKey = GlobalKey<ScaffoldState>();
final filterStore = MultiSelectKeyStore.of<String>('myFilter');
filterStore.registerScaffold(scaffoldKey);

Scaffold(
  key: scaffoldKey,
  endDrawer: Drawer(
    child: MultiSelectField<String>.drawer(
      label: 'Filter',
      keyDrawer: 'myFilter',
      scaffoldKey: scaffoldKey,
      data: () => [
        Choice(null, 'Condition'),  // Group title
        Choice('new', 'New'),
        Choice('used', 'Used'),
        Choice('refurbished', 'Refurbished'),
      ],
      menuHeader: const Padding(
        padding: EdgeInsets.all(16),
        child: Text('Filters', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
      ),
      onSelect: (selected, _) {
        print(selected.map((c) => c.key).toList());
      },
    ),
  ),
  body: Center(
    child: ElevatedButton(
      onPressed: () => filterStore.openDrawer(),
      child: const Text('Open Filters'),
    ),
  ),
)

Programmatic Control

Open or close the drawer from anywhere:

final store = MultiSelectKeyStore.of<String>('myFilter');

store.openDrawer();
store.closeDrawer();

// Clean up when done
MultiSelectKeyStore.dispose('myFilter');

Overlay Mode

Without scaffoldKey, it renders a trigger button that opens a standalone overlay drawer. No Scaffold configuration needed:

MultiSelectField<String>.drawer(
  label: 'Filters',
  data: () => [
    Choice('new', 'New'),
    Choice('used', 'Used'),
    Choice('refurbished', 'Refurbished'),
  ],
  drawerStyle: const DrawerStyle(
    width: 280,
    position: DrawerPosition.right,
  ),
  onSelect: (selected, _) {
    print(selected.map((c) => c.key).toList());
  },
)

The overlay respects SafeArea so it never overlaps system notifications. Tap outside to dismiss.

You can also pass a custom child widget as the trigger:

MultiSelectField<String>.drawer(
  label: 'Filters',
  child: const Icon(Icons.filter_list),
  data: () => choices,
  onSelect: (selected, _) => applyFilters(),
)

Close on Select #

By default, the menu stays open after selecting an item (multi-selection friendly). Set closeOnSelect: true to automatically close the menu after each selection. Available in all variants.

// Standard — menu closes after each tap
MultiSelectField<String>(
  data: () => choices,
  closeOnSelect: true,
  onSelect: (selected, _) => print(selected),
)

// Bottom Sheet — sheet dismisses after selection
MultiSelectField<String>.bottomSheet(
  label: 'Category',
  data: () => choices,
  closeOnSelect: true,
  onSelect: (selected, _) => print(selected),
)

// Drawer — drawer closes after selection
MultiSelectField<String>.drawer(
  label: 'Filter',
  scaffoldKey: scaffoldKey,
  data: () => choices,
  closeOnSelect: true,
  onSelect: (selected, _) => print(selected),
)

onChanged Callback #

Simple callback that fires only on user interaction, never on default data. Use it when you don't need to distinguish between user and default selections.

MultiSelectField<String>(
  data: () => choices,
  defaultData: [Choice('1', 'Apple')],  // Does NOT trigger onChanged
  onChanged: (selectedItems) {
    // Only fires when the user taps an item
    print('User selected: ${selectedItems.length} items');
  },
)

You can use onChanged alone, onSelect alone, or both together:

MultiSelectField<String>(
  data: () => choices,
  onSelect: (items, isDefault) => print('onSelect: isDefault=$isDefault'),
  onChanged: (items) => print('onChanged: ${items.length} items'),
)

Field Width Control (Standard) #

Control the field width without wrapping in SizedBox:

// Shrinks to fit the label/chips — compact inline selector
MultiSelectField<String>(
  data: () => choices,
  fieldWidth: FieldWidth.fitContent,
  iconSpacing: 2,
)

// Fixed width in pixels
MultiSelectField<String>(
  data: () => choices,
  fieldWidth: FieldWidth.fixed(200),
)

// Default: fills all available width (unchanged behavior)
MultiSelectField<String>(
  data: () => choices,
)

Icon Spacing (Standard) #

Control the gap between the content area and the dropdown arrow:

MultiSelectField<String>(
  data: () => choices,
  iconSpacing: 8, // 8px gap between label and arrow icon
)

Some screen captures #

With grouping list. #

Screenshot 2024-09-04 at 9 45 32 PM

Multiple selection. #

Screenshot 2024-09-04 at 9 54 57 PM

Multiple selection custom widget. #

Screenshot 2024-09-04 at 9 55 49 PM

Text Filtering. #

Screenshot 2024-09-04 at 9 56 50 PM Screenshot 2024-09-04 at 9 54 03 PM

Video example #

ScreenRecording2024-09-05at12 19 59AM-ezgif com-video-to-gif-converter

Contribution #

Contributions are welcome! If you have ideas for new features or improvements, please open an issue or submit a pull request.

  1. Fork the repository.
  2. Create a new branch (git checkout -b feature/new-feature).
  3. Commit your changes (git commit -am 'Add new feature').
  4. Push to the branch (git push origin feature/new-feature).
  5. Open a pull request.

License #

This project is licensed under the MIT License - see the LICENSE file for details.


11
likes
160
points
929
downloads

Documentation

API reference

Publisher

verified publisherjhonacode.com

Weekly Downloads

A flexible dropdown field supporting single/multiple selection modes, styles, titles, etc

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on multiselect_field