最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

dart - Flutter setState Not Rebuilding DropdownSearch with Lazy Loading - Stack Overflow

programmeradmin5浏览0评论

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
Add a comment  | 

2 Answers 2

Reset to default 0

setState(() {}) 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,
    ),
  ),
);
发布评论

评论列表(0)

  1. 暂无评论