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

如何创建聚合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间通信,正确姿势是事件驱动:

  1. 每个ChildBloc加载完成数据后,发布包含数据的专属事件(比如BirthdaysLoaded(List<Event> data));
  2. AggregatorBloc订阅所有ChildBloc的状态变化,接收到事件后合并数据到自身状态,同时计算当前进度(已完成子Bloc数量/总子Bloc数量);
  3. 日历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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 16:23:11