Adaptive Search Dropdown

A mobile-friendly, adaptive search dropdown widget for Flutter with support for single/multi-selection, infinite scrolling, asynchronous search, and full UI customisation.

Features

  • Mobile First — overlay panel adapts to keyboard and available screen height, flips above the field when needed
  • Searchable — optional search bar with 400ms debounce for remote filtering
  • Single & Multi-Select — built-in support for both modes
  • Pagination — auto-loads more items on scroll via onLoadMore
  • "Add New" Row — lets users create items that are not in the list
  • Read-Only Mode — greyed-out field that cannot be opened
  • Full UI Customisation — every colour, text style, icon, padding, and border is configurable via AdaptiveDropdownStyle, or replace entire sections with builder callbacks

Getting Started

dependencies:
  adaptive_search_dropdown: ^0.0.1

Usage

Basic (uses sensible defaults)

AdaptiveSearchDropdown<String>(
  label: 'Select Country',
  items: ['United States', 'Canada', 'Mexico', 'United Kingdom'],
  itemAsString: (item) => item,
  onChanged: (value) => print("Selected: $value"),
)

Multi-Select

AdaptiveSearchDropdown<String>(
  label: 'Select Skills',
  items: ['Flutter', 'Dart', 'Firebase', 'Swift'],
  itemAsString: (item) => item,
  multiSelect: true,
  onSelectionChanged: (values) => print("Selected: $values"),
)

Async Data & Pagination

AdaptiveSearchDropdown<UserModel>(
  label: 'Assign User',
  items: users,
  itemAsString: (user) => user.name,
  itemId: (user) => user.id,
  onTap: () async => await fetchUsers(page: 1),
  onSearch: (query) async => await searchUsers(query),
  onLoadMore: (page, query) async => await fetchUsers(page: page, search: query),
  totalPages: totalPages,
)

Full UI Customisation

Pass your own colours, text styles, icons, paddings — everything is optional, defaults are used for anything you skip.

// Define your app's style once
final myDropdownStyle = AdaptiveDropdownStyle(
  // Accent colour used for selected text, tick box, check icon, add-row
  accentColor: Color(0xff03A4AA),

  // Field
  fieldTextStyle: TextStyle(fontSize: 14, fontFamily: 'MyFont'),
  fieldBorderRadius: BorderRadius.circular(10),
  fieldContentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 12),
  readOnlyFillColor: Color(0xffF7F9FA),
  readOnlyTextStyle: TextStyle(color: Color(0xff72879D)),

  // Label
  labelStyle: TextStyle(color: Color(0xff72879D)),
  requiredAsteriskStyle: TextStyle(color: Color(0xffFF5247)),

  // Error
  errorBorderColor: Color(0xffFF5247),
  errorTextStyle: TextStyle(color: Color(0xffFF5247), fontSize: 12),

  // Suffix icons
  arrowIcon: SvgPicture.asset("assets/images/dashboard/down_arrow.svg", height: 9),
  clearIcon: Icon(Icons.close_rounded, size: 18, color: Color(0xff72879D)),

  // Panel
  panelBackgroundColor: Colors.white,
  panelDarkBackgroundColor: Color(0xff161F28),
  panelBorderColor: Color(0xffD2E3F4),
  panelDarkBorderColor: Color(0xff243342),
  panelBorderRadius: BorderRadius.circular(10),

  // Rows
  rowTextStyle: TextStyle(fontSize: 13),
  selectedRowTextStyle: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xff03A4AA)),
  selectedRowColor: Color(0xffD9F1F2),      // light mode
  selectedRowDarkColor: Color(0xff03A4AA).withOpacity(0.12), // dark mode
  rowPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),

  // Search bar
  searchTextStyle: TextStyle(fontSize: 14),
  searchHintStyle: TextStyle(color: Color(0xff72879D)),
  searchCursorColor: Color(0xff03A4AA),

  // Create/Add row
  createRowTextStyle: TextStyle(fontWeight: FontWeight.w600, color: Color(0xff03A4AA)),
  createRowBackgroundColor: Color(0xffD9F1F2),

  // Empty state
  emptyTextStyle: TextStyle(color: Color(0xff72879D)),
);

// Use it
AdaptiveSearchDropdown<LeadModel>(
  style: myDropdownStyle,
  label: 'Lead Name',
  items: leads,
  itemAsString: (lead) => lead.name,
  itemId: (lead) => lead.id,
  onChanged: (value) => ...,
)

Option 2: Builder Callbacks (full UI replacement)

For total control over a specific part, use builder callbacks:

AdaptiveSearchDropdown<UserModel>(
  label: 'Select User',
  items: users,
  itemAsString: (user) => user.name,

  // Build each row yourself
  itemBuilder: (item, isSelected, isMultiSelect) {
    return ListTile(
      leading: CircleAvatar(backgroundImage: NetworkImage(item.avatar)),
      title: Text(item.name),
      trailing: isSelected ? Icon(Icons.check, color: Colors.teal) : null,
    );
  },

  // Build the closed field yourself
  selectedFieldBuilder: (text, isOpen) {
    return Container(
      padding: EdgeInsets.all(12),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.teal),
        borderRadius: BorderRadius.circular(8),
      ),
      child: Text(text.isEmpty ? 'Pick a user...' : text),
    );
  },

  // Custom empty state
  emptyBuilder: (query) => Center(
    child: Text('No users found for "$query"'),
  ),

  // Custom loading
  loadingBuilder: () => Center(child: Lottie.asset('assets/loading.json')),

  onChanged: (value) => ...,
)

Option 3: Mix Both

Use AdaptiveDropdownStyle for colours and a builder for one specific piece:

AdaptiveSearchDropdown<UserModel>(
  style: myDropdownStyle,   // your colours and styles
  itemBuilder: (item, isSelected, _) => MyCustomRow(item, isSelected),  // custom row only
  ...
)

API Reference

Property Type Description
style AdaptiveDropdownStyle? Full visual customisation
items List<T> The list of items to display
label String Field label text
itemAsString String Function(T) How to display each item
itemId Object? Function(T)? Unique identity for each item
itemSubtitle String? Function(T)? Optional second line per row
initialItem T? Pre-selected item
isRequired bool Shows * and validates non-empty
readOnly bool Greys out field, prevents opening
multiSelect bool Enables multi-selection mode
initialItems List<T>? Pre-selected items for multi-select
onChanged void Function(T?)? Called on single-select change
onSelectionChanged void Function(List<T>)? Called on multi-select change
onTap Future<List<T>> Function()? Load items when panel opens
onSearch Future<List<T>> Function(String)? Server-side search
onLoadMore Future<List<T>> Function(int, String)? Pagination
totalPages int? Total pages from server
canCreate bool Show "Add" row
onCreate VoidCallback? Called when "Add" is tapped
itemBuilder Widget Function(T, bool, bool)? Custom row builder
selectedFieldBuilder Widget Function(String, bool)? Custom field builder
emptyBuilder Widget Function(String)? Custom empty state
loadingBuilder Widget Function()? Custom loading state

Libraries

adaptive_search_dropdown
A mobile-friendly, adaptive search dropdown widget for Flutter.