You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Cubit实现列表搜索时状态无法实时更新的问题排查

问题:Flutter模态框搜索列表无法实时更新,仅热重载后生效

代码场景

1. 页面组件代码

class SelectStoreWidget extends StatelessWidget {
  final Function onNavigate;
  SelectStoreWidget({
    super.key,
    required this.onNavigate,
  });
  final cubit = AppliesCubit();
  final controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return SafeArea(
        child: BlocProvider<AppliesCubit>(
            create: (context) => cubit..fetchStores(),
            child: BlocConsumer<AppliesCubit, DataState>(
                listener: listener,
                builder: (context, state) {
                  if (state is LoadingState) {
                    return const CustomLoadingWidget();
                  }

                  if (state is AppliesState) {
                    return buildBody(state);
                  }
                  return Container();
                })));
  }

  buildBody(AppliesState state) {
    return Consumer<ChooseStoreProvider>(builder: (context, ctrl, child) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
        
          CustomTextField(
            onChangeValue: (val) {},
            hintText: AppStrings.selectGroceryField,
            
            readOnly: true,
            suffixIcon: AppImages.arrowDown,
            onTap: () {
              ModalHelper.showStores(context, (val) {
              }, state, cubit);
            },
            onlyTap: true,
          ),
        
          ),
        ],
      );
    });
  }
}

2. 模态框调用代码

static showStores(context, Function onTapYes, AppliesState state,
      AppliesCubit cubit) {
    showModalBottomSheet(
        backgroundColor: Colors.white,
        elevation: 0,
        isScrollControlled: true,
        context: context,
        builder: (_) => SelectStoreModal(
              onTapYes: onTapYes,
              state: state,
              cubit: cubit,
            ));
  }

3. 模态框组件代码(搜索功能)

class SelectStoreModal extends StatelessWidget {
  final Function onTapYes;
  final AppliesCubit cubit;
  final AppliesState state;
  const SelectStoreModal({
    super.key,
    required this.onTapYes,
    required this.state,
    required this.cubit,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding:
          EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
      child: Container(        
          child: Consumer<ChooseStoreProvider>(builder: (context, ctrl, child) {
            return SingleChildScrollView(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                mainAxisAlignment: MainAxisAlignment.start,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  CustomTextField(
                    hintStyle: TextStyle(
                        fontSize: Dimens.font14,
                        fontWeight: FontWeight.w400,
                        color: AppColors.black600),
                    onChangeValue: (value) {
                      print(value);
                     cubit.searchStores(value);
                    },
                    backgroundColor: AppColors.black100,
                    borderColor: Colors.transparent,
                    prefixIcon: AppImages.search,
                    hintText: AppStrings.search,
                  ),
                  SizedBox(
                    height: 24.h,
                  ),
                  ListView.builder(
                      shrinkWrap: true,
                      itemCount: state.filteredStores?.length,
                      itemBuilder: (ctx, i) {
                        final data = state.filteredStores?[i];
                        return Column(
                          children: [
                            InkWell(
                              onTap: () {
                                ctrl.select(i);
                              },
                              child: Row(
                                children: [
                                  ctrl.selectedStore == i
                                      ? SvgPicture.asset(AppImages.filledRadio)
                                      : SvgPicture.asset(AppImages.emptyRadio),
                                  SizedBox(
                                    width: 16.w,
                                  ),
                                  StoreComponent(
                                    data: data!,
                                  )
                                ],
                              ),
                            ),
                           
                          ],
                        );
                      }),
                 
                ],
              ),
            );
          })),
    );
  }
}

4. Cubit逻辑代码

Future<void> fetchStores() async {
    var currentState = state as AppliesState;

    emit(currentState);
    emit(LoadingState());

    var response = StoreModel.grocieries;
    currentState.stores = response;
    currentState.filteredStores = response;

    emit(SuccessState());
    emit(currentState);
  }

  Future<void> searchStores(String value) async {
    var currentState = state as AppliesState;
    
    emit(currentState);
    emit(LoadingState());

    currentState.filteredStores = currentState.stores.where((data) {
      final search = data.storeName ?? data.address;
      if (search != null) {
        return search.toLowerCase().contains(value.toLowerCase());
      } else {
        return false;
      }
    }).toList();
    emit(SuccessState());
    emit(currentState);
  }

