dropdown_plus_bloc 0.1.3
dropdown_plus_bloc: ^0.1.3 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 first-class BLoC / Cubit integration. Provides two ready-to-use widgets:
| Widget | Description |
|---|---|
SearchableDropdownPlus |
Single-select searchable dropdown |
MultiSelectDropdownPlus |
Multi-select dropdown with chip display |
Features #
- π BLoC / Cubit integration β pass any
CubitorBlocand let the widget react to state changes automatically - π 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
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
},
)
Theme Customisation #
Pass a DropdownPlusTheme to either widget 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 |
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