Flutter Bloc如何实现初始拉取全量数据后按需发射不同状态?
核心结论
首先明确存储位置选择:和当前页面生命周期绑定、仅在当前页面交互流程中使用的预拉取全量数据,统一存储在Cubit对应的状态类中,不要存储在Repository层。
Repository的核心职责是抽象数据源(API、本地DB、缓存),只负责提供数据读写能力,不应该持有和单页面交互状态绑定的临时数据,否则多页面实例同时调用Repository时会出现数据污染、缓存过期不更新的问题。
具体实现方案
按照flutter_bloc + Repository的分层规范,实现逻辑拆分为三层:
1. Repository层实现
Repository只做一件事:聚合拉取页面初始化需要的全量数据,不处理任何UI交互相关的状态逻辑。
// 全量数据的实体类,根据业务自己定义字段 class StorePageFullData { final List<Store> stores; final List<Route> drivingRoutes; final List<Route> walkingRoutes; StorePageFullData({ required this.stores, required this.drivingRoutes, required this.walkingRoutes, }); } class StoreRepository { Future<StorePageFullData> fetchAllPageData(Location userLocation) async { // 用Future.wait并行拉取,缩短加载时间 final results = await Future.wait([ _api.getStoreList(), _api.getDrivingRoutes(userLocation: userLocation), _api.getWalkingRoutes(userLocation: userLocation), ]); return StorePageFullData( stores: results[0] as List<Store>, drivingRoutes: results[1] as List<Route>, walkingRoutes: results[2] as List<Route>, ); } }
2. Cubit与状态层实现
状态类持有全量数据,同时维护当前UI需要展示的路由类型标记;Cubit在初始化时自动触发全量数据拉取,提供切换路线的方法,不需要重复请求接口。
// 路线类型枚举 enum RouteType { driving, walking } // 基类状态 sealed class StoreMapState extends Equatable { const StoreMapState(); } class StoreMapLoading extends StoreMapState { @override List<Object?> get props => []; } class StoreMapLoadFailed extends StoreMapState { final String errorMsg; const StoreMapLoadFailed(this.errorMsg); @override List<Object?> get props => [errorMsg]; } // 加载完成的状态,持有全量数据+当前展示标记 class StoreMapLoaded extends StoreMapState { final List<Store> stores; final List<Route> drivingRoutes; final List<Route> walkingRoutes; final RouteType currentRouteType; const StoreMapLoaded({ required this.stores, required this.drivingRoutes, required this.walkingRoutes, this.currentRouteType = RouteType.driving, }); // copyWith方法方便切换状态时复用已有全量数据 StoreMapLoaded copyWith({RouteType? currentRouteType}) { return StoreMapLoaded( stores: stores, drivingRoutes: drivingRoutes, walkingRoutes: walkingRoutes, currentRouteType: currentRouteType ?? this.currentRouteType, ); } @override List<Object?> get props => [stores, drivingRoutes, walkingRoutes, currentRouteType]; } // Cubit实现 class StoreMapCubit extends Cubit<StoreMapState> { final StoreRepository _repository; final Location userLocation; StoreMapCubit({ required StoreRepository repository, required this.userLocation, }) : _repository = repository, super(StoreMapLoading()) { // 初始化时直接触发全量数据拉取 _initLoad(); } Future<void> _initLoad() async { try { final fullData = await _repository.fetchAllPageData(userLocation); // 首次加载完成默认展示驾车路线,全量数据随状态一起发出 emit(StoreMapLoaded( stores: fullData.stores, drivingRoutes: fullData.drivingRoutes, walkingRoutes: fullData.walkingRoutes, )); } catch (e) { emit(StoreMapLoadFailed(e.toString())); } } // 切换路线方法,不需要重新请求,直接复用已有数据发新状态 void switchRoute(RouteType targetType) { final curState = state; if (curState is! StoreMapLoaded || curState.currentRouteType == targetType) return; emit(curState.copyWith(currentRouteType: targetType)); } }
3. 页面层实现
- 页面初始化时通过
BlocProvider创建StoreMapCubit实例,传入当前用户位置和Repository实例 - 用
BlocBuilder监听状态变化:- 收到
StoreMapLoading时展示加载进度条 - 收到
StoreMapLoadFailed时展示错误提示和重试按钮 - 收到
StoreMapLoaded时,根据currentRouteType取对应的路线数据传给地图组件渲染
- 收到
- 路线切换按钮点击时,调用
context.read<StoreMapCubit>().switchRoute(对应路线类型)即可触发UI更新,无网络请求开销
注意事项
- 如果预拉取的数据需要跨多个页面复用、数据体量较大,可以在Repository层做带唯一标识的缓存(比如以用户位置+城市编码为key),但单页面的Cubit仍然需要持有当前页面使用的数据副本,避免多页面操作导致Repository缓存变化引发当前页面状态异常
- 不要在Cubit类的私有字段里存全量数据,所有UI渲染需要的数据都要放在状态类中,否则切后台、组件重建时会出现数据丢失、UI和状态不一致的问题
- 全量拉取时建议用
Future.wait并行请求多个接口,比串行请求速度快很多,减少用户等待时间
内容的提问来源于stack exchange,提问作者Mary
相关产品推荐
相关产品推荐

