ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flutter 状态管理框架对比(四):Bloc/Cubit 怎么让购物车变化可追踪

Flutter 状态管理框架对比(四):Bloc/Cubit 怎么让购物车变化可追踪 商品列表点了“加入购物车”详情页的角标变了结算页的数量却没变更糟的是结算页刚拿到价格时用户又加了一件商品。此时问题已经不只是“怎么刷新 Widget”还要弄清楚谁接收操作哪一次报价仍然有效离开页面后谁负责清理状态对象一句话结论Cubit 用方法直接产出新状态Bloc 先接收事件再由处理器产出新状态两者都应让同一份购物车状态在明确的作用域内流向界面。本文沿用系列第一篇的购物车列表、详情、结算页共享数量结算页异步询价。依赖锁定为flutter_bloc: 9.1.12026-09-29 核对 pub.dev 版本 API。示例是可改造的教学代码询价函数用延时模拟商品明细、真实接口、持久化和服务端价格校验留给业务层。1. 先把一次点击的路径画清楚flutter_bloc提供 Widget 与状态对象之间的连接状态转换的主体来自bloc。两条路径的差别集中在操作入口Cubit按钮 → cubit.addItem() → emit(新 CartState) → 订阅者更新 Bloc按钮 → bloc.add(AddItem()) → onAddItem 处理器 → emit(新 CartState) → 订阅者更新在购物车计数这种“调用后就能决定下一状态”的操作里我会先用 Cubit。事件名称本身需要被追踪或业务流程要按事件类型分别处理时再用 Bloc多写一层事件不是正确性的前提。问题CubitBloc业务入口调用公开方法如addItem()派发事件如add(AddItem())状态在哪里产生方法中调用emit()onEvent注册的处理器中调用emit()界面如何观察BlocBuilder、BlocSelector等同一组 Widget适合当前案例的点购物车增减、单次询价入口短而清楚订单流程出现库存锁定、优惠券、支付回调等多类事件时事件名称更容易追踪这里的emit输出的是一份新的状态快照不应靠修改旧对象里的可变字段来“通知”界面。Bloc/Cubit的公开概念与flutter_blocWidget 的职责分别见文末官方文档。2. 谁持有状态谁只负责显示要让三个路由读到同一个购物车BlocProvider应放在三个路由的共同祖先上。下例放在MaterialApp外层因此列表、详情和结算页都能从各自的BuildContext读取同一个CartCubit。如果只把它放进商品列表页的路由内部新路由就不在那个作用域里。Widget在购物车例子中的职责不该做的事BlocProvider创建并向后代提供CartCubitcreate创建的实例随 provider 销毁而关闭代替业务方法计算价格BlocBuilder根据状态构建价格、加载提示等界面在builder中弹 SnackBar 或发请求BlocSelector只选出count让数量展示与报价等字段分开观察选出的值应是不可变的把可变集合当选择结果指望原地修改能被识别BlocListener监听一次性界面动作如报价失败时提示当成展示数据的构建器BlocBuilder负责“现在画什么”BlocListener负责“发生变化时做一次什么”。把询价放在builder里会因为重建而重复请求这里由按钮调用refreshPrice()。3. 把列表、详情、结算接到同一份状态下面代码可放入 Flutter 工程的lib/main.dart并在pubspec.yaml的dependencies中加入flutter_bloc: 9.1.1。fakePrice只用于演示异步状态不代表实际结算价格。代码没有在本仓库运行 Flutter 构建迁入工程后仍应按项目 SDK 和接口类型编译、替换模拟数据。importpackage:flutter/material.dart;importpackage:flutter_bloc/flutter_bloc.dart;immutableclassCartState{constCartState({this.count0,this.priceCents,this.loadingfalse,this.error,});finalint count;finalint?priceCents;finalbool loading;finalString?error;overridebooloperator(Objectother)otherisCartStatecountother.countpriceCentsother.priceCentsloadingother.loadingerrorother.error;overrideintgethashCodeObject.hash(count,priceCents,loading,error);}classCartCubitextendsCubitCartState{CartCubit(this._fetchPrice):super(constCartState());finalFutureintFunction(int count)_fetchPrice;int _requestId0;voidaddItem(){_requestId;// 数量变了旧报价不再适用。emit(CartState(count:state.count1));}FuturevoidrefreshPrice()async{if(state.loading)return;finalcountstate.count;finalrequestId_requestId;emit(CartState(count:count,loading:true));try{finalcentsawait_fetchPrice(count);if(isClosed||requestId!_requestId)return;emit(CartState(count:count,priceCents:cents));}catch(_){if(isClosed||requestId!_requestId)return;emit(CartState(count:count,error:报价失败请重试));}}}FutureintfakePrice(int count)async{awaitFuturevoid.delayed(constDuration(milliseconds:300));returncount*1299;}voidmain()runApp(BlocProvider(create:(_)CartCubit(fakePrice),child:MaterialApp(routes:{/:(_)constCartPage(title:商品列表,next:/detail),/detail:(_)constCartPage(title:商品详情,next:/checkout),/checkout:(_)constCartPage(title:结算),},),),);classCartPageextendsStatelessWidget{constCartPage({super.key,requiredthis.title,this.next});finalStringtitle;finalString?next;overrideWidgetbuild(BuildContextcontext)Scaffold(appBar:AppBar(title:Text(title)),body:Column(children:[BlocSelectorCartCubit,CartState,int(selector:(state)state.count,builder:(_,count)Text(购物车数量$count),),TextButton(onPressed:()context.readCartCubit().addItem(),child:constText(加入购物车),),if(next!null)TextButton(onPressed:()Navigator.pushNamed(context,next!),child:constText(下一页),),if(nextnull)constCheckoutPrice(),],),);}classCheckoutPriceextendsStatelessWidget{constCheckoutPrice({super.key});overrideWidgetbuild(BuildContextcontext)BlocListenerCartCubit,CartState(listenWhen:(before,after)after.error!nullbefore.error!after.error,listener:(context,state)ScaffoldMessenger.of(context).showSnackBar(SnackBar(content:Text(state.error!)),),child:Column(children:[TextButton(onPressed:()context.readCartCubit().refreshPrice(),child:constText(获取最新价格),),BlocBuilderCartCubit,CartState(builder:(_,state){if(state.loading)returnconstText(正在询价…);if(state.priceCentsnull)returnconstText(尚无有效报价);returnText(报价¥${(state.priceCents!/100).toStringAsFixed(2)});},),],),);}从列表进入详情再进入结算三个页面的数量都由同一个CartCubit提供。结算页按钮触发询价后loading、成功报价或错误会依次成为新的CartState失败提示只由BlocListener处理。示例保留“报价只属于询价时的数量”这一约束加购会清掉旧报价并使在途请求失效。4. 状态相等、异步回包和关闭时机有什么坑上例手写了与hashCode是为了让状态的“相等”有业务含义。emit对与当前状态相等的后续状态会跳过如果状态对象原地修改或者相等比较漏掉loading、error界面可能收不到预期变化。实际项目可用不可变状态类或Equatable减少样板代码但比较字段必须覆盖所有影响展示的值。BlocSelector的int结果本身不可变适合作为局部订阅值。异步请求的另一个坑是旧结果覆盖新状态。例子中_requestId记录当前请求加购后旧请求即使返回也不会把旧数量的价格写回。isClosed检查避免 provider 销毁后继续emit。这两个检查不是取消网络请求真实 API 如需节省流量或处理重复点击还要接入取消能力、超时和服务端幂等规则。结算金额最终以服务端确认结果为准。BlocProvider(create: ...)创建的实例由该 provider 关闭通过BlocProvider.value(value: existingCubit, ...)传入已有实例时它不会替你关闭。作用域因此要跟业务生命周期对齐本例放在应用入口可跨路由共享也意味着直到该作用域销毁才自动关闭。如果购物车只服务某个登录会话就应把它放到会话对应的作用域登出时明确结束会话。5. 什么时候把 Cubit 升成 Bloc当操作不再只是加一件或请求一次报价而是需要按“加入商品”“锁定库存”“应用优惠”“提交订单”“接收支付结果”分别记录和处理时可以把方法入口改为事件入口。概念上的对应关系是add(AddItem())进入onAddItem((event, emit) { ... })处理器再输出CartStateWidget 的订阅方式不用随之重写。真正迁移时还要确定并发事件的处理策略尤其不能让较早的异步事件在较晚事件之后写回旧状态。因此对本篇购物车计数与询价我会保留 Cubit方法名已经足够表达操作状态、错误和请求失效也都能在一个对象里看清。需要事件日志和更复杂的流程编排时再引入 Bloc。记住最短的检查路径即可谁创建状态对象 → 谁触发方法或事件 → 谁输出新状态 → 哪些 Widget 观察 → 谁关闭对象。参考资料flutter_bloc 9.1.1 版本页flutter_bloc 9.1.1 APIBlocProviderflutter_bloc 9.1.1 APIBlocBuilderflutter_bloc 9.1.1 APIBlocSelectorflutter_bloc 9.1.1 APIBlocListenerBloc 官方文档核心概念
返回列表