A dropdown that works with all iterables instead of just lists. It has multiselect features for selecting multiple items as well.

This dropdown stays performant because it consumes any Iterable instead of forcing you to copy the data into Lists, and it uses ListView.builder so only the options currently visible on screen are ever built and kept in memory.

Screenshots

Custom Decoration

Single Select Single Select with Search Multi Select Multi Select with Search
Single Select Single Select with Search Multi Select Multi Select with Search

Features

Use this package in your Flutter app to:

  • select multiple items in dropdowns
  • use iterables in a dropdown
  • Keep huge data sources responsive by only building the visible options through ListView.builder
  • Provide custom first/last options via CustomItems that can act as headers or buttons
  • Has a future constructor for building from APIs
  • Package size <= 40KB

Platform Support

Platform Supported
Android
iOS
Web
Windows
MacOS
Linux

Getting started

Add the package to your Flutter app

flutter pub add iterable_dropdown

Import the package where you want to use it

import 'package:iterable_dropdown/iterable_dropdown.dart';

Usage

Directly use the component

@override
Widget build(BuildContext context) {
  return IterableDropdown<String>.builder(
    controller: _dropdownController,
    items: items,
    itemBuilder: (_, _, item, selected, toggleSelection) {
      return ListTile(
        key: ValueKey(item.key),
        title: Text(item.label),
        trailing: selected ? const Icon(Icons.check_circle_outline) : null,
        selected: selected,
        onTap: toggleSelection,
      );
    },
    selectionMode: SelectionMode.multi,
  );
}

You can also open or close the dropdown programmatically by interacting with the controller.

_dropdownController.openDropdown() // show dropdown overlay
_dropdownController.closeDropdown() // hide dropdown overlay

// getters to check if dropdown is open or close
_dropdownController.isOpen
_dropdownController.isClose

Fetch items from a Future

When your data comes from an async source, use the .future constructor. The future runs only once per controller lifecycle unless you explicitly refresh it.

@override
Widget build(BuildContext context) {
  return IterableDropdown<String>.future(
    controller: _dropdownController,
    future: _loadCities, // returns Future<Iterable<IterableDropdownItem<String>>>
    loaderColor: Colors.indigo,
    itemBuilder: (_, __, item, selected, toggleSelection) => ListTile(
      key: ValueKey(item.key),
      title: Text(item.label),
      trailing: selected ? const Icon(Icons.check_circle_outline) : null,
      selected: selected,
      onTap: toggleSelection,
    ),
  );
}

Future<Iterable<IterableDropdownItem<String>>> _loadCities() async {
  final cities = await cityRepository.fetchCities();
  return cities.map((city) => IterableDropdownItem(key: city.id, label: city.name, value: city));
}

You can also refresh the dropdown using the _dropdownController.refresh() method.

Custom Items (Pinned Rows)

Use the customItems parameter when you need pinned widgets before or after the generated options. These widgets never get filtered or selected, which makes them perfect for headers, dividers, or persistent actions. For instance, a customer selector can expose an Add new customer row at the top that launches a creation flow.

@override
Widget build(BuildContext context) {
  return IterableDropdown<Customer>.builder(
    controller: _customerController,
    items: customers,
    itemBuilder: (context, index, item, selected, toggleSelection) => ListTile(
      key: ValueKey(item.key),
      title: Text(item.label),
      trailing: selected ? const Icon(Icons.check_circle_outline) : null,
      selected: selected,
      onTap: toggleSelection,
    ),
    customItems: CustomItems(
      start: ListTile(
        leading: const Icon(Icons.add),
        title: const Text('Add new customer'),
        onTap: _createCustomer,
      ),
      end: TextButton(
        onPressed: _showAdvancedFilters,
        child: const Text('Show all customers'),
      ),
    ),
  );
}

Those boundary widgets stay visible regardless of what the user types into the search box and cannot be selected, so they are ideal for headers or CTA rows.

Custom Trigger (MenuAnchor-style)

When you want full control over the dropdown button, provide a builder and an optional child. The builder receives the controller and the child widget so you can recreate a MenuAnchor-like experience while keeping the overlay and selection logic intact.

@override
Widget build(BuildContext context) {
  return IterableDropdown<String>.builder(
    controller: _dropdownController,
    items: items,
    itemBuilder: (_, _, item, selected, toggleSelection) => ListTile(
      key: ValueKey(item.key),
      title: Text(item.label),
      trailing: selected ? const Icon(Icons.check_circle_outline) : null,
      selected: selected,
      onTap: toggleSelection,
    ),
    builder: (context, controller, child) {
      return FilledButton(
        onPressed: controller.toggleDropdown,
        child: child ?? const Text('Choose items'),
      );
    },
    child: Row(
      spacing: 8,
      mainAxisSize: MainAxisSize.min,
      children: [
        const Icon(Icons.list_alt),
        Text(_dropdownController.selectedItems.isEmpty
            ? 'Choose items'
            : '${_dropdownController.selectedItems.length} selected'),
      ],
    ),
  );
}

Additional information

You can contribute to this package via the Github Repo. Check out the Contribution Page for reference.

Libraries

iterable_dropdown
A package for creating dropdowns out of any Iterables instead of just List