在商品详情页点“加入购物车”顶部角标立即加一切到结算页那里也要看到同一数量。用 GetX 写出这段联动很短0.obs保存值Obx观察值Get.find()找到同一个 Controller。短代码很有吸引力但跨页面之后真正要管的是这个 Controller 到底由谁创建什么时候应该消失一句话结论GetX 能把状态更新和共享对象写得很直接只有把 Controller 的作用域、依赖入口和销毁时机一起约定好简洁才不会变成隐式全局状态。本文是系列第五章示例按get: 4.7.3的常见 API 写讨论 GetX 的响应式状态、依赖注册和生命周期。代码是一个可放进 Flutter 工程改造的最小示例价格由内存里的假仓库返回不包含真实接口、登录态和路由。本机未配置 Flutter SDK因此示例未做运行验证包版本来自 2026-09-29 的 pub.dev 包信息。1. 从点击到重建GetX 把哪几步接起来先看购物车计数的主线点击按钮 → 调用 CartController.addItem() → itemCount.value 变化 → 读取过 itemCount.value 的 Obx 重新构建CartController是状态持有者.obs让普通值变成可观察值RxObx在构建回调中读取 Rx 并订阅变化。Get.put()注册 ControllerGet.find()从注册表取回它。响应式更新和依赖注册是两个不同责任只会写.obs还不足以保证多个页面拿到同一份状态。下面的例子把 Controller 注册在应用启动处因而它在示例中是应用级状态。MaterialApp已足够演示状态管理只有使用 GetX 的命名路由等能力时才需要按相应文档接入GetMaterialApp。dependencies:get:4.7.3importpackage:flutter/material.dart;importpackage:get/get.dart;classCartControllerextendsGetxController{CartController(this._repository);finalPriceRepository_repository;finalitemCount0.obs;finaltotalCentsRxnInt();finalpriceStatusPriceStatus.idle.obs;finalpriceErrorRxnString();int _requestId0;bool _closedfalse;voidaddItem(){itemCount.value;_requestId;// 数量变化后旧报价失效。totalCents.valuenull;priceStatus.valuePriceStatus.idle;priceError.valuenull;}FuturevoidloadPrice()async{finalrequestId_requestId;finalcountitemCount.value;priceStatus.valuePriceStatus.loading;priceError.valuenull;try{finalcentsawait_repository.fetchTotalCents(count);if(_closed||requestId!_requestId)return;totalCents.valuecents;priceStatus.valuePriceStatus.success;}catch(_){if(_closed||requestId!_requestId)return;totalCents.valuenull;priceError.value报价失败请重试;priceStatus.valuePriceStatus.failure;}}overridevoidonClose(){_closedtrue;_requestId;super.onClose();}}enumPriceStatus{idle,loading,success,failure}abstractclassPriceRepository{FutureintfetchTotalCents(int count);}classFakePriceRepositoryimplementsPriceRepository{overrideFutureintfetchTotalCents(int count)async{awaitFuturevoid.delayed(constDuration(milliseconds:300));returncount*1999;}}voidmain(){Get.put(CartController(FakePriceRepository()),permanent:true);runApp(constCartApp());}classCartAppextendsStatelessWidget{constCartApp({super.key});overrideWidgetbuild(BuildContextcontext){returnconstMaterialApp(home:CartPage());}}classCartPageextendsStatelessWidget{constCartPage({super.key});overrideWidgetbuild(BuildContextcontext){finalcartGet.findCartController();returnScaffold(appBar:AppBar(title:constText(购物车)),body:Center(child:Column(mainAxisAlignment:MainAxisAlignment.center,children:[Obx(()Text(商品数${cart.itemCount.value})),Obx((){finalstatuscart.priceStatus.value;if(statusPriceStatus.loading)returnconstText(正在报价…);if(statusPriceStatus.failure){returnText(cart.priceError.value??报价失败);}if(statusPriceStatus.success){returnText(报价${cart.totalCents.value}分);}returnconstText(价格待查询);}),ElevatedButton(onPressed:cart.loadPrice,child:constText(查询价格),),],),),floatingActionButton:FloatingActionButton(onPressed:cart.addItem,child:constIcon(Icons.add),),);}}按钮调用addItem()itemCount改变读取它的Obx重新构建文字。查询价格时另一个Obx显示加载、报价或错误。列表页、详情页、结算页若都取同一个已注册的CartController就能观察同一购物车。假仓库只延迟返回“数量 × 1999 分”不会主动失败要观察失败分支可让fetchTotalCents()在演示时抛出异常。真实加入购物车和报价仍应由业务仓库及服务端决定。2.Obx、GetBuilder和Get.find()不做同一件事GetX 常见的几个入口容易被一起叫成“状态管理”实际职责不同入口做什么何时考虑.obs与Obx定义 Rx 值界面读取并响应变化一小块 UI 跟随具体值更新。GetBuilderT与update()Controller 显式通知关联 Widget 重建希望按操作时机手动触发刷新不需要每个字段都是 Rx。Get.put()、Get.lazyPut()、Get.find()注册、延迟创建和获取对象多个页面需要共享同一 Controller 或服务。Bindings把依赖注册与页面入口放在一起需要清楚地限定路由级依赖及创建时机。Obx观察的是其构建过程中读到的 Rx。把整个大页面包在一个Obx里任一被读取的 Rx 变化都可能使这块页面重新构建把观察范围收在角标、价格或错误提示附近更新原因更容易看清。GetBuilder走的是显式update()路径。它可以和响应式方式共存但同一份状态同时靠.obs与update()维护会让“是谁触发刷新”难以追踪。一个功能先选一种主要更新方式再按需要做局部例外。3. 作用域与销毁才是跨页面共享的难点上面的permanent: true是示例中的明确选择购物车跟着整个应用存在页面退出不会自动销毁它。这个选项适合解释“跨页面共享”不等于所有 Controller 都应永久注册。若购物车只属于登录会话退出登录时要安排消费者先离开旧会话再重建或删除相关状态强制删除永久注册对象之前也要确认没有仍在使用它的界面。页面专属 Controller 则应在页面作用域创建。若项目使用 GetX 路由可以通过Bindings注册页面依赖并按所选 SmartManagement 配置核对实际释放行为。GetxController的onInit()与onClose()可放初始化和清理逻辑如果 Controller 自己持有定时器、订阅、TextEditingController或流离开作用域时应取消或释放。不要因为页面看不见了就假设全局注册的对象也自动消失。依赖注册还有一条边界业务层最好通过构造函数接收仓库接口。若每个方法内部都Get.findRepository()依赖从函数签名上消失单元测试也要先准备全局注册表。GetX 可以负责装配对象业务对象仍然可以保持显式依赖。4. 异步价格不能只用一个double表达结算页需要从接口获取价格时只有price.obs不够。示例用PriceStatus、totalCents和priceError区分加载、成功与失败生产环境还应在加载时限制重复提交失败时给用户明确的重试路径。多个 Rx 字段连续变化时界面可能看到中间状态复杂流程可考虑把相关字段组合成一个不可变状态对象再让 UI 观察它。如果用户快速改数量并连续请求两次结果可能乱序返回。示例用_requestId忽略旧报价它不会取消底层请求。无论用 GetX、Riverpod 还是 Bloc都需要决定是取消旧请求、只接收最后一次结果还是在提交时重新向服务端确认。Obx只负责让 UI 看见状态变化不能自动保证异步结果仍属于当前购物车。同理Controller 中捕获异常后不能只把loading留在true。用try/finally结束加载状态给失败分支可见的错误并在页面离开后避免把过期结果当成当前页面的价格。测试应覆盖成功、失败、连续点击、退出页面等路径而不只测角标加一。5. 什么时候用 GetX如果团队已统一采用 GetX 的路由、依赖注册和 Controller 作用域它能让页面与状态的连接写得很集中。评估新项目时我会先要求一份约定应用级和页面级对象分别在哪里注册谁负责释放业务层能否直接访问全局注册表异步结果怎样处理过期与失败。有了这些规则代码短才是真的省事。若只是一个页面内的展开状态setState()更直接若团队更看重显式依赖图或事件流也应对照 Riverpod、Bloc/Cubit 再决定。GetX 的优势是把响应式、依赖和路由放进同一套工具选它时也要同时管理这三者的边界。下一章会用同一购物车需求做横向实战对照各方案的代码组织、测试与迁移成本。参考资料GetX 包文档与版本GetX 项目仓库Flutter 官方Introduction to state management