dropdown_plus_bloc 0.1.6
dropdown_plus_bloc: ^0.1.6 copied to clipboard
Customizable Flutter dropdowns with BLoC/Cubit integration: searchable single-select and multi-select with chips, offline caching, and theming.
dropdown_plus #
A highly customisable Flutter dropdown package with optional BLoC / Cubit integration. Use *Plus widgets with a cubit, or plain widgets with your own state.
| Widget | Description |
|---|---|
SearchableDropdownPlus |
Single-select searchable dropdown (BLoC/Cubit) |
MultiSelectDropdownPlus |
Multi-select with chips (BLoC/Cubit) |
SearchableDropdown |
Single-select searchable dropdown (no BLoC β pass items / isLoading) |
MultiSelectDropdown |
Multi-select with chips (no BLoC β pass items / isLoading) |
Features #
- π BLoC / Cubit integration β
SearchableDropdownPlus/MultiSelectDropdownPluswire to anyCubitorBloc - π¦ Plain StatefulWidget API β
SearchableDropdown/MultiSelectDropdownwork withsetState, Provider, Riverpod, etc. - π Real-time search β calls your cubit's search method as the user types
- π΄ Offline caching β falls back to client-side filtering when no internet is available
- π¨ Preset + custom theming β use
themeStylefor out-of-the-box looks orDropdownPlusThemefor full control - π§© Custom builders β override item rows, chip display, and the trigger button content
- π Controlled mode β sync selected value(s) from external state (e.g. QR scan, form reset)
- β Multi-select helpers β "Select All" / "Clear All" header, "+N more" overflow chip
- π Smooth animations β animated open/close, arrow rotation, item selection
Screenshots #
Single select #
Multi select #
Installation #
Add to your pubspec.yaml:
dependencies:
dropdown_plus: ^0.1.0
flutter_bloc: ">=8.0.0 <10.0.0" # only transitive dep needed
Then run:
flutter pub get
Quick Start #
import 'package:dropdown_plus/dropdown_plus.dart';
Single Select #
SearchableDropdownPlus<WorkerCubit, WorkerState>(
cubit: context.read<WorkerCubit>(),
hintText: 'Search workerβ¦',
onSearch: (query) => context.read<WorkerCubit>().search(query),
onStateChange: (state, updateList, updateLoading) {
if (state is WorkersLoaded) {
updateList(
state.workers
.map((w) => DropdownItem(value: w, label: w.name))
.toList(),
);
updateLoading(false);
} else if (state is WorkersLoading) {
updateLoading(true);
} else if (state is WorkersError) {
updateLoading(false);
}
},
onSelectionChanged: (item) {
final worker = item.value as Worker;
// use worker
},
)
Multi Select #
MultiSelectDropdownPlus<WorkerCubit, WorkerState>(
cubit: context.read<WorkerCubit>(),
hintText: 'Select workersβ¦',
onSearch: (query) => context.read<WorkerCubit>().search(query),
onStateChange: (state, updateList, updateLoading) {
if (state is WorkersLoaded) {
updateList(
state.workers
.map((w) => DropdownItem(value: w, label: '${w.name} (${w.id})'))
.toList(),
);
updateLoading(false);
} else if (state is WorkersLoading) {
updateLoading(true);
}
},
onSelectionChanged: (items) {
final workers = items.map((e) => e.value as Worker).toList();
// use workers
},
)
Without BLoC #
Pass the current item list and loading flag from your own state. If onSearch is omitted, the search box filters locally over items. If onSearch is set, call your API and rebuild with updated items / isLoading.
Single select
SearchableDropdown(
hintText: 'Search workerβ¦',
items: workers,
isLoading: loadingWorkers,
selectedValue: selectedWorkerItem,
onSearch: (query) async {
setState(() => loadingWorkers = true);
final list = await api.searchWorkers(query);
setState(() {
workers = list.map((w) => DropdownItem(value: w, label: w.name)).toList();
loadingWorkers = false;
});
},
onSelectionChanged: (item) =>
setState(() => selectedWorkerItem = item),
)
Multi select
MultiSelectDropdown(
hintText: 'Select workersβ¦',
items: workers,
isLoading: loadingWorkers,
selectedItems: selectedWorkerItems,
onSearch: (query) async {
setState(() => loadingWorkers = true);
final list = await api.searchWorkers(query);
setState(() {
workers = list.map((w) => DropdownItem(value: w, label: w.name)).toList();
loadingWorkers = false;
});
},
onSelectionChanged: (items) =>
setState(() => selectedWorkerItems = items),
)
Theme Customisation #
Pass a DropdownPlusTheme to any dropdown widget (*Plus or plain) to change its appearance:
SearchableDropdownPlus(
...
dropdownTheme: DropdownPlusTheme(
// Trigger button
backgroundColor: Colors.grey[100],
borderColor: Colors.grey[300],
activeBorderColor: Colors.deepPurple,
borderRadius: 12,
// Hint & text
hintStyle: TextStyle(color: Colors.grey, fontSize: 14),
triggerTextStyle: TextStyle(color: Colors.black87, fontWeight: FontWeight.w600),
// Dropdown panel
menuBackgroundColor: Colors.white,
menuBorderRadius: 16,
menuElevation: 8,
menuMaxHeight: 280,
// Search bar
searchBarBackgroundColor: Colors.grey[50],
searchHintStyle: TextStyle(color: Colors.grey),
searchIconColor: Colors.grey,
// Items
itemTextStyle: TextStyle(color: Colors.black87),
selectedItemTextStyle: TextStyle(color: Colors.deepPurple, fontWeight: FontWeight.bold),
selectedItemBackgroundColor: Colors.deepPurple.withValues(alpha: 0.08),
// Loading / empty
loadingIndicatorColor: Colors.deepPurple,
noResultsTextStyle: TextStyle(color: Colors.grey),
),
)
Preset Theme Styles (themeStyle) #
Use themeStyle when you want a ready-to-use UI without configuring every field:
SearchableDropdownPlus<WorkerCubit, WorkerState>(
cubit: context.read<WorkerCubit>(),
hintText: 'Search workerβ¦',
themeStyle: DropdownPlusThemeStyle.compact,
onSearch: (query) => context.read<WorkerCubit>().search(query),
onStateChange: (state, updateList, updateLoading) {
// ...
},
)
Available presets:
| Enum Value | Style |
|---|---|
DropdownPlusThemeStyle.material |
Default Material-like appearance |
DropdownPlusThemeStyle.minimal |
Light borders and subtle surfaces |
DropdownPlusThemeStyle.rounded |
Larger radius and softer card-like look |
DropdownPlusThemeStyle.outlined |
Strong border-focused style |
DropdownPlusThemeStyle.dark |
Dark surfaces with light foregrounds |
DropdownPlusThemeStyle.compact |
Dense spacing and smaller visuals |
You can also start from a preset and override specific properties:
MultiSelectDropdownPlus<WorkerCubit, WorkerState>(
cubit: context.read<WorkerCubit>(),
hintText: 'Select workersβ¦',
themeStyle: DropdownPlusThemeStyle.dark,
dropdownTheme: DropdownPlusThemePresets
.forStyle(DropdownPlusThemeStyle.dark)
.copyWith(borderRadius: 16),
onSearch: (query) => context.read<WorkerCubit>().search(query),
onStateChange: (state, updateList, updateLoading) {
// ...
},
)
dropdownTheme takes precedence over themeStyle when both are provided.
Dark Theme Example #
dropdownTheme: DropdownPlusTheme(
backgroundColor: const Color(0xFF1E1E2E),
menuBackgroundColor: const Color(0xFF2A2A3E),
borderColor: Colors.white12,
activeBorderColor: Colors.blueAccent,
hintStyle: TextStyle(color: Colors.white38),
triggerTextStyle: TextStyle(color: Colors.white),
itemTextStyle: TextStyle(color: Colors.white70),
selectedItemTextStyle: TextStyle(color: Colors.blueAccent, fontWeight: FontWeight.w600),
selectedItemBackgroundColor: Colors.blueAccent.withValues(alpha:0.15),
dividerColor: Colors.white10,
searchBarBackgroundColor: Colors.white10,
searchHintStyle: TextStyle(color: Colors.white38),
searchTextStyle: TextStyle(color: Colors.white),
searchIconColor: Colors.white38,
chipBackgroundColor: Colors.blueAccent.withValues(alpha: 0.2),
chipTextStyle: TextStyle(color: Colors.blueAccent),
chipBorderColor: Colors.blueAccent.withValues(alpha: 0.4),
loadingIndicatorColor: Colors.blueAccent,
checkboxActiveColor: Colors.blueAccent,
headerBackgroundColor: const Color(0xFF252535),
arrowIconColor: Colors.white54,
)
DropdownPlusTheme Reference #
| Property | Type | Default | Description |
|---|---|---|---|
backgroundColor |
Color? |
Colors.white |
Trigger button background |
borderColor |
Color? |
outline@50% |
Border colour when closed |
activeBorderColor |
Color? |
primary |
Border colour when open |
borderWidth |
double |
1.0 |
Border width when closed |
activeBorderWidth |
double |
1.5 |
Border width when open |
borderRadius |
double |
10.0 |
Trigger corner radius |
contentPadding |
EdgeInsets? |
h14 v12 |
Trigger inner padding |
hintStyle |
TextStyle? |
onSurface@60% |
Placeholder text style |
triggerTextStyle |
TextStyle? |
bodyMedium |
Selected value text style (single) |
menuBackgroundColor |
Color? |
Colors.white |
Panel background |
menuBorderRadius |
double |
12.0 |
Panel corner radius |
menuElevation |
double |
12.0 |
Panel shadow elevation |
menuMaxHeight |
double |
320.0 |
Panel max height |
menuBorderColor |
Color? |
outline@20% |
Panel border colour |
searchBarBackgroundColor |
Color? |
surface@30% |
Search input container |
searchHintStyle |
TextStyle? |
onSurface@50% |
Search hint |
searchTextStyle |
TextStyle? |
theme default | Search input text |
searchIconColor |
Color? |
onSurface@50% |
Search icon |
itemTextStyle |
TextStyle? |
onSurface 14sp |
Normal item text |
selectedItemTextStyle |
TextStyle? |
primary w500 |
Selected item text |
selectedItemBackgroundColor |
Color? |
primaryContainer@30% |
Selected item row bg |
itemPadding |
EdgeInsets? |
h16 v12 |
Item row padding |
dividerColor |
Color? |
outline@8% |
Divider between items |
checkboxBorderColor |
Color? |
outline@40% |
Circle checkbox border (unselected) |
checkboxActiveColor |
Color? |
primary |
Circle checkbox fill (selected) |
checkboxSize |
double |
22.0 |
Circle checkbox diameter |
chipBackgroundColor |
Color? |
primary@10% |
Chip background |
chipTextStyle |
TextStyle? |
primary w500 12sp |
Chip text |
chipBorderColor |
Color? |
primary@30% |
Chip border |
chipBorderRadius |
double |
16.0 |
Chip corner radius |
chipDeleteIconColor |
Color? |
primary |
Chip Γ icon colour |
chipDeleteIconSize |
double |
14.0 |
Chip Γ icon size |
countChipBackgroundColor |
Color? |
surfaceContainerHighest |
"+N more" chip background |
countChipTextStyle |
TextStyle? |
onSurface@70% |
"+N more" chip text |
loadingIndicatorColor |
Color? |
primary |
Spinner colour |
loadingTextStyle |
TextStyle? |
onSurface@60% 13sp |
Loading message style |
noResultsTextStyle |
TextStyle? |
onSurface@60% 13sp |
No results message style |
noResultsIconColor |
Color? |
onSurface@40% |
No results icon colour |
arrowIconColor |
Color? |
onSurface@60% |
Caret icon colour |
arrowIconSize |
double |
22.0 |
Caret icon size |
headerBackgroundColor |
Color? |
surface@30% |
Multi-select header row bg |
selectAllTextStyle |
TextStyle? |
primary w600 13sp |
"Select All" button style |
selectedCountTextStyle |
TextStyle? |
primary w600 11sp |
"N selected" badge text |
selectedCountBackgroundColor |
Color? |
primary@10% |
"N selected" badge bg |
API Reference #
SearchableDropdownPlus<C, S> #
| Parameter | Type | Required | Description |
|---|---|---|---|
cubit |
C |
β | BLoC/Cubit instance |
onSearch |
void Function(String) |
β | Called on every search change |
onStateChange |
void Function(S, updateList, updateLoading) |
β | Maps state to list/loading updates |
hintText |
String |
β | Placeholder text |
selectedValue |
DropdownItem? |
β | Pre-selected value (controlled mode) |
onSelectionChanged |
void Function(DropdownItem)? |
β | User selection callback |
searchHint |
String? |
β | Search input placeholder |
noResultsText |
String? |
β | Empty-state message |
loadingText |
String? |
β | Loading-state message |
needInitialFetch |
bool |
β | Trigger search on mount (default: false) |
dropdownTheme |
DropdownPlusTheme? |
β | Visual customisation |
themeStyle |
DropdownPlusThemeStyle? |
β | Preset style (ignored when dropdownTheme is set) |
itemBuilder |
Widget Function(item, isSelected)? |
β | Custom item row |
selectedValueBuilder |
Widget Function(item)? |
β | Custom trigger content |
checkInternetConnection |
Future<bool> Function()? |
β | Custom connectivity check |
MultiSelectDropdownPlus<C, S> #
All parameters from SearchableDropdownPlus plus:
| Parameter | Type | Default | Description |
|---|---|---|---|
selectedItems |
List<DropdownItem> |
[] |
Pre-selected items (controlled) |
onSelectionChanged |
void Function(List<DropdownItem>)? |
β | Selection change callback |
maxDisplayChips |
int |
2 |
Max chips before "+N more" overflow |
selectedItemBuilder |
Widget Function(List<DropdownItem>)? |
β | Custom chips display |
buttonHeight |
double? |
β | Fixed trigger height |
buttonWidth |
double? |
β | Fixed trigger width |
SearchableDropdown #
| Parameter | Type | Required | Description |
|---|---|---|---|
hintText |
String |
β | Placeholder text |
items |
List<DropdownItem> |
β | Items to show (update from parent when search results change) |
isLoading |
bool |
β | Shows loading UI in trigger and panel when empty |
onSearch |
void Function(String)? |
β | Remote search on each keystroke when online; omit for local-only filter |
selectedValue |
DropdownItem? |
β | Controlled selection |
onSelectionChanged |
void Function(DropdownItem)? |
β | User picked an item |
searchHint |
String? |
β | Search placeholder |
noResultsText |
String? |
β | Empty state text |
loadingText |
String? |
β | Loading message |
needInitialFetch |
bool |
β | If true and onSearch is set, calls onSearch('') on mount |
dropdownTheme |
DropdownPlusTheme? |
β | Theme overrides |
themeStyle |
DropdownPlusThemeStyle? |
β | Preset style |
itemBuilder |
Widget Function(item, isSelected)? |
β | Custom row |
selectedValueBuilder |
Widget Function(item)? |
β | Custom trigger when selected |
checkInternetConnection |
Future<bool> Function()? |
β | Offline β local filter |
MultiSelectDropdown #
Same parameters as SearchableDropdown, plus:
| Parameter | Type | Default | Description |
|---|---|---|---|
selectedItems |
List<DropdownItem> |
[] |
Controlled selection |
onSelectionChanged |
void Function(List<DropdownItem>)? |
β | Selection changed |
maxDisplayChips |
int |
2 |
Chips before "+N more" |
selectedItemBuilder |
Widget Function(List<DropdownItem>)? |
β | Custom chip row in trigger |
buttonHeight |
double? |
β | Fixed trigger height |
buttonWidth |
double? |
β | Fixed trigger width |
Offline Caching #
Provide checkInternetConnection to enable offline fallback:
SearchableDropdownPlus(
...
checkInternetConnection: () async {
final result = await Connectivity().checkConnectivity();
return result != ConnectivityResult.none;
},
)
When offline, the widget performs client-side filtering on the cached item list instead of calling onSearch.
Controlled Mode #
Use selectedValue / selectedItems to sync selection with external state (e.g. form reset, QR scan):
// For QR scan β increment key to force re-sync
SearchableDropdownPlus(
key: ValueKey(qrKey),
selectedValue: scannedItem,
...
)
Custom Builders #
Custom item row #
SearchableDropdownPlus(
...
itemBuilder: (item, isSelected) {
final worker = item.value as Worker;
return ListTile(
leading: CircleAvatar(child: Text(worker.name[0])),
title: Text(worker.name),
subtitle: Text(worker.department),
trailing: isSelected ? Icon(Icons.check, color: Colors.green) : null,
);
},
)
Custom selected chips (multi-select) #
MultiSelectDropdownPlus(
...
selectedItemBuilder: (selected) => Text(
selected.map((e) => e.label).join(' β’ '),
overflow: TextOverflow.ellipsis,
),
)
License #
MIT Β© Lidhin