Item Dropper

A customizable, accessible dropdown package for Flutter with powerful single-select and multi-select capabilities, built-in search filtering, and full keyboard navigation support.


πŸ“Έ Screenshots

Single-Select Dropdown

Single-Select Demo

Multi-Select Dropdown with Chips

Multi-Select Demo


✨ Features

Core Functionality

  • βœ… Single-Select Dropdown - Traditional dropdown with single item selection
  • βœ… Multi-Select Dropdown - Select multiple items displayed as chips
  • βœ… Real-time Search - Filter items as you type
  • βœ… Keyboard Navigation - Full support for arrow keys, Enter, and Escape
  • βœ… Add New Items - Allow users to create new items on-the-fly
  • βœ… Delete Items - Optional delete buttons for managing the item list
  • βœ… Group Headers - Organize items with visual separators
  • βœ… Smart Positioning - Automatically positions dropdown above or below based on screen space

Customization

  • 🎨 Fully Styleable - Custom text styles, decorations, and item builders
  • 🎯 Max Selection Limit - Optionally limit number of selections in multi-select
  • βš™οΈ Flexible Configuration - Width, height, elevation, and more
  • 🎭 Custom Item Rendering - Provide your own popup item builder
  • πŸ–ŒοΈ Custom Decorations - Customize field container and chip appearance

Accessibility

  • β™Ώ Screen Reader Support - Full semantic labels and live region announcements
  • ⌨️ Keyboard Accessible - Navigate and select without a mouse
  • πŸ”Š Selection Announcements - Screen readers announce item selections

Performance

  • ⚑ Optimized Filtering - Cached results for fast search
  • πŸ“¦ Smart Rebuilds - Efficient state management prevents unnecessary rebuilds
  • πŸ’Ύ Decoration Caching - Pre-computed decorations for smooth rendering

πŸ“¦ Installation

Add item_dropper to your pubspec.yaml:

dependencies:
  item_dropper: ^1.0.0

Then run:

flutter pub get

πŸš€ Quick Start

Single-Select Dropdown

import 'package:item_dropper/item_dropper.dart';

// Create your items
final items = [
  ItemDropperItem(value: '1', label: 'Apple'),
  ItemDropperItem(value: '2', label: 'Banana'),
  ItemDropperItem(value: '3', label: 'Orange'),
  ItemDropperItem(value: '4', label: 'Grapes'),
];

// Track selected item
ItemDropperItem<String>? selectedItem;

// Use in your widget tree
SingleItemDropper<String>(
  items: items,
  selectedItem: selectedItem,
  width: 300,
  onChanged: (item) {
    setState(() {
      selectedItem = item;
    });
  },
)

selectedItem supplies the initial selection and subsequent parent updates. Changing it updates the dropdown; changing a non-null value to null clears both selection and text. Unchanged values, including null or an omitted argument, preserve the user's selection across parent rebuilds. Parent-driven changes do not invoke onChanged.

To clear a selection through this parameter, keep the parent variable updated from onChanged, as above, then set it to null. A variable that stays null cannot signal a new clear request.

Multi-Select Dropdown

import 'package:item_dropper/item_dropper.dart';

// Create your items
final items = [
  ItemDropperItem(value: '1', label: 'Red'),
  ItemDropperItem(value: '2', label: 'Blue'),
  ItemDropperItem(value: '3', label: 'Green'),
  ItemDropperItem(value: '4', label: 'Yellow'),
];

// Track selected items
List<ItemDropperItem<String>> selectedItems = [];

// Use in your widget tree
MultiItemDropper<String>(
  items: items,
  selectedItems: selectedItems,
  width: 400,
  maxSelected: 3, // Optional: limit selections
  onChanged: (items) {
    setState(() {
      selectedItems = items;
    });
  },
)

🎯 Common Use Cases

With Search/Filter

Both widgets automatically support search - just start typing!

SingleItemDropper<String>(
  items: longListOfItems,
  selectedItem: selectedItem,
  width: 300,
  onChanged: (item) => setState(() => selectedItem = item),
  // Search is built-in - no configuration needed!
)

With Group Headers

final itemsWithGroups = [
  ItemDropperItem(value: 'header1', label: 'Fruits', isGroupHeader: true),
  ItemDropperItem(value: '1', label: 'Apple'),
  ItemDropperItem(value: '2', label: 'Banana'),
  ItemDropperItem(value: 'header2', label: 'Vegetables', isGroupHeader: true),
  ItemDropperItem(value: '3', label: 'Carrot'),
  ItemDropperItem(value: '4', label: 'Lettuce'),
];

SingleItemDropper<String>(
  items: itemsWithGroups,
  selectedItem: selectedItem,
  width: 300,
  onChanged: (item) => setState(() => selectedItem = item),
)

Allow Adding New Items

Both dropdowns refresh their item caches when the parent rebuilds, including after in-place list changes such as setState(() => items.add(newItem)). Replacing the list also works. Item rows are immutable; replace a row to update its label or other properties.

