Flutter Bloc已emit状态,但BlocBuilder未更新数据问题求助
Flutter Bloc搜索功能在Firebase真实数据下失效问题
问题现象
- 通过StreamBuilder从Firebase获取数据正常,数据已存入visibleItem列表
- 搜索框
onChanged触发时,SearchBloc的emit操作执行正常,但BlocBuilder未接收到新状态 - 使用模拟示例数据时功能正常,改用Firebase真实数据后搜索失效
相关代码
CategoryScreen页面代码
visibleItems = searchState.results.where((item) => item.isVisible).toList(); // 此处判断item的isVisible状态,该状态在SearchBloc中更新
Widget build(BuildContext context) { return StreamBuilder( stream: categoryBloc.categories, builder: (context, AsyncSnapshot<List<CategoryModel>> snapshot) { if (snapshot.hasData) { _searchBloc = SearchBloc(snapshot.data ?? []); return _buildScreen(); } else { return const Center(child: CircularProgressIndicator()); } }); } Widget _buildScreen() { final double appBarHeight = MediaQuery.of(context).padding.top + kToolbarHeight; return WillPopScope( onWillPop: () async { return _showDiscardWidget(context); }, child: MultiBlocProvider( providers: [ BlocProvider<ItemSelectionCubit>(create: (_) => ItemSelectionCubit()), BlocProvider<SearchBloc>(create: (_) => _searchBloc), ], child: BlocBuilder<SearchBloc, SearchState>( builder: (context, searchState) { final bool shouldDisplayBackgorund = _searchController.text.isEmpty && MediaQuery.of(context).viewInsets.bottom == 0; visibleItems = searchState.results.where((item) => item.isVisible).toList(); return BlocBuilder<ItemSelectionCubit, ItemSelectionState>( builder: (context, itemSelectionState) { return Scaffold( backgroundColor: Colors.white, body: SafeArea( top: false, child: CustomScrollView( controller: _scrollController, slivers: [ CustomSliverAppBar( maxExtent: shouldDisplayBackgorund ? appBarBackgroundHeight : appBarHeight, minExtent: appBarHeight, appBar: _popupAppbar( appBarHeight, itemSelectionState, context), background: shouldDisplayBackgorund ? _backgroundAppbar(itemSelectionState, context) : null, ), SliverList( delegate: SliverChildListDelegate( [ visibleItems.isNotEmpty ? CategoryItemWidget(itemSelectionState, visibleItems, searchState.query) : NoResultFoundWidget( resultQuery: searchState.query ?? ""), ], ), ), ], ), ), ); }, ); }, ), ), ); } Widget _popupAppbar(double appBarHeight, ItemSelectionState itemSelectionState, BuildContext context) { return Container( height: appBarHeight, width: double.infinity, decoration: const BoxDecoration(color: Colors.white, boxShadow: [ BoxShadow(color: Colors.black26, offset: Offset(1, 0), blurRadius: 5), ]), child: Align( alignment: Alignment.bottomCenter, child: SearchAppBar( hint: 'Search Category', onChanged: (query) { _searchBloc.add(SearchQueryChanged(query: query)); }, onPressedPrefix: () { Navigator.of(context).pop(); }, onPressedSuffix: () { BlocProvider.of<ItemSelectionCubit>(context).toggleView(); }, searchController: _searchController, onPressedSuffixInSearchBar: () { _searchController.clear(); _searchBloc.add(SearchCleared()); }, suffixIconPath: itemSelectionState.isGrid ? AppAssetsPath.gridIcon : AppAssetsPath.listIcon, isExpanding: visibleItems.isEmpty, ), ), ); }
// 搜索查询变更触发点 onChanged: (query) { _searchBloc.add(SearchQueryChanged(query: query)); },
SearchBloc搜索逻辑代码
class SearchBloc extends Bloc<SearchEvent, SearchState> { final List<CategoryModel> _items; SearchBloc(this._items) : super(SearchState( query: null, results: _items.map((item) => item..isVisible = true).toList())) { on<SearchQueryChanged>((event, emit) { emit(_mapSearchQueryChangedToState(event.query)); }); on<SearchCleared>((event, emit) { emit(_mapSearchClearedToState()); }); } void onSearchQueryChanged(String query) { add(SearchQueryChanged( query: query, )); } void onSearchCleared() { add(SearchCleared()); } SearchState _mapSearchQueryChangedToState(String query) { final results = _items.map((item) { item.isVisible = item.displayName!.toLowerCase().contains(query.toLowerCase()); return item; }).toList(); return SearchState(query: query, results: results); } SearchState _mapSearchClearedToState() { final results = _items.map((item) { item.isVisible = true; return item; }).toList(); return SearchState(query: null, results: results); } }
class SearchState { final String? query; final List<CategoryModel> results; SearchState({this.query, required this.results}); }
abstract class SearchEvent { const SearchEvent(); } class SearchQueryChanged extends SearchEvent { final String query; const SearchQueryChanged({required this.query}); } class SearchCleared extends SearchEvent {}
CategoryBloc Firebase数据获取代码
class CategoryBloc { final _categoryController = StreamController<List<CategoryModel>>.broadcast(); Stream<List<CategoryModel>> get categories => _categoryController.stream; Future<void> fetchCategories() async { try { await getData<CategoryModel>( path: 'common', builder: (data) => CategoryModel.fromJson(data)) .then((value) { _categoryController.sink.add(value); }); } catch (error) { print(error); _categoryController.sink.addError(error); } } void dispose() { _categoryController.close(); } }
Future<List<T>> getData<T>( {required String path, required T Function(Map<String, dynamic>) builder}) async { List<T> list = []; try { final response = await FirebaseFirestore.instance .collection(path) .doc('onboardingCategories') .get(); final data = response.data(); final List<dynamic> rawData = data!['categories']; list = rawData.map((item) => builder(item)).toList(); return list; } on FirebaseException catch (e) { if (kDebugMode) { print("Failed with error '${e.code}': ${e.message}"); } return list; } catch (e) { throw Exception(e.toString()); } }
问题原因分析
- Bloc实例不一致:在StreamBuilder的builder回调中每次重建都会重新创建
_searchBloc,但BlocProvider使用的是外部的_searchBloc,页面重建时会导致触发事件的Bloc和BlocBuilder监听的Bloc不是同一个实例。 - 状态不可变性缺失:SearchState直接修改原CategoryModel对象的
isVisible属性,Bloc依赖状态的不可变性检测变化,Firebase返回的对象可能被框架视为不可变,直接修改属性无法触发状态变化检测。 - StreamBuilder重建导致Bloc丢失:StreamBuilder的builder会在数据变化或页面重建时触发,每次都会创建新的SearchBloc,旧实例被丢弃,后续事件发送到新实例但BlocBuilder仍监听旧实例。
修复方案
方案1:统一Bloc实例管理
将SearchBloc的创建移到BlocProvider内部,避免重复创建:
Widget build(BuildContext context) { return StreamBuilder( stream: categoryBloc.categories, builder: (context, AsyncSnapshot<List<CategoryModel>> snapshot) { if (snapshot.hasData) { return _buildScreen(snapshot.data!); } else { return const Center(child: CircularProgressIndicator()); } }); } Widget _buildScreen(List<CategoryModel> categories) { final double appBarHeight = MediaQuery.of(context).padding.top + kToolbarHeight; return WillPopScope( onWillPop: () async => _showDiscardWidget(context), child: MultiBlocProvider( providers: [ BlocProvider<ItemSelectionCubit>(create: (_) => ItemSelectionCubit()), // 直接在BlocProvider内创建SearchBloc,保证实例唯一 BlocProvider<SearchBloc>(create: (_) => SearchBloc(categories)), ], child: BlocBuilder<SearchBloc, SearchState>( builder: (context, searchState) { final bool shouldDisplayBackgorund = _searchController.text.isEmpty && MediaQuery.of(context).viewInsets.bottom == 0; final visibleItems = searchState.results.where((item) => item.isVisible).toList(); return BlocBuilder<ItemSelectionCubit, ItemSelectionState>( builder: (context, itemSelectionState) { return Scaffold( backgroundColor: Colors.white, body: SafeArea( top: false, child: CustomScrollView( controller: _scrollController, slivers: [ CustomSliverAppBar( maxExtent: shouldDisplayBackgorund ? appBarBackgroundHeight : appBarHeight, minExtent: appBarHeight, appBar: _popupAppbar(appBarHeight, itemSelectionState, context), background: shouldDisplayBackgorund ? _backgroundAppbar(itemSelectionState, context) : null, ), SliverList( delegate: SliverChildListDelegate( [ visibleItems.isNotEmpty ? CategoryItemWidget(itemSelectionState, visibleItems, searchState.query) : NoResultFoundWidget(resultQuery: searchState.query ?? ""), ], ), ), ], ), ), ); }, ); }, ), ), ); }
同时修改搜索事件触发方式,从上下文获取Bloc实例:
onChanged: (query) { BlocProvider.of<SearchBloc>(context).add(SearchQueryChanged(query: query)); }, onPressedSuffixInSearchBar: () { _searchController.clear(); BlocProvider.of<SearchBloc>(context).add(SearchCleared()); },
方案2:保证状态不可变性
修改SearchBloc逻辑,创建CategoryModel副本而非直接修改原对象:
// 先给CategoryModel添加copyWith方法 class CategoryModel { final String? displayName; final bool isVisible; CategoryModel({this.displayName, this.isVisible = false}); CategoryModel copyWith({ String? displayName, bool? isVisible, }) { return CategoryModel( displayName: displayName ?? this.displayName, isVisible: isVisible ?? this.isVisible, ); } factory CategoryModel.fromJson(Map<String, dynamic> json) { return CategoryModel( displayName: json['displayName'], isVisible: json['isVisible'] ?? false, ); } }
更新SearchBloc的状态生成逻辑:
SearchState _mapSearchQueryChangedToState(String query) { final results = _items.map((item) { final isVisible = item.displayName!.toLowerCase().contains(query.toLowerCase()); return item.copyWith(isVisible: isVisible); }).toList(); return SearchState(query: query, results: results); } SearchState _mapSearchClearedToState() { final results = _items.map((item) { return item.copyWith(isVisible: true); }).toList(); return SearchState(query: null, results: results); }
内容的提问来源于stack exchange,提问作者Harshit Kumawat
相关产品推荐
相关产品推荐

