如何创建聚合Bloc实现日历应用数据分阶段加载?
我开发了一款日历应用,需要从外部API加载大量日历事件数据,这个过程耗时较长,希望在等待完整数据期间向用户反馈当前加载状态。目前通过一个Bloc加载所有日历事件,等全部获取完成后,再向基于BlocBuilder的Widget发送加载完成状态。
我想了解如何拆分为更细粒度的Bloc?各类日历事件可通过API单独获取(也能像现在这样一次性获取)。我计划创建多个ChildBloc和一个AggregatorBloc,AggregatorBloc监听所有ChildBloc,每当某个ChildBloc从API获取到数据后,就把数据传递给AggregatorBloc,日历随之更新新事件。这样所有子Bloc可以并行请求API,每当有子Bloc准备好数据,日历就更新。日历Widget只需监听AggregatorBloc,所有ChildBloc通过AggregatorBloc对外屏蔽。
最终效果是用户初始看到空日历,底部有进度条,直到所有子Bloc返回数据后进度条填满。每次ChildBloc向AggregatorBloc传递数据时,日历填充该子Bloc的事件,进度条也填充一部分。
这种架构是否合理?还是应该直接在Widget中使用各个ChildBloc,放弃AggregatorBloc的想法?我尝试用Streams和Sinks实现,但没成功,之后看到Bloc文档明确指出不应这样做。
我该如何推进?有什么建议吗?
补充问题
如果按以下方式实现,API请求是否会变成串行而非并行?
emit(state.copyWith( newStatus: UserCalendarMainPageEventDetailsStateEnum.startLoadingAll, )); var birthdays = await _remoteApi.fetchBirthdays(); progress += 1; emit(state.copyWith( newStatus: UserCalendarMainPageEventDetailsStateEnum.progress, progressCount: progress )); var holidays = await _remoteApi.fetchHolidays(); progress += 1; emit(state.copyWith( newStatus: UserCalendarMainPageEventDetailsStateEnum.progress, progressCount: progress )); }
架构合理性分析
你提出的多ChildBloc+AggregatorBloc架构是合理的,核心优势包括:
- 并行请求:各个ChildBloc独立发起API请求,大幅压缩整体加载耗时;
- 状态聚合:Widget层只需监听AggregatorBloc,无需处理多个子Bloc的状态细节,符合单一职责原则;
- 渐进式体验:实现"加载一部分展示一部分"的即时反馈,同步更新进度条,比全量加载的用户体验好很多。
如果直接在Widget中监听多个ChildBloc,会导致Widget逻辑臃肿、状态管理分散,后续维护成本飙升,不推荐这种方案。
Bloc间通信的正确实现方式
Bloc文档不建议直接用Streams/Sinks做Bloc间通信,正确姿势是事件驱动:
- 每个ChildBloc加载完成数据后,发布包含数据的专属事件(比如
BirthdaysLoaded(List<Event> data)); - AggregatorBloc订阅所有ChildBloc的状态变化,接收到事件后合并数据到自身状态,同时计算当前进度(已完成子Bloc数量/总子Bloc数量);
- 日历Widget监听AggregatorBloc的状态更新,每次触发就渲染新数据和进度条。
具体落地步骤:
- 让AggregatorBloc通过
BlocProvider获取各个ChildBloc实例,调用listen方法订阅状态变化; - 每个ChildBloc的状态需包含"加载中/加载完成/加载失败"三种状态,方便AggregatorBloc判断进度;
- AggregatorBloc的状态要维护:已加载的各类事件集合、当前进度值、整体加载状态(部分完成/全部完成)。
补充问题解答
你给出的代码确实是串行请求,因为await会阻塞后续代码,必须等fetchBirthdays()完成后才会执行fetchHolidays()。要改成并行请求,用Future.wait或Stream.fromFutures实现:
方案1:用Future.wait+循环处理
emit(state.copyWith( newStatus: UserCalendarMainPageEventDetailsStateEnum.startLoadingAll, )); // 同时发起所有请求,并行执行 final futures = [ _remoteApi.fetchBirthdays(), _remoteApi.fetchHolidays(), // 其他事件请求... ]; // 逐个处理完成的请求,更新进度 for (final future in futures) { await future; progress += 1; emit(state.copyWith( newStatus: UserCalendarMainPageEventDetailsStateEnum.progress, progressCount: progress )); }
方案2:用Stream.fromFutures灵活处理结果
emit(state.copyWith( newStatus: UserCalendarMainPageEventDetailsStateEnum.startLoadingAll, )); // 给每个请求绑定类型标识,方便后续合并数据 final futures = [ _remoteApi.fetchBirthdays().then((data) => ('birthdays', data)), _remoteApi.fetchHolidays().then((data) => ('holidays', data)), ]; // 监听每个请求的完成事件 await for (final result in Stream.fromFutures(futures)) { progress += 1; // 根据类型合并数据到状态 final mergedState = switch(result) { ('birthdays', List<Event> data) => state.copyWith(birthdays: data), ('holidays', List<Event> data) => state.copyWith(holidays: data), _ => state, }; emit(mergedState.copyWith( newStatus: UserCalendarMainPageEventDetailsStateEnum.progress, progressCount: progress )); }
额外优化建议
- 给每个ChildBloc添加错误处理,加载失败时也要通知AggregatorBloc,避免进度条卡住;
- 给ChildBloc加缓存逻辑,重复进入页面时直接复用缓存数据,提升二次加载速度;
- 进度计算可以更精细:比如根据每个API返回的数据量占比分配进度权重,而非单纯按请求数量计数。
内容的提问来源于stack exchange,提问作者Hoegje