问题原因分析

  1. 状态不可变性违反:直接修改AppliesState实例的stores和filteredStores属性,而Bloc的核心是通过替换状态实例触发UI重建。修改现有状态属性无法被Bloc感知,导致UI不更新,仅热重载时才会重新读取状态。
  2. 模态框未监听状态变化:模态框初始化时接收的是静态state参数,后续Cubit emit新状态时,模态框没有监听该变化,不会重建列表。
  3. emit顺序混乱:多次emit重复或不必要的状态(如先emit当前状态,再emit Loading,最后又emit当前状态),干扰状态流,导致UI无法正确响应有效更新。

修复方案

第一步:让AppliesState成为不可变类

添加copyWith方法,确保状态更新时创建新实例:

class AppliesState extends DataState {
  final List<StoreModel>? stores;
  final List<StoreModel>? filteredStores;

  const AppliesState({this.stores, this.filteredStores});

  AppliesState copyWith({
    List<StoreModel>? stores,
    List<StoreModel>? filteredStores,
  }) {
    return AppliesState(
      stores: stores ?? this.stores,
      filteredStores: filteredStores ?? this.filteredStores,
    );
  }
}

第二步:修改Cubit方法,使用不可变状态更新

Future<void> fetchStores() async {
  emit(LoadingState());

  var response = StoreModel.grocieries;
  emit(AppliesState(
    stores: response,
    filteredStores: response,
  ));
}

Future<void> searchStores(String value) async {
  emit(LoadingState());
  
  final currentState = state as AppliesState;
  final filteredStores = currentState.stores?.where((data) {
    final search = data.storeName ?? data.address;
    return search?.toLowerCase().contains(value.toLowerCase()) ?? false;
  }).toList();

  emit(currentState.copyWith(filteredStores: filteredStores));
}

第三步:修改模态框,监听Cubit状态变化

移除静态state参数,改用BlocConsumer监听状态:

class SelectStoreModal extends StatelessWidget {
  final Function onTapYes;
  final AppliesCubit cubit;

  const SelectStoreModal({
    super.key,
    required this.onTapYes,
    required this.cubit,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
      child: BlocProvider.value(
        value: cubit,
        child: BlocConsumer<AppliesCubit, DataState>(
          listener: (context, state) {},
          builder: (context, state) {
            if (state is LoadingState) {
              return const CustomLoadingWidget();
            }
            if (state is AppliesState) {
              return buildModalContent(context, state);
            }
            return Container();
          },
        ),
      ),
    );
  }

  Widget buildModalContent(BuildContext context, AppliesState state) {
    return Consumer<ChooseStoreProvider>(builder: (context, ctrl, child) {
      return SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            CustomTextField(
              hintStyle: TextStyle(
                  fontSize: Dimens.font14,
                  fontWeight: FontWeight.w400,
                  color: AppColors.black600),
              onChangeValue: (value) => cubit.searchStores(value),
              backgroundColor: AppColors.black100,
              borderColor: Colors.transparent,
              prefixIcon: AppImages.search,
              hintText: AppStrings.search,
            ),
            SizedBox(height: 24.h),
            ListView.builder(
              shrinkWrap: true,
              itemCount: state.filteredStores?.length ?? 0,
              itemBuilder: (ctx, i) {
                final data = state.filteredStores?[i];
                if (data == null) return const SizedBox();
                
                return Column(
                  children: [
                    InkWell(
                      onTap: () => ctrl.select(i),
                      child: Row(
                        children: [
                          ctrl.selectedStore == i
                              ? SvgPicture.asset(AppImages.filledRadio)
                              : SvgPicture.asset(AppImages.emptyRadio),
                          SizedBox(width: 16.w),
                          StoreComponent(data: data),
                        ],
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      );
    });
  }
}

第四步:修改模态框调用代码,移除state参数

static showStores(context, Function onTapYes, AppliesCubit cubit) {
  showModalBottomSheet(
    backgroundColor: Colors.white,
    elevation: 0,
    isScrollControlled: true,
    context: context,
    builder: (_) => SelectStoreModal(
      onTapYes: onTapYes,
      cubit: cubit,
    ),
  );
}

内容的提问来源于stack exchange,提问作者Bear

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 11:22:32