dropdown_plus_bloc 1.4.0
dropdown_plus_bloc: ^1.4.0 copied to clipboard
Customizable Flutter dropdowns with BLoC/Cubit integration: searchable single-select and multi-select with chips, offline caching, and theming.
dropdown_plus_bloc #
Searchable single-select and multi-select Flutter dropdowns with optional BLoC/Cubit integration, theming, form fields, pagination, and an opt-in typed API.
| Widget | Description |
|---|---|
SearchableDropdownPlus |
Single-select with BLoC/Cubit |
MultiSelectDropdownPlus |
Multi-select with chips + BLoC/Cubit |
SearchableDropdown |
Single-select without BLoC (items / isLoading) |
MultiSelectDropdown |
Multi-select without BLoC |
*FormField |
Form wrappers with validator / onSaved |
Typed* (opt-in) |
Work with User (or any T) instead of DropdownItem |
import 'package:dropdown_plus_bloc/dropdown_plus_bloc.dart'; // default API
import 'package:dropdown_plus_bloc/typed.dart'; // typed API + controllers
Features #
- BLoC/Cubit or plain
items/isLoadingAPIs - Typed generics (
Typed*) — no manualDropdownItemmapping - Programmatic control —
DropdownPlusController/DropdownPlusMultiController - Form fields with validation
- Search with optional debounce and offline cache fallback
- Pagination via
onLoadMore/hasMore/isLoadingMore - Theming presets + full
DropdownPlusThemeoverrides - Custom builders, controlled selection, accessibility (
Semantics,focusNode)
Screenshots #
Single select #
Multi select #
Form field #
Installation #
dependencies:
dropdown_plus_bloc: ^1.4.0
flutter pub get
flutter_bloc is a transitive dependency (needed for *Plus widgets).
Quick start (BLoC) #
SearchableDropdownPlus<UsersCubit, UsersState>(
cubit: context.read<UsersCubit>(),
hintText: 'Select user…',
onSearch: (query) => context.read<UsersCubit>().search(query),
onStateChange: (state, updateList, updateLoading) {
if (state is UsersLoaded) {
updateList(
state.users
.map((u) => DropdownItem(value: u, label: u.name))
.toList(),
);
updateLoading(false);
} else if (state is UsersLoading) {
updateLoading(true);
}
},
onSelectionChanged: (item) {
final user = item.value as User;
},
)
Multi-select #
MultiSelectDropdownPlus<UsersCubit, UsersState>(
cubit: context.read<UsersCubit>(),
hintText: 'Select users…',
onSearch: (query) => context.read<UsersCubit>().search(query),
onStateChange: (state, updateList, updateLoading) {
if (state is UsersLoaded) {
updateList(
state.users
.map((u) => DropdownItem(value: u, label: u.name))
.toList(),
);
updateLoading(false);
}
},
onSelectionChanged: (items) {
final users = items.map((e) => e.value as User).toList();
},
)
Without BLoC #
Pass items and isLoading from your own state. Omit onSearch for local filtering over items, or provide onSearch and rebuild with new results.
SearchableDropdown(
hintText: 'Select user…',
items: userItems,
isLoading: isLoading,
selectedValue: selectedUserItem,
onSearch: (query) async {
setState(() => isLoading = true);
final list = await api.searchUsers(query);
setState(() {
userItems =
list.map((u) => DropdownItem(value: u, label: u.name)).toList();
isLoading = false;
});
},
onSelectionChanged: (item) =>
setState(() => selectedUserItem = item),
)
MultiSelectDropdown(
hintText: 'Select users…',
items: userItems,
isLoading: isLoading,
selectedItems: selectedUserItems,
onSelectionChanged: (items) =>
setState(() => selectedUserItems = items),
)
Typed API #
Opt-in import — use your domain type T directly:
import 'package:dropdown_plus_bloc/typed.dart';
TypedSearchableDropdownPlus<User, UsersCubit, UsersState>(
cubit: context.read<UsersCubit>(),
hintText: 'Select user…',
itemLabel: (user) => user.name,
itemEquals: (a, b) => a.id == b.id,
value: selectedUser,
onChanged: (user) => setState(() => selectedUser = user),
onSearch: (query) => context.read<UsersCubit>().search(query),
onStateChange: (state, updateItems, updateLoading) {
if (state is UsersLoaded) {
updateItems(state.users); // List<User>
updateLoading(false);
} else if (state is UsersLoading) {
updateLoading(true);
}
},
)
TypedSearchableDropdown<User>(
hintText: 'Select user…',
items: users,
isLoading: isLoading,
itemLabel: (u) => u.name,
value: selectedUser,
onChanged: (user) => setState(() => selectedUser = user),
)
| Default export | Typed export |
|---|---|
SearchableDropdownPlus<C, S> |
TypedSearchableDropdownPlus<T, C, S> |
MultiSelectDropdownPlus<C, S> |
TypedMultiSelectDropdownPlus<T, C, S> |
SearchableDropdown |
TypedSearchableDropdown<T> |
MultiSelectDropdown |
TypedMultiSelectDropdown<T> |
Bridge helper for gradual migration:
final items = users.toDropdownItems((u) => u.name);
More detail: doc/TYPED_API.md
Controller #
Programmatic select / clear / open / close (typed widgets):
final controller = DropdownPlusController<User>();
TypedSearchableDropdown<User>(
controller: controller,
hintText: 'Select user…',
items: users,
isLoading: false,
itemLabel: (u) => u.name,
onChanged: (user) => setState(() => selectedUser = user),
);
controller.select(user); // also calls onChanged
controller.clear();
controller.open();
controller.close();
Multi-select: DropdownPlusMultiController<T> with select, deselect, setValues, and clear.
When a controller is passed, it owns selection. Widget value / values are only used as an initial seed.
Form fields #
Form(
key: _formKey,
child: SearchableDropdownFormField(
hintText: 'Select user…',
items: userItems,
isLoading: false,
validator: (value) => value == null ? 'Required' : null,
onSaved: (item) => _savedUser = item?.value as User?,
),
)
| Widget | Wraps |
|---|---|
SearchableDropdownFormField |
SearchableDropdown |
SearchableDropdownPlusFormField<C, S> |
SearchableDropdownPlus |
MultiSelectDropdownFormField |
MultiSelectDropdown |
MultiSelectDropdownPlusFormField<C, S> |
MultiSelectDropdownPlus |
Shared params: validator, onSaved, autovalidateMode, initialValue / initialValues, enabled.
Pagination #
SearchableDropdownPlus<UsersCubit, UsersState>(
cubit: cubit,
hintText: 'Select user…',
hasMore: state.hasMore,
isLoadingMore: state.isLoadingMore,
onLoadMore: cubit.fetchNextPage,
onSearch: cubit.search,
onStateChange: (state, updateList, updateLoading) { /* ... */ },
)
Available on all four dropdown widgets and their typed counterparts.
Theming #
Presets #
SearchableDropdownPlus(
themeStyle: DropdownPlusThemeStyle.compact,
// ...
)
| Style | Look |
|---|---|
material |
Default Material-like |
minimal |
Light borders, subtle surfaces |
rounded |
Larger radius, soft panel |
outlined |
Strong border focus |
dark |
Dark surfaces |
compact |
Dense spacing |
dropdownTheme overrides themeStyle when both are set.
Custom theme #
dropdownTheme: DropdownPlusTheme(
backgroundColor: Colors.grey[100],
borderColor: Colors.grey[300],
activeBorderColor: Colors.deepPurple,
borderRadius: 12,
menuMaxHeight: 280,
selectedItemBackgroundColor: Colors.deepPurple.withValues(alpha: 0.08),
)
Override a preset selectively:
themeStyle: DropdownPlusThemeStyle.dark,
dropdownTheme: DropdownPlusThemePresets
.forStyle(DropdownPlusThemeStyle.dark)
.copyWith(borderRadius: 16),
See the DropdownPlusTheme table below for all properties.
Controlled mode #
Sync selection from outside (form reset, QR scan, etc.):
SearchableDropdownPlus(
key: ValueKey(qrKey), // bump key to force re-sync if needed
selectedValue: scannedItem,
// ...
)
Or use a controller with the typed API.
Custom builders #
itemBuilder: (item, isSelected) {
final user = item.value as User;
return ListTile(
leading: CircleAvatar(child: Text(user.name[0])),
title: Text(user.name),
trailing: isSelected ? const Icon(Icons.check) : null,
);
},
selectedItemBuilder: (selected) => Text(
selected.map((e) => e.label).join(' • '),
overflow: TextOverflow.ellipsis,
),
Also supported: emptyBuilder, loadingBuilder, errorBuilder, and (typed) valueBuilder / valuesBuilder.
Offline caching #
checkInternetConnection: () async {
final result = await Connectivity().checkConnectivity();
return result != ConnectivityResult.none;
},
When offline, the widget filters the cached list locally instead of calling onSearch.
Common parameters #
These apply across the main dropdown widgets (names vary slightly for multi / typed):
| Parameter | Description |
|---|---|
hintText |
Trigger placeholder |
enabled |
Disable open/search when false |
debounceDuration |
Delay before onSearch (default: none) |
minSearchLength |
Skip onSearch until query length ≥ N |
autofocusSearch |
Focus search field when panel opens |
searchHint / noResultsText / loadingText |
Panel copy |
emptyBuilder / loadingBuilder / errorBuilder |
Custom panel states |
error / onRetry |
Controlled error display |
onLoadMore / hasMore / isLoadingMore |
Pagination |
semanticsLabel / focusNode |
Accessibility |
dropdownTheme / themeStyle |
Appearance |
checkInternetConnection |
Offline fallback |
API reference #
SearchableDropdownPlus<C, S> #
| Parameter | Required | Description |
|---|---|---|
cubit |
✅ | BLoC/Cubit instance |
onSearch |
✅ | Called on search changes |
onStateChange |
✅ | Maps state → updateList / updateLoading |
hintText |
✅ | Placeholder |
selectedValue |
— | Controlled selection |
onSelectionChanged |
— | User pick callback |
needInitialFetch |
— | Call onSearch('') on mount |
itemBuilder / selectedValueBuilder |
— | Custom UI |
| (+ common parameters) |
MultiSelectDropdownPlus<C, S> #
Same as single-select Plus, plus:
| Parameter | Default | Description |
|---|---|---|
selectedItems |
[] |
Controlled selection |
onSelectionChanged |
— | (List<DropdownItem>) |
maxDisplayChips |
2 |
Chips before +N more |
selectedItemBuilder |
— | Custom chip row |
buttonHeight / buttonWidth |
— | Fixed trigger size |
SearchableDropdown / MultiSelectDropdown #
| Parameter | Required | Description |
|---|---|---|
hintText |
✅ | Placeholder |
items |
✅ | Current list |
isLoading |
✅ | Loading UI |
onSearch |
— | Remote search; omit for local filter |
selectedValue / selectedItems |
— | Controlled selection |
onSelectionChanged |
— | Selection callback |
| (+ multi extras & common parameters) |
Typed parameter map #
| Typed | Replaces |
|---|---|
itemLabel: (T) => String |
Manual DropdownItem(value:, label:) |
value / values |
selectedValue / selectedItems |
onChanged |
onSelectionChanged |
updateItems(List<T>) |
updateList(List<DropdownItem>) |
itemEquals |
Value equality |
controller |
Programmatic control |
DropdownPlusTheme reference #
| Property | Default | Description |
|---|---|---|
backgroundColor |
Colors.white |
Trigger background |
borderColor |
outline@50% |
Closed border |
activeBorderColor |
primary |
Open border |
borderWidth / activeBorderWidth |
1.0 / 1.5 |
Border widths |
borderRadius |
10.0 |
Trigger radius |
contentPadding |
h14 v12 |
Trigger padding |
hintStyle / triggerTextStyle |
theme | Trigger text |
menuBackgroundColor |
Colors.white |
Panel background |
menuBorderRadius / menuElevation |
12 / 12 |
Panel chrome |
menuMaxHeight |
320.0 |
Panel max height |
menuBorderColor |
outline@20% |
Panel border |
searchBarBackgroundColor |
surface@30% |
Search field bg |
searchHintStyle / searchTextStyle / searchIconColor |
theme | Search styling |
itemTextStyle / selectedItemTextStyle |
theme | Item text |
selectedItemBackgroundColor |
primaryContainer@30% |
Selected row |
itemPadding |
h16 v12 |
Item padding |
dividerColor |
outline@8% |
Dividers |
checkboxBorderColor / checkboxActiveColor / checkboxSize |
theme / 22 |
Multi-select checkbox |
chipBackgroundColor / chipTextStyle / chipBorderColor |
theme | Chips |
chipBorderRadius / chipDeleteIconColor / chipDeleteIconSize |
16 / theme / 14 |
Chip chrome |
countChipBackgroundColor / countChipTextStyle |
theme | +N more chip |
loadingIndicatorColor / loadingTextStyle |
theme | Loading |
noResultsTextStyle / noResultsIconColor |
theme | Empty state |
arrowIconColor / arrowIconSize |
theme / 22 |
Caret |
headerBackgroundColor |
surface@30% |
Multi header |
selectAllTextStyle |
theme | Select All |
selectedCountTextStyle / selectedCountBackgroundColor |
theme | Selected badge |
License #
MIT © Lidhin