使用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); }
问题原因分析
- 状态不可变性违反:直接修改
AppliesState实例的stores和filteredStores属性,而Bloc的核心是通过替换状态实例触发UI重建。修改现有状态属性无法被Bloc感知,导致UI不更新,仅热重载时才会重新读取状态。 - 模态框未监听状态变化:模态框初始化时接收的是静态
state参数,后续Cubit emit新状态时,模态框没有监听该变化,不会重建列表。 - 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
相关产品推荐
相关产品推荐

