I am using DropdownSearch with lazy loading inside a modal bottom sheet in Flutter. The list of items is fetched using Bloc (LocationMasterCubit), and I am handling infinite scrolling to load more items dynamically.
The problem is that when I call setState(() {}); after fetching more items, the dropdown list does not update correctly. It seems like setState is not triggering a rebuild for the dropdown items.
Code Snippet: Here’s how I implemented the lazy loading inside DropdownSearch:
BlocBuilder<LocationMasterCubit, LocationMasterState>(
builder: (context, state) {
return DropdownSearch<String>(
suffixProps: DropdownSuffixProps(
dropdownButtonProps: DropdownButtonProps(
iconClosed: const Icon(Icons.keyboard_arrow_down),
iconOpened: const Icon(Icons.keyboard_arrow_up),
iconSize: 20.sp,
color: ColorManager.black,
),
),
selectedItem: locationMaster?.costCenter1 ?? costCenter1,
decoratorProps: const DropDownDecoratorProps(
decoration: InputDecoration(labelText: 'Cost Center 1'),
),
items: (f, loadProps) => locationMasterCubit.costCenter1?.result ?? [],
itemAsString: (item) => item,
onChanged: (selectedItem) {
setState(() {
costCenter1 = selectedItem!;
});
},
popupProps: PopupProps.menu(
itemBuilder: (context, item, isDisabled, isSelected) {
return ListTile(
title: Text(item),
);
},
constraints: BoxConstraints(maxHeight: 150.h),
listViewProps: ListViewProps(
controller: scrollController,
),
searchFieldProps: TextFieldProps(
decoration: const InputDecoration(hintText: 'Search...'),
onChanged: (value) {
locationMasterCubit.getCostCenter1(search: value, page: 1);
},
),
showSearchBox: true,
),
);
},
);
ScrollController (Handling Pagination):
scrollController.addListener(() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent) {
currentPage++;
await locationMasterCubit.getCostCenter1(page: currentPage);
setState(() {}); // Not updating the dropdown items
}
});
Issue: When I scroll to the bottom, getCostCenter1(page: currentPage) fetches more items successfully.
However, DropdownSearch does not update to show the newly fetched items.
setState(() {}) is called, but it does not trigger a rebuild.
Cubit logic:
Future<void> getCostCenter1({String? search, required int page}) async {
try {
if (!isSearching) {
isSearching = true;
page == 1 ? emit(_FetchingData()) : emit(_FetchingNextData());
if (page == 1) costCenter1 = null;
final response = await locationMasterApiService.getCostCenter1(
search: search, page: page);
final costCenter1List = <String>[
if (costCenter1 != null) ...costCenter1!.result,
...response['items'].map((cost) => cost)
];
costCenter1 =
PaginatedResponse<String>.fromJson(response, costCenter1List);
isSearching = false;
emit(_FetchedData());
}
} on ApiException catch (e) {
isSearching = false;
emit(
_LocationMasterFailed(
message: e.maybeWhen(
connectionException: (message, context) => message,
basic: (message, context) => message,
orElse: () => e.context!,
),
),
);
}
}
Question:
How can I make sure DropdownSearch updates properly when new items are fetched? Should I use StatefulBuilder, BlocListener, or another approach?
Any help would be appreciated!
I am using DropdownSearch with lazy loading inside a modal bottom sheet in Flutter. The list of items is fetched using Bloc (LocationMasterCubit), and I am handling infinite scrolling to load more items dynamically.
The problem is that when I call setState(() {}); after fetching more items, the dropdown list does not update correctly. It seems like setState is not triggering a rebuild for the dropdown items.
Code Snippet: Here’s how I implemented the lazy loading inside DropdownSearch:
BlocBuilder<LocationMasterCubit, LocationMasterState>(
builder: (context, state) {
return DropdownSearch<String>(
suffixProps: DropdownSuffixProps(
dropdownButtonProps: DropdownButtonProps(
iconClosed: const Icon(Icons.keyboard_arrow_down),
iconOpened: const Icon(Icons.keyboard_arrow_up),
iconSize: 20.sp,
color: ColorManager.black,
),
),
selectedItem: locationMaster?.costCenter1 ?? costCenter1,
decoratorProps: const DropDownDecoratorProps(
decoration: InputDecoration(labelText: 'Cost Center 1'),
),
items: (f, loadProps) => locationMasterCubit.costCenter1?.result ?? [],
itemAsString: (item) => item,
onChanged: (selectedItem) {
setState(() {
costCenter1 = selectedItem!;
});
},
popupProps: PopupProps.menu(
itemBuilder: (context, item, isDisabled, isSelected) {
return ListTile(
title: Text(item),
);
},
constraints: BoxConstraints(maxHeight: 150.h),
listViewProps: ListViewProps(
controller: scrollController,
),
searchFieldProps: TextFieldProps(
decoration: const InputDecoration(hintText: 'Search...'),
onChanged: (value) {
locationMasterCubit.getCostCenter1(search: value, page: 1);
},
),
showSearchBox: true,
),
);
},
);
ScrollController (Handling Pagination):
scrollController.addListener(() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent) {
currentPage++;
await locationMasterCubit.getCostCenter1(page: currentPage);
setState(() {}); // Not updating the dropdown items
}
});
Issue: When I scroll to the bottom, getCostCenter1(page: currentPage) fetches more items successfully.
However, DropdownSearch does not update to show the newly fetched items.
setState(() {}) is called, but it does not trigger a rebuild.
Cubit logic:
Future<void> getCostCenter1({String? search, required int page}) async {
try {
if (!isSearching) {
isSearching = true;
page == 1 ? emit(_FetchingData()) : emit(_FetchingNextData());
if (page == 1) costCenter1 = null;
final response = await locationMasterApiService.getCostCenter1(
search: search, page: page);
final costCenter1List = <String>[
if (costCenter1 != null) ...costCenter1!.result,
...response['items'].map((cost) => cost)
];
costCenter1 =
PaginatedResponse<String>.fromJson(response, costCenter1List);
isSearching = false;
emit(_FetchedData());
}
} on ApiException catch (e) {
isSearching = false;
emit(
_LocationMasterFailed(
message: e.maybeWhen(
connectionException: (message, context) => message,
basic: (message, context) => message,
orElse: () => e.context!,
),
),
);
}
}
Question:
How can I make sure DropdownSearch updates properly when new items are fetched? Should I use StatefulBuilder, BlocListener, or another approach?
Any help would be appreciated!
Share Improve this question edited Mar 26 at 8:20 elvril asked Mar 25 at 17:32 elvrilelvril 12 bronze badges 3- I don't think you should be using setState if you have the Cubits managing the state. For a bit more clarification, could you edit it to add the getCostCenter1 event handler. – ParaPsychic Commented Mar 25 at 18:02
- so i added for you the event handler but i found somehow a solutions that works (see the second solution), waiting for a better solution. thank you! – elvril Commented Mar 26 at 8:22
- @elvril Why is the _FetchedData state empty? it should have the fields (literally, the state) to hold data. looking back on the code, I realized that you are not using the state at all. There is no need to setState if you are using BlocBuilder correct. The Cubit should emit states, and the BlocBuilder should use the state while building the widget. While emiting a new state, the BlocBuilder will also reactively rebuild. Please see some example apps that use Cubit. – ParaPsychic Commented Mar 26 at 10:26
2 Answers
Reset to default 0setState(() {})
only triggers a rebuild for the widget it is inside, but DropdownSearch
is getting its items from locationMasterCubit.costCenter1?.result ?? []
, which is outside setState
’s scope.
Instead of relying on setState
, use BlocListener
to trigger UI updates when the costCenter1
list is updated.
BlocConsumer<LocationMasterCubit, LocationMasterState>(
listener: (context, state) {
// When new items are fetched, trigger UI update
if (state is LocationMasterLoaded) {
setState(() {}); // Forces dropdown to refresh with new data
}
},
builder: (context, state) {
return DropdownSearch<String>(
suffixProps: DropdownSuffixProps(
dropdownButtonProps: DropdownButtonProps(
iconClosed: const Icon(Icons.keyboard_arrow_down),
iconOpened: const Icon(Icons.keyboard_arrow_up),
iconSize: 20.sp,
color: ColorManager.black,
),
),
........ and more............
Removed setState(() {})
from scrollController.addListener
: The UI will now rebuild based on Bloc state changes instead.
Initially, I thought the problem was due to incorrect usage of setState
, but after checking the logs and read the code of the package, I found that the package itself does not automatically refresh the dropdown items when the state changes.
The dropdown_search
package has lazy loading with skip
and take
parameters, but my data structure wasn’t suitable for using skip
. After extensive research, I found a workaround by using a key and manually calling reloadItems()
.
By adding a GlobalKey<DropdownSearchState<String>>
and calling reloadItems()
inside BlocListener
, the dropdown correctly updates when the Cubit fetches new data.
final dropdownKey = GlobalKey<DropdownSearchState<String>>();
BlocListener<LocationMasterCubit, LocationMasterState>(
listener: (context, state) {
state.whenOrNull(fetchedData: () {
dropdownKey.currentState?.getPopupState?.reloadItems('');
});
},
child: DropdownSearch<String>(
key: dropdownKey, // Assign the key here
suffixProps: DropdownSuffixProps(
dropdownButtonProps: DropdownButtonProps(
iconClosed: const Icon(Icons.keyboard_arrow_down),
iconOpened: const Icon(Icons.keyboard_arrow_up),
iconSize: 20.sp,
color: ColorManager.black,
),
),
selectedItem: locationMaster?.costCenter1 ?? costCenter1,
decoratorProps: const DropDownDecoratorProps(
decoration: InputDecoration(labelText: 'Cost Center 1'),
),
items: (f, loadProps) => locationMasterCubit.costCenter1?.result ?? [],
itemAsString: (item) => item,
onSelected: (selectedItem) {
setState(() {
costCenter1 = selectedItem!;
});
},
validator: (value) => (value == null || value.isEmpty) ? "Required field" : null,
popupProps: PopupProps.menu(
constraints: BoxConstraints(maxHeight: 150.h),
listViewProps: ListViewProps(
controller: scrollController,
),
searchFieldProps: TextFieldProps(
decoration: const InputDecoration(hintText: 'Search...'),
onSelected: (value) {
locationMasterCubit.getCostCenter1(search: value, page: 1);
},
),
showSearchBox: true,
),
),
);