Flutter中BLoC emit状态无法更新UI问题求助
开发了包含AppBar、Tabs和TabView的页面,AppBar内设有TextField。每次文本内容变化时,代码会进入BLoC的emit方法,但UI始终无法接收到更新后的状态。
相关代码
Widget代码
class Buildings extends StatefulWidget { const Buildings({super.key}); @override State<StatefulWidget> createState() => _BuildingState(); } class _BuildingState extends State<Buildings> { late TextEditingController _searchController; late NearbybuildingsBloc nearbybuildingsBloc; late AllbuildingsBloc allbuildingsBloc; late String _searchText=''; late BuildContext allBuildingContext; @override void initState() { nearbybuildingsBloc = NearbybuildingsBloc(); allbuildingsBloc = AllbuildingsBloc(); _searchController = TextEditingController(); _searchController.addListener(() { _searchText = _searchController.text; }); super.initState(); } @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider<NearbybuildingsBloc>(create: (context) => nearbybuildingsBloc..add(NearbyBuildings())), BlocProvider<AllbuildingsBloc>(create: (context) => allbuildingsBloc..add(AllBuildings())) ], child: DefaultTabController( length: 2, child: Scaffold( appBar: AppBar( backgroundColor: AppColor.bt_indigo, title: TextField( onChanged: (value) { allbuildingsBloc.add( SearchbuildingLoadingEvent(query: _searchText), ); }, style: const TextStyle(color: Colors.white), controller: _searchController, decoration: InputDecoration( suffixIcon: _searchText.isEmpty ? null :IconButton( icon: const Icon(Icons.clear), color: Colors.white, onPressed: () => _searchController.clear(), ), isDense: true, hintText: 'Building name', hintStyle: const TextStyle(color: AppColor.border_color), enabledBorder: const OutlineInputBorder( borderSide: BorderSide( width: 2, color: AppColor.text_editor_background), borderRadius: BorderRadius.all( Radius.circular(10.0)), ), focusedBorder: const OutlineInputBorder( borderSide: BorderSide( width: 2, color: AppColor.text_editor_background), borderRadius: BorderRadius.all( Radius.circular(10.0)), ), fillColor: AppColor.text_editor_background,), ), actions: [ NamedIcon( text: '', iconData: Icons.filter_list_alt, onTap: () { /*showDialog( context: context, barrierDismissible: true, builder: (BuildContext buildContext) { return StatefulBuilder( builder: (buildContext, state) { return _openFilterCategoryDialog( filterCategories, context, state); }); });*/ }, ), NamedIcon( text: '', iconData: Icons.help, notificationCount: 1, onTap: () { }, )], bottom: _searchText.isEmpty? TabBar( tabs: [ Tab(text: 'Near by'), Tab(text: 'All Buildings') ], ):null, ), body: _searchText.isEmpty?TabBarView( children: [ // Nearby buildings BlocBuilder<NearbybuildingsBloc, NearbybuildingsState>( builder: (context, state) { if( state is NearbybuildingsLoading){ return const Center(child: CircularProgressIndicator( color: AppColor.bt_indigo, )); } if(state is NearbybuildingsGpsPerm){ return row.getGPS(context); } if (state is NearbybuildingsLoaded) { return Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Text("Showing ${state.nearBuildingList.length.toString()} buildings ", style: const TextStyle( fontSize: 13, color: AppColor.bt_indigo, fontFamily: 'BT Regular'), ), Expanded( child: ListView.builder( itemCount: state.nearBuildingList.length, itemBuilder: (BuildContext context, int position) { return row.getRow(state.nearBuildingList[position], context); }), ) ], ); } return const Text("Failed to load buildings"); }, ), // All buildings BlocBuilder<AllbuildingsBloc, AllbuildingsState>( builder: (context, state) { if( state is AllbuildingsError){ return const Text("Failed to load buildings"); } if (state is AllbuildingsLoaded) { return Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Text("Showing ${state.all.length.toString()} buildings ", style: const TextStyle( fontSize: 13, color: AppColor.bt_indigo, fontFamily: 'BT Regular'), ), Expanded( child: ListView.builder( itemCount: state.all.length, itemBuilder: (BuildContext context, int position) { return row.getRow(state.all[position], context); }), ) ], ); } return const Text("Failed to load buildings"); }, ), ], ): // All buildings BlocBuilder<AllbuildingsBloc, AllbuildingsState>( builder: (context, state) { if( state is SearchbuildingLoadingState){ return const Center(child: CircularProgressIndicator( color: AppColor.bt_indigo, )); } if (state is SearchBuildingLoadedState) { return Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Text("Showing ${state.searchresults.length.toString()} buildings ", style: const TextStyle( fontSize: 13, color: AppColor.bt_indigo, fontFamily: 'BT Regular'), ), Expanded( child: ListView.builder( itemCount: state.searchresults.length, itemBuilder: (BuildContext context, int position) { return row.getRow(state.searchresults[position], context); }), ) ], ); } return const Text("Failed to load buildings"); }, ), // This trailing comma makes auto-formatting nicer for build methods. ), ), ); } }
State代码
class SearchBuildingLoadedState extends AllbuildingsState{ List<Building> searchresults; SearchBuildingLoadedState({required this.searchresults}); @override List<Object> get props { return [searchresults]; } }
Event代码
class SearchbuildingLoadingEvent extends AllbuildingsEvent { final String query; SearchbuildingLoadingEvent({required this.query}); @override List<Object> get props { return [query]; } }
BLoC代码
class AllbuildingsBloc extends Bloc<AllbuildingsEvent, AllbuildingsState> { AllbuildingsBloc() : super(AllbuildingsInitial()) { on<SearchbuildingLoadingEvent>(_onSearchBuilding); } Future<void> _onSearchBuilding(SearchbuildingLoadingEvent event, Emitter<AllbuildingsState> emit) async { emit(SearchbuildingLoadingState()); if(event.query.isNotEmpty || event.query.isNotEmpty) { final database = await $FloorAppDataBase .databaseBuilder('bootcamp-instagram-project.db') .build(); final buildingDao = database.buildingDao; var getBuilding = await buildingDao.getSearchBuildings( event.query); if (getBuilding != null) { emit(SearchBuildingLoadedState(searchresults: getBuilding)); } else { emit(SearchbuildingNoDataState()); } }else{ emit(SearchbuildingNoDataState()); } } }
问题分析
_searchText更新未触发UI重建:
_searchText是State类的成员变量,但更新时没有调用setState,导致依赖_searchText的UI部分(如AppBar底部的TabBar、body的条件渲染)无法刷新。即使BLoC状态更新,搜索结果对应的BlocBuilder可能根本没被渲染到界面上。传递给Event的query是旧值:TextField的
onChanged回调中使用_searchText作为查询参数,但_searchText是在TextEditingController的listener中更新的,listener的执行时机晚于onChanged,导致传递的是更新前的旧值,BLoC处理的查询内容不正确。BlocBuilder未处理完整状态:搜索结果的BlocBuilder没有处理
AllbuildingsInitial、SearchbuildingNoDataState等状态,会错误显示"Failed to load buildings",同时初始状态下也无法正确展示内容。BLoC实例管理不规范:在initState中手动创建BLoC实例再传入BlocProvider,不符合Bloc的最佳实践,可能导致上下文关联问题。
解决方案
1. 修复_searchText的更新逻辑
去掉TextEditingController的listener,直接在onChanged中更新_searchText并调用setState,确保UI能及时刷新:
TextField( onChanged: (value) { setState(() { _searchText = value; }); BlocProvider.of<AllbuildingsBloc>(context).add( SearchbuildingLoadingEvent(query: value), // 直接用当前输入的value ); }, // 其他属性不变 )
同时修改clear按钮的逻辑,确保清空后触发状态更新:
IconButton( icon: const Icon(Icons.clear), color: Colors.white, onPressed: () { _searchController.clear(); setState(() { _searchText = ''; }); BlocProvider.of<AllbuildingsBloc>(context).add(AllBuildings()); // 清空后重新加载全部建筑 }, )
2. 完善BlocBuilder的状态处理
更新搜索结果的BlocBuilder,处理所有可能的状态:
BlocBuilder<AllbuildingsBloc, AllbuildingsState>( builder: (context, state) { if (state is SearchbuildingLoadingState) { return const Center(child: CircularProgressIndicator(color: AppColor.bt_indigo)); } if (state is SearchBuildingLoadedState) { return Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Text("Showing ${state.searchresults.length.toString()} buildings ", style: const TextStyle( fontSize: 13, color: AppColor.bt_indigo, fontFamily: 'BT Regular'), ), Expanded( child: state.searchresults.isEmpty ? const Text("No buildings found") : ListView.builder( itemCount: state.searchresults.length, itemBuilder: (BuildContext context, int position) { return row.getRow(state.searchresults[position], context); }), ) ], ); } if (state is SearchbuildingNoDataState) { return const Text("No buildings found"); } if (state is AllbuildingsInitial) { return const Center(child: CircularProgressIndicator(color: AppColor.bt_indigo)); } return const Text("Failed to load buildings"); }, )
3. 优化BLoC实例创建
让BlocProvider负责创建BLoC实例,避免手动管理的问题:
MultiBlocProvider( providers: [ BlocProvider<NearbybuildingsBloc>( create: (context) => NearbybuildingsBloc()..add(NearbyBuildings()) ), BlocProvider<AllbuildingsBloc>( create: (context) => AllbuildingsBloc()..add(AllBuildings()) ) ], // 子组件不变 )
同时删除State类中手动创建的nearbybuildingsBloc和allbuildingsBloc变量,通过BlocProvider.of<AllbuildingsBloc>(context)获取实例。
4. 修复BLoC中的冗余判断
BLoC中的if(event.query.isNotEmpty || event.query.isNotEmpty)是重复判断,改为:
if(event.query.isNotEmpty) { // 原有逻辑 } else { emit(SearchbuildingNoDataState()); }
额外优化建议
- 为搜索添加防抖处理,避免每次输入字符都触发数据库查询,提升性能:可以使用
Timer延迟触发搜索事件。 - 关闭数据库连接:在BLoC中创建的数据库实例使用后应关闭,避免资源泄漏,可以在BLoC的
close方法中处理。
内容的提问来源于stack exchange,提问作者Rahul Matte

