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

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

问题原因分析

  1. Bloc实例不一致:在StreamBuilder的builder回调中每次重建都会重新创建_searchBloc,但BlocProvider使用的是外部的_searchBloc,页面重建时会导致触发事件的Bloc和BlocBuilder监听的Bloc不是同一个实例。
  2. 状态不可变性缺失:SearchState直接修改原CategoryModel对象的isVisible属性,Bloc依赖状态的不可变性检测变化,Firebase返回的对象可能被框架视为不可变,直接修改属性无法触发状态变化检测。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 02:35:19