You need to enable JavaScript to run this app. 如何使用Flutter Bloc在API调用(如购物车商品加量)时实现加载状态切换
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Flutter Bloc在API调用(如购物车商品加量)时实现加载状态切换

flutter_bloc 实现操作类API局部加载态的落地方法

状态设计:别沿用列表场景的全页loading结构

你之前做列表拉取场景的bloc时,大概率会在状态里放一个布尔值isLoading,用来控制整个页面的加载占位——这个设计放到操作类请求上就会出问题:只要用户点一下加号,整个页面都会被蒙层挡住,既不是你想要的“按钮位置转个小圈”的效果,还容易堵死其他交互。

适配操作类场景的状态结构,核心是加一个可追踪的操作标识,而不是单一的全局布尔值:

  • 核心业务数据:比如购物车的商品列表
  • loadingActionId:当前正在执行的请求的唯一标识,值为null就代表没有正在进行的操作
  • 错误提示:操作失败时要展示的消息
class CartState {
  final List<CartItem> cartItems;
  // 核心字段:匹配单个操作的加载标记,无进行中操作时为null
  final String? loadingActionId;
  final String? errorMessage;

  CartState({
    required this.cartItems,
    this.loadingActionId,
    this.errorMessage,
  });

  CartState copyWith({
    List<CartItem>? cartItems,
    String? Function()? loadingActionId,
    String? errorMessage,
  }) {
    return CartState(
      cartItems: cartItems ?? this.cartItems,
      // 直接覆盖loading标记,不需要做合并逻辑
      loadingActionId: loadingActionId != null ? loadingActionId() : this.loadingActionId,
      errorMessage: errorMessage,
    );
  }
}

Bloc事件处理:卡死时序才不会出体验问题

很多人实现的时候会把时序搞反:等API都返回结果了,才想起发个loading状态——用户点完按钮等了两秒,转圈刚弹出来结果就消失了,完全达不到预期效果。

你预期的流程是完全合理的,Bloc里处理事件的时候严格按这个顺序走就行:

  1. 刚收到用户触发的操作事件,立刻发个带loading标记的状态,让页面马上出转圈
  2. 发起实际的API请求
  3. 请求成功后,更新对应的业务数据,同时把loading标记清空
  4. 请求失败/抛异常的时候,一样要把loading标记清空,再带上错误信息
class CartBloc extends Bloc<CartEvent, CartState> {
  final CartApiService apiService;
  CartBloc(this.apiService) : super(CartState(cartItems: [])) {
    on<IncrementCartItemEvent>((event, emit) async {
      // 第一步:触发事件后立刻发loading状态,绑定当前操作的唯一id
      emit(state.copyWith(
        loadingActionId: () => "increment_${event.itemId}",
      ));

      try {
        // 第二步:执行真实API调用
        final updatedItem = await apiService.increaseItemCount(event.itemId);
        // 第三步:更新购物车数据,同时清空loading标记
        final newCartList = state.cartItems.map((item) {
          return item.id == updatedItem.id ? updatedItem : item;
        }).toList();
        emit(state.copyWith(
          cartItems: newCartList,
          loadingActionId: () => null,
        ));
      } catch (e) {
        // 第四步:异常路径也要清空loading标记
        emit(state.copyWith(
          loadingActionId: () => null,
          errorMessage: "修改商品数量失败,请稍后重试",
        ));
      }
    });
  }
}

页面渲染:精准匹配单个按钮的加载态

写组件的时候,不需要自己管loading状态,直接从Bloc读当前的loading标记,和当前按钮绑定的操作id做对比就行:

  • 两边id对得上的时候,把原来的加号图标换成尺寸差不多的小转圈,同时把按钮的点击事件设为null,防止用户连点触发重复请求
  • 对不上的时候,就正常显示可点击的加号图标
BlocBuilder<CartBloc, CartState>(
  builder: (context, state) {
    // 判断当前按钮是不是处于loading状态
    final isCurrentBtnLoading = state.loadingActionId == "increment_${item.id}";
    return IconButton(
      // loading时禁用点击,避免重复提交
      onPressed: isCurrentBtnLoading
          ? null
          : () => context.read<CartBloc>().add(IncrementCartItemEvent(itemId: item.id)),
      icon: isCurrentBtnLoading
          ? const SizedBox(
              width: 20,
              height: 20,
              child: CircularProgressIndicator(strokeWidth: 2),
            )
          : const Icon(Icons.add),
    );
  },
)

容易踩的几个坑

  • 不管请求是成功、失败、还是超时,一定要在代码路径的最后把loading标记重置为null,不然转圈会一直留在按钮上,再也消不掉
  • 别图省事用全局布尔值当loading标记:如果用户同时点了两个商品的加减按钮,全局布尔值会让所有按钮都一起转圈,状态完全乱掉
  • 不用额外给loading加超时兜底的逻辑:Bloc本身的事件处理是按顺序排队的,只要所有代码路径都覆盖到清loading的动作,就不会出现状态残留

内容的提问来源于stack exchange,提问作者Abhi Singh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.11.04 17:45:04