multiselect_field 2.3.0
multiselect_field: ^2.3.0 copied to clipboard
A flexible dropdown field supporting single/multiple selection modes, styles, titles, etc
MultiSelectField #
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).onChangedcallback: 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 ofChoiceelements 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 tofalse.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 tomaxLines(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. #
Multiple selection. #
Multiple selection custom widget. #
Text Filtering. #
With title and footer. #
Video example #
Contribution #
Contributions are welcome! If you have ideas for new features or improvements, please open an issue or submit a pull request.
- Fork the repository.
- Create a new branch (
git checkout -b feature/new-feature). - Commit your changes (
git commit -am 'Add new feature'). - Push to the branch (
git push origin feature/new-feature). - Open a pull request.
License #
This project is licensed under the MIT License - see the LICENSE file for details.