Detecting these changes takes O(n) item comparisons per parent rebuild and an O(n) saved snapshot per dropdown. Account for this cost with very large lists.

Both dropdowns support onAddItem with an initially empty items list. The callback runs when the user selects the add row, and may return null to cancel. Custom popup builders must check item.isAddItem before reading item.value: add rows are actions with a label, not values of your item type. Reading .value on an add row throws a StateError; render its label instead.

SingleItemDropper<String>(
  items: items,
  selectedItem: selectedItem,
  width: 300,
  onChanged: (item) => setState(() => selectedItem = item),
  onAddItem: (searchText) {
    // Create new item from search text
    final newItem = ItemDropperItem(
      value: searchText.toLowerCase(),
      label: searchText,
    );
    
    // Add to your items list
    setState(() {
      items.add(newItem);
    });
    
    // Return the new item to auto-select it
    return newItem;
  },
)

Custom Styling

MultiItemDropper<String>(
  items: items,
  selectedItems: selectedItems,
  width: 400,
  onChanged: (items) => setState(() => selectedItems = items),
  
  // Customize field text style
  fieldTextStyle: TextStyle(
    fontSize: 14,
    fontWeight: FontWeight.w500,
    color: Colors.blue.shade900,
  ),
  
  // Customize popup text style
  popupTextStyle: TextStyle(
    fontSize: 12,
    color: Colors.black87,
  ),
  
  // Customize group header style
  popupGroupHeaderStyle: TextStyle(
    fontSize: 11,
    fontWeight: FontWeight.bold,
    color: Colors.grey.shade700,
  ),
  
  // Custom chip decoration
  selectedChipDecoration: BoxDecoration(
    color: Colors.green.shade100,
    borderRadius: BorderRadius.circular(16),
  ),
  
  // Custom field decoration
  fieldDecoration: BoxDecoration(
    color: Colors.white,
    border: Border.all(color: Colors.blue, width: 2),
    borderRadius: BorderRadius.circular(12),
  ),
)

Both dropdowns use elevation for the popup's Material shadow (default 4.0); 0 removes the shadow. In multi-select, this also applies to the no-results and maximum-selection messages. A null multi-select elevation uses the default.

For list popups, showScrollbar controls persistent thumb visibility (default true); false lets the scrollbar appear while scrolling and fade afterward. scrollbarThickness sets its width in logical pixels (default 6.0). These settings and elevation update when the parent rebuilds, including while open.

MultiItemDropper<String>(
  items: items,
  width: 300,
  selectedItems: selectedItems,
  elevation: 8,
  showScrollbar: true,
  scrollbarThickness: 10,
  onChanged: (items) => setState(() => selectedItems = items),
)

Disabled State

SingleItemDropper<String>(
  items: items,
  selectedItem: selectedItem,
  width: 300,
  enabled: false, // Disables interaction
  onChanged: (item) => setState(() => selectedItem = item),
)

πŸ“š Advanced Features

Custom Item Rendering

Provide your own popup item builder for complete control:

SingleItemDropper<String>(
  items: items,
  selectedItem: selectedItem,
  width: 300,
  onChanged: (item) => setState(() => selectedItem = item),
  popupItemBuilder: (context, item, isSelected) {
    return Container(
      padding: EdgeInsets.all(12),
      color: isSelected ? Colors.blue.shade100 : Colors.white,
      child: Row(
        children: [
          Icon(Icons.star, color: Colors.amber),
          SizedBox(width: 8),
          Text(item.label),
        ],
      ),
    );
  },
)

Programmatic Focus Control

final inputKey = GlobalKey();

SingleItemDropper<String>(
  inputKey: inputKey,
  items: items,
  selectedItem: selectedItem,
  width: 300,
  onChanged: (item) => setState(() => selectedItem = item),
)

// Later, programmatically focus:
FocusScope.of(context).requestFocus(
  FocusNode()..requestFocus(),
);

Keyboard Navigation

Built-in keyboard support:

  • ↑/↓ Arrow Keys - Navigate through items
  • Enter - Select highlighted item
  • Escape - Close dropdown
  • Type to search - Filter items in real-time

Localization

All user-facing text can be customized by providing an ItemDropperLocalizations instance:

SingleItemDropper<String>(
  items: items,
  selectedItem: selectedItem,
  width: 300,
  onChanged: (item) => setState(() => selectedItem = item),
  localizations: ItemDropperLocalizations(
    addItemPrefix: 'Ajouter "',
    addItemSuffix: '"',
    noResultsFound: 'Aucun rΓ©sultat trouvΓ©',
    deleteDialogTitle: 'Supprimer "{label}"?',
    deleteDialogContent: 'Cet Γ©lΓ©ment sera supprimΓ© de la liste.',
    deleteDialogCancel: 'Annuler',
    deleteDialogDelete: 'Supprimer',
    // ... other strings
  ),
)

