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

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());
    }

  }
}

问题分析

  1. _searchText更新未触发UI重建:_searchText是State类的成员变量,但更新时没有调用setState,导致依赖_searchText的UI部分(如AppBar底部的TabBar、body的条件渲染)无法刷新。即使BLoC状态更新,搜索结果对应的BlocBuilder可能根本没被渲染到界面上。

  2. 传递给Event的query是旧值:TextField的onChanged回调中使用_searchText作为查询参数,但_searchText是在TextEditingController的listener中更新的,listener的执行时机晚于onChanged,导致传递的是更新前的旧值,BLoC处理的查询内容不正确。

  3. BlocBuilder未处理完整状态:搜索结果的BlocBuilder没有处理AllbuildingsInitial、SearchbuildingNoDataState等状态,会错误显示"Failed to load buildings",同时初始状态下也无法正确展示内容。

  4. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 07:45:38