看四篇教程时,每种框架似乎都能让购物车角标加一。真正做结算页,问题才出现:接口正在重新报价,用户又加了一件商品;旧报价随后返回,界面能不能把它当成新价格?页面退出或用户登出时,这份状态由谁清理?
一句话结论:四种方案都能完成购物车;影响长期维护的不是“加一”写了几行,而是状态的真实来源、异步结果的归属、订阅范围和可验证的销毁规则。
这是系列第六章,也是第一轮横向实战。对比版本固定为 provider 6.1.5+1、flutter_riverpod 3.4.3、flutter_bloc 9.1.1、get 4.7.3,版本信息于 2026-09-29 从 pub.dev 核对。文中的调用链是各库公开模型的概念化流程,不替代前四篇的逐行示例;没有跨设备基准测试,也不据此给框架排性能名次。
1. 先固定需求,否则对比不公平
我们只做一个小而完整的购物车场景:
| 打开商品列表 | 角标显示 0;详情页与结算页读同一数量。 |
| 点击“加入购物车” | 数量变 1,已显示该数据的页面得到更新。 |
| 在结算页触发报价 | 显示加载中;成功展示报价,失败展示重试入口。 |
| 报价期间再次改数量 | 旧数量对应的报价不能覆盖新数量的结果。 |
| 离开结算页或登出 | 页面专属请求停止或被忽略;登录会话的购物车按业务规则重置。 |
数据边界也要固定:本地购物车数量可以先展示,但最终支付金额由服务端确认。状态管理库只负责组织客户端的状态流,不决定库存、价格和支付的真值。
四套方案都应有同一份领域模型:购物车数量、报价状态(未请求/加载/成功/失败)、本次报价对应的购物车版本。仓库接口负责请求;页面负责显示和触发动作。把这几个职责固定,比较才不会变成“某篇示例漏了错误处理,所以它更短”。
2. 同一次点击,在四套方案里走哪条路?
| Provider | context.read 找到 ChangeNotifier,调用方法 | 提供给 Widget 子树的 Notifier | Consumer 或 context.select 等 | ChangeNotifierProvider 放置位置;由其创建的对象随 Provider 销毁。 |
| Riverpod | ref.read 找到 Notifier,调用方法 | provider 所管理的状态与依赖 | ref.watch 及选择性订阅 | ProviderScope、provider 生命周期配置与订阅关系。 |
| Bloc/Cubit | context.read 找到 Cubit,调用方法;Bloc 可发送事件 | Cubit/Bloc 输出的状态序列 | BlocBuilder、BlocSelector 等 | BlocProvider 放置位置;由其创建的 Bloc/Cubit 随 Provider 关闭。 |
| GetX | Get.find 找到 Controller,调用方法 | Controller 中的 Rx 或手动更新字段 | Obx 或 GetBuilder | 注册位置、Bindings 和实际清理策略;全局永久注册需显式重置。 |
这张表展示的是归属路径,不是每个库唯一的写法。Provider 可以提供非 ChangeNotifier 对象;Bloc 与 Cubit 的入口也不同;GetX 能用响应式或显式更新。框架名称不能代替你设计作用域:若把购物车放在某个很快销毁的详情页作用域里,任何方案都会在页面切换后丢状态。
实际代码组织可以先保持同一层次:
cart/
cart_repository.dart 请求报价、提交变化
cart_state.dart 数量、报价状态、请求版本
cart_owner.dart Notifier / Cubit / Bloc / Controller
cart_pages.dart 列表、详情、结算界面
cart_owner.dart 的实现因框架而变,仓库协议与业务状态尽量保持一致。这样团队能比较状态工具本身,也能把迁移影响限制在持有者和界面接线处。文件名是教学用的组织示意,并非四个库规定的目录结构。
3. 异步报价是最能暴露差异的地方吗?
它更能暴露状态设计的差异,而不是某个库能否做异步。四者都能表示加载、成功、失败;Riverpod 有内置的异步状态模型,其他方案通常在自己的状态对象或响应式字段里表达。真正需要统一的是这条规则:结果只能写回发起它时仍然有效的那份购物车状态。
例如购物车数量从 1 变成 2 时,旧请求的报价即使更晚返回,也应被取消或忽略。实现可以给每次请求分配递增版本,写回前比较当前版本;也可以通过可取消请求或按业务设计排队。选哪一种取决于仓库和后端接口。不能期待 notifyListeners()、emit()、.obs 或 ref.watch() 自动解决乱序响应。
资源释放也类似。结算页离开后,页面专属请求或订阅需要取消;若底层请求不能取消,至少要防止旧结果更新已经失效的状态。购物车本身若属于登录会话,不应随任意一个页面关闭而消失;登出时再按明确的会话规则重置。
4. 怎么测,才能发现选型带来的真实成本?
用同一组测试场景,比数代码行数更有价值:
各库的测试入口不同:Provider 可直接测 Notifier,并在 Widget 测试里包上对应 Provider;Riverpod 可用容器和依赖覆盖隔离状态,再测 ProviderScope 下的界面;Cubit/Bloc 可测输出的状态序列,Widget 测试包上 BlocProvider;GetX 可以直接测 Controller 的 Rx 值,涉及 Get.put/Get.find 的测试则要清理注册,避免前一个用例污染下一个用例。
这里的“容易测”主要来自业务依赖是否显式。无论哪一套,如果状态持有者在方法内部偷偷读取全局单例仓库,替换假数据源都会更费劲。把 CartRepository 作为构造参数传进去,通常比换框架更快改善测试。
5. 迁移与选型:我会怎么定?
| 只有少数局部状态 | 把状态留在页面,避免过早全局化 | setState、ValueNotifier 就够用。 |
| 已广泛使用 ChangeNotifier | 整理 Provider 作用域和局部订阅 | 继续用 Provider,除非实际痛点在依赖与异步组合。 |
| 新项目有多层依赖和异步数据 | 定义 provider 图、缓存和失效规则 | 我会先评估 Riverpod。 |
| 复杂订单流程要追踪事件 | 明确事件、状态和失败路径 | 评估 Bloc;简单状态可用 Cubit。 |
| 团队已使用 GetX 路由与注册 | 写清应用级、会话级、页面级对象边界 | 可继续用 GetX,同时约束全局访问。 |
迁移时不要让旧框架与新框架各保存一份可修改的购物车。先抽出仓库协议和业务状态,选一个唯一持有者;把一条页面路径接到新方案,验证上面的测试,再扩大范围。若旧项目的主要问题只是某个页面订阅过宽,先缩小重建范围,通常比全量换库省事。
这一轮对比没有单一赢家。局部状态先局部处理;共享状态只保留一份真实来源;异步结果与生命周期按业务规则验证。 做到这三点,再看团队是否需要 Riverpod 的依赖图、Bloc 的事件轨迹、Provider 的渐进接入或 GetX 的集成工作流,选型就有了依据。
参考资料
- Flutter 官方状态管理导论
- Provider、Flutter Riverpod、flutter_bloc、GetX 包文档与版本信息
网硕互联帮助中心




评论前必须登录!
注册