See ItemDropperLocalizations class for all available strings.


🎨 Customization Options

SingleItemDropper Parameters

Parameter Type Default Description
items List<ItemDropperItem<T>> required The items to display in the dropdown
selectedItem ItemDropperItem<T>? null The currently selected item
onChanged Function(ItemDropperItem<T>?) required Called when the selection changes
popupItemBuilder Widget Function(...)? null Optional custom builder for popup items
width double required The width of the dropdown field
enabled bool true Whether the dropdown is enabled
hintText String? null Hint/placeholder text for input field
showKeyboard bool false Whether to show the mobile keyboard
onAddItem ItemDropperItem<T>? Function(String)? null Callback for adding new items based on search text entered by user
onDeleteItem Function(ItemDropperItem<T>)? null Callback for deleting items
maxDropdownHeight double 200.0 Maximum dropdown popup height
elevation double 4.0 Popup shadow elevation
showScrollbar bool true Keep the popup scrollbar thumb visible
scrollbarThickness double 6.0 Popup vertical scrollbar thickness
fieldTextStyle TextStyle? null Text style for input/search field
popupTextStyle TextStyle? null Text style for popup dropdown items
popupGroupHeaderStyle TextStyle? null Text style for group headers in popup
itemHeight double? null Height for popup dropdown items
fieldDecoration BoxDecoration? null Optional BoxDecoration for field container
showDropdownPositionIcon bool true Show the dropdown position arrow (down/up)
showDeleteAllIcon bool true Show the clear (X) icon
localizations ItemDropperLocalizations? null Localization strings for user-facing text (optional)
inputKey GlobalKey? null Key for programmatic access
dependencies SingleItemDropperDependencies<T>? null Dependency factory hooks for tests and advanced integrations

MultiItemDropper Parameters

Parameter Type Default Description
items List<ItemDropperItem<T>> required The items to display in the dropdown
selectedItems List<ItemDropperItem<T>>? null The currently selected items
onChanged Function(List<ItemDropperItem<T>>) required Called when the selection changes
popupItemBuilder Widget Function(...)? null Optional custom builder for popup items
width double required The width of the dropdown field
enabled bool true Whether the dropdown is enabled
hintText String? null Hint/placeholder text for input field
maxSelected int? null Maximum number of items selectable in multi-select dropdown
onAddItem ItemDropperItem<T>? Function(String)? null Callback for adding new items based on search text entered by user
onDeleteItem Function(ItemDropperItem<T>)? null Callback for deleting items
maxDropdownHeight double 200.0 Maximum dropdown popup height
showScrollbar bool true Keep the popup scrollbar thumb visible
scrollbarThickness double 6.0 Popup vertical scrollbar thickness
itemHeight double? null Height for popup dropdown items
popupTextStyle TextStyle? null Text style for popup dropdown items
popupGroupHeaderStyle TextStyle? null Text style for group headers in popup
fieldTextStyle TextStyle? null Text style for input/search field and chips
selectedChipDecoration BoxDecoration? null Custom BoxDecoration for selected chips
fieldDecoration BoxDecoration? null Optional BoxDecoration for field container
elevation double? null Popup shadow elevation (null uses 4.0)
inputKey GlobalKey? null Key for programmatic access to widget
showDropdownPositionIcon bool true Show the dropdown position arrow (down/up)
showDeleteAllIcon bool true Show the clear (X) icon (clears search/all selections)
localizations ItemDropperLocalizations? null Localization strings for user-facing text (optional)
dependencies MultiItemDropperDependencies<T>? null Dependency factory hooks for tests and advanced integrations

ItemDropperItem Properties

Property Type Default Description
value T required Value associated with the item
label String required Display text
isGroupHeader bool false Whether item is a group header
isEnabled bool true Whether item can be selected
isDeletable bool false Whether item can be deleted
isAddItem bool false Reserved sentinel for internally generated add-item rows

ItemDropperItem uses value equality across all six properties, so equivalent items behave consistently in comparisons, sets, and map keys.

Shared Widget Contract

SingleItemDropper<T> and MultiItemDropper<T> implement ItemDropperBase<T>. Use this shared type when application code needs to accept either widget while reading their common configuration.

Dependency Hooks

SingleItemDropperDependencies<T> and MultiItemDropperDependencies<T> provide optional factory hooks for substituting managers and controllers in focused tests or advanced integrations. Normal application usage does not need to pass dependencies.


πŸ“– Full Documentation


🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

πŸ› Issues

Found a bug or have a feature request? Please open an issue on GitHub.


πŸ“„ License

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


πŸ™ Acknowledgments

Built with ❀️ using Flutter. Designed for developers who need powerful, accessible dropdown components.


πŸ“ Changelog

See CHANGELOG.md for a list of changes in each version.


⭐ Show Your Support

If you find this package useful, please give it a ⭐ on GitHub!


Keywords: flutter, dropdown, select, multi-select, autocomplete, searchable, accessible, keyboard navigation, chips