如何使用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里处理事件的时候严格按这个顺序走就行:
- 刚收到用户触发的操作事件,立刻发个带loading标记的状态,让页面马上出转圈
- 发起实际的API请求
- 请求成功后,更新对应的业务数据,同时把loading标记清空
- 请求失败/抛异常的时候,一样要把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
相关产品推荐
相关产品推荐

