Flutter 状态管理框架对比(五):GetX 的响应式、依赖注册与生命周期

在商品详情页点"加入购物车",顶部角标立即加一;切到结算页,那里也要看到同一数量。用 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 把哪几步接起来?

先看购物车计数的主线:

text 复制代码
点击按钮 → 调用 CartController.addItem()
         → itemCount.value 变化
         → 读取过 itemCount.value 的 Obx 重新构建

CartController 是状态持有者,.obs 让普通值变成可观察值(Rx),Obx 在构建回调中读取 Rx 并订阅变化。Get.put() 注册 Controller,Get.find() 从注册表取回它。响应式更新和依赖注册是两个不同责任;只会写 .obs,还不足以保证多个页面拿到同一份状态。

下面的例子把 Controller 注册在应用启动处,因而它在示例中是应用级状态 。MaterialApp 已足够演示状态管理;只有使用 GetX 的命名路由等能力时,才需要按相应文档接入 GetMaterialApp。

yaml 复制代码
dependencies:
  get: 4.7.3
dart 复制代码
import 'package:flutter/material.dart';
import 'package:get/get.dart';

class CartController extends GetxController {
  CartController(this._repository);

  final PriceRepository _repository;
  final itemCount = 0.obs;
  final totalCents = RxnInt();
  final priceStatus = PriceStatus.idle.obs;
  final priceError = RxnString();
  int _requestId = 0;
  bool _closed = false;

  void addItem() {
    itemCount.value++;
    _requestId++; // 数量变化后,旧报价失效。
    totalCents.value = null;
    priceStatus.value = PriceStatus.idle;
    priceError.value = null;
  }

  Future<void> loadPrice() async {
    final requestId = ++_requestId;
    final count = itemCount.value;
    priceStatus.value = PriceStatus.loading;
    priceError.value = null;
    try {
      final cents = await _repository.fetchTotalCents(count);
      if (_closed || requestId != _requestId) return;
      totalCents.value = cents;
      priceStatus.value = PriceStatus.success;
    } catch (_) {
      if (_closed || requestId != _requestId) return;
      totalCents.value = null;
      priceError.value = '报价失败,请重试';
      priceStatus.value = PriceStatus.failure;
    }
  }

  @override
  void onClose() {
    _closed = true;
    _requestId++;
    super.onClose();
  }
}

enum PriceStatus { idle, loading, success, failure }

abstract class PriceRepository {
  Future<int> fetchTotalCents(int count);
}

class FakePriceRepository implements PriceRepository {
  @override
  Future<int> fetchTotalCents(int count) async {
    await Future<void>.delayed(const Duration(milliseconds: 300));
    return count * 1999;
  }
}

void main() {
  Get.put(CartController(FakePriceRepository()), permanent: true);
  runApp(const CartApp());
}

class CartApp extends StatelessWidget {
  const CartApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: CartPage());
  }
}

class CartPage extends StatelessWidget {
  const CartPage({super.key});

  @override
  Widget build(BuildContext context) {
    final cart = Get.find<CartController>();
    return Scaffold(
      appBar: AppBar(title: const Text('购物车')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Obx(() => Text('商品数:${cart.itemCount.value}')),
            Obx(() {
              final status = cart.priceStatus.value;
              if (status == PriceStatus.loading) return const Text('正在报价...');
              if (status == PriceStatus.failure) {
                return Text(cart.priceError.value ?? '报价失败');
              }
              if (status == PriceStatus.success) {
                return Text('报价:${cart.totalCents.value} 分');
              }
              return const Text('价格待查询');
            }),
            ElevatedButton(
              onPressed: cart.loadPrice,
              child: const Text('查询价格'),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: cart.addItem,
        child: const Icon(Icons.add),
      ),
    );
  }
}

按钮调用 addItem(),itemCount 改变,读取它的 Obx 重新构建文字。查询价格时,另一个 Obx 显示加载、报价或错误。列表页、详情页、结算页若都取同一个已注册的 CartController,就能观察同一购物车。假仓库只延迟返回"数量 × 1999 分",不会主动失败;要观察失败分支,可让 fetchTotalCents() 在演示时抛出异常。真实加入购物车和报价仍应由业务仓库及服务端决定。

2. Obx、GetBuilder 和 Get.find() 不做同一件事

GetX 常见的几个入口容易被一起叫成"状态管理",实际职责不同:

入口 做什么 何时考虑
.obs 与 Obx 定义 Rx 值,界面读取并响应变化 一小块 UI 跟随具体值更新。
GetBuilder<T> 与 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.find<Repository>(),依赖从函数签名上消失,单元测试也要先准备全局注册表。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 的优势是把响应式、依赖和路由放进同一套工具;选它时也要同时管理这三者的边界。

下一章会用同一购物车需求做横向实战,对照各方案的代码组织、测试与迁移成本。

参考资料

相关推荐
广州智造2 小时前
Radioss复合材料使用的单元属性
前端·设计·cad·建模·cae·radioss
张宏宇2 小时前
我把微软开源的 tgrep 做成了本地版 GitHub Code Search:45,629 个文件里搜一次 10ms
前端·后端
huakoh2 小时前
MCP 调用超时后别急着重试:三步读回分清执行状态
前端
打工仔折腾 AI2 小时前
把模型切换交给平台:用蓝耘智能路由搭建商品评论分析工具
java·服务器·前端·后端·python·性能优化·ai agent 实战
不可能片场2 小时前
Electron 发布标题的冒号 击穿命令行解析
前端·electron
500842 小时前
React Native for OpenHarmony 实战:三方库 react-native-crypto-js 的鸿蒙化适配指南
javascript·react native·性能优化·electron·harmonyos
reeswell3 小时前
我开源了 inspect-devtools —— 让 AI 终于能"看见"你屏幕上的组件
前端·人工智能
Yuhano3 小时前
W3. 实现Agent工具调用引擎
前端·aigc·ai编程
flash俊杰3 小时前
第三节 基于 Vue 3 的领域模型架构——Schema 驱动的低代码后台实战
前端