JHHK

欢迎来到我的个人网站
行者常至 为者常成

GetX

目录

介绍

一、什么是 GetX?

GetX 是一个轻量级且功能全面的 Flutter 框架,它不止是状态管理库,而是三合一解决方案:

            GetX
             │
   ┌─────────┼─────────┐
   │         │         │
 状态管理   路由管理   依赖注入

① 状态管理:响应式(Rx + Obx)与简单状态(GetBuilder)两种方案。
② 路由管理:无需 context 的导航、路由拦截、动态参数、中间件。
③ 依赖注入:Get.put / Get.find,在任意位置获取实例,自动管理生命周期。

二、为什么选择 GetX?

① 不依赖 BuildContext:弹窗、Toast、导航、获取 Controller 都不需要 context,深层 Widget 和业务层调用极其方便。
② 样板代码极少:没有 Provider 的 Consumer、Bloc 的 Event/State 类,一个 .obs + 一个 Obx 就能完成响应式更新。
③ 性能优秀:Obx 只重建观察 Rx 变化的最小范围;GetBuilder 是类似 setState 的手动刷新,几乎零开销。
④ 集成度高:国际化、主题切换、GetConnect 网络请求、GetStorage 本地存储、Snackbar/Dialog 一应俱全。

三、与其它方案的定位对比

对比项 Provider BLoC GetX
学习成本 低 高 低
样板代码 中 多 少
路由能力 无 无 内置
依赖注入 无 无 内置
context 依赖 需要 需要 不需要
强约束/规范性 弱 强(Event/State 强制分离) 弱(自由度高)

注意:自由是把双刃剑。GetX 缺少 BLoC 那种强制架构约束,大型项目必须靠团队规范约束写法,否则容易失控。

核心概念

一、两种状态管理方式

1、响应式(Rx + Obx)
变量标记为 .obs,UI 用 Obx 包裹,变量变化时自动重建:

var count = 0.obs;        // 声明 Rx 变量(本质是 RxInt)

Obx(() => Text('${count.value}'))  // Obx 内部订阅了 count

count.value++;            // 修改 → 自动通知 → Obx 重建

原理:.obs 返回 Rx 包装对象,赋值时(setter)触发流通知;Obx 内部监听 build 中使用到的 Rx 变量,只重建自己。

2、简单状态(GetBuilder + update())
类似 setState 的手动刷新,内部是 Set 缓存 + 直接 rebuild,无流开销:

class CounterController extends GetxController {
  int count = 0;                 // 普通变量,不是 Rx

  void increment() {
    count++;
    update();                    // 手动通知 GetBuilder 刷新
  }
}

GetBuilder<CounterController>(
  builder: (ctrl) => Text('${ctrl.count}'),
)

3、两者对比与选择

对比项 Obx(响应式) GetBuilder(简单状态)
变量类型 必须是 Rx(.obs) 普通变量
通知方式 赋值自动通知 手动调用 update()
性能 流订阅,略低 几乎零开销,极高
局部刷新粒度 每个 Rx 变量独立 默认整个 Controller(可用 update([id]) 指定)
适用场景 数据变化频繁、字段独立 一次性/低频更新、列表整体刷新
经验法则:
字段多且各字段独立变化   → Rx + Obx
低频整体刷新、追求极致性能 → GetBuilder
(同一个 Controller 里可以混用,但同一个数据源不要两边同时监听)

二、Controller 生命周期

onInit()      → 初始化(进内存时调用一次):注入依赖、拉取数据
   │
onReady()     → 首帧渲染完成后调用:弹引导、开动画
   │
onClose()     → 销毁前调用:取消订阅、释放 Timer/Controller

三、依赖注入:put 家族与 find

方法 特点 场景
Get.put 立即实例化并注册 进入页面即用的依赖
Get.lazyPut 懒加载,首次 find 时才实例化 按需创建的重量级依赖
Get.putAsync 异步注册(Future 工厂) SharedPreferences 等异步初始化
Get.create 每次 find 都创建新实例 同一 Controller 多实例(更推荐 tag 方案)
Get.find 获取已注册实例 任意位置获取
生命周期:
Get.put / Get.lazyPut 注册的实例,在被 Get.delete 或路由的 smartManagement
回收时,自动调用 onClose()。

四、路由管理

GetMaterialApp                       // 入口替换 MaterialApp
   │
GetPage(name: '/home', page: ... , binding: HomeBinding())
   │
Get.toNamed('/home')                 // 跳转(等价 push)
Get.offNamed('/login')               // 跳转并关闭当前页(等价 pushReplacement)
Get.offAllNamed('/login')            // 清空栈跳转(登录失效场景)
Get.back()                           // 返回(无需 context)
Get.toNamed('/detail', arguments: {'id': 100})  // 传参

目标页取参:
Get.arguments['id']

基本用法

一、响应式计数器(最简 Demo)

import 'package:flutter/material.dart';
import 'package:get/get.dart';

// ① Controller:继承 GetxController,变量标记 .obs
class CounterController extends GetxController {
  var count = 0.obs;

  void increment() => count.value++;
}

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(home: const HomePage());
  }
}

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

  @override
  Widget build(BuildContext context) {
    // ② 注入 Controller(页面级,页面销毁自动回收)
    final controller = Get.put(CounterController());

    return Scaffold(
      appBar: AppBar(title: const Text('GetX Rx Demo')),
      body: Center(
        // ③ Obx 只包住需要刷新的部分,范围越小越好
        child: Obx(() => Text(
          '${controller.count.value}',
          style: const TextStyle(fontSize: 48),
        )),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: controller.increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}

二、GetBuilder 版计数器

class CounterController extends GetxController {
  int count = 0;

  void increment() {
    count++;
    update(); // 手动通知
  }
}

// UI:
GetBuilder<CounterController>(
  init: CounterController(),  // 已 put 过可省略 init
  builder: (ctrl) => Text('${ctrl.count}'),
)

三、Binding:路由与依赖绑定(推荐写法)

把 Controller 的注入从 UI 中剥离,集中到 Binding 管理:

class HomeBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => HomeController());
  }
}

// 路由表关联:
GetPage(
  name: '/home',
  page: () => const HomePage(),
  binding: HomeBinding(),          // 进入路由自动注入,离开自动销毁
),

// 页面内直接 find 使用,不再关心创建细节:
final controller = Get.find<HomeController>();

四、一次性副作用:ever / once / debounce / interval

Rx 变量除了驱动 UI,还能挂监听器处理副作用(对比 Bloc 的 BlocListener):

class SearchController extends GetxController {
  var keyword = ''.obs;

  @override
  void onInit() {
    super.onInit();

    // 每次keyword变化都执行
    ever(keyword, (_) => debugPrint('keyword changed'));

    // 输入防抖:停止输入500ms后才执行(搜索请求的标配)
    debounce<String>(
      keyword,
      (value) => doSearch(value),
      time: const Duration(milliseconds: 500),
    );

    // 节流:500ms内最多执行一次
    interval(keyword, (_) => log('throttled'), time: const Duration(milliseconds: 500));
  }
}

最佳实践

一、Controller 拆分原则

与 Provider/BLoC 同理,GetX 的 Controller 也应按业务领域拆分,而不是按页面拆分:

1、一个 Controller 负责一个业务领域
「购物车 Controller」被购物车页、商品详情页、结算页共同使用,而不是每个页面复制一份。

2、生命周期不同的数据,不要放进同一个 Controller
全局数据(用户信息、主题)用全局 Controller;页面数据(表单、筛选条件)用页面级 Controller,随页面销毁。

全局 Controller:App 启动时 Get.put(可加 permanent: true),随 App 存活。
页面级 Controller:通过路由 Binding 注入或页面内 Get.put,离开路由自动回收。

  全局 Controller 页面级 Controller
注入方式 main 中 Get.put(permanent) Binding 或页面内 Get.put
销毁时机 App 退出 离开路由(smartManagement 自动)
作用范围 全 App 该路由页面
典型数据 用户信息、主题、购物车 表单、筛选、列表状态

3、高频变化的数据独立拆分
输入防抖、进度这类高频更新的数据独立成小 Controller,避免牵连大范围重建。

App
├── 全局 Controller(permanent)
│      ├── UserController
│      ├── ThemeController
│      └── CartController(跨页面)
│
└── 页面级 Controller(Binding 随路由回收)
       ├── ProductListController
       ├── SearchController(高频输入,独立拆出)
       └── OrderSubmitController(用完即弃)

二、Obx 使用规范

1、Obx 范围最小化
只包住真正读取 Rx 变量的 Widget,不要把整个页面裹进 Obx。

// ❌ 差:整个页面重建
Obx(() => Scaffold(
  body: Column(children: [
    Text('${ctrl.count.value}'),
    const ExpensiveWidget(),  // 不依赖 count,却每次都重建
  ]),
))

// ✅ 好:只有 Text 重建
Column(children: [
  Obx(() => Text('${ctrl.count.value}')),
  const ExpensiveWidget(),
])

2、Obx 内必须真正用到 Rx 变量
Obx 在 build 时收集用到的 Rx 变量作为订阅依据。如果 Obx 内部没有读取任何 .obs 变量,运行时直接报错:

"[Get] the improper use of a GetX has been detected ..."
(错误用法:Obx 里没有可订阅对象)

3、读取 .value 的时机
只有 Obx/GetBuilder 内部读取才建立订阅关系;在事件回调、生命周期方法里读取 .value 只是取值,不会触发订阅。

三、Rx 使用规范

1、字段级细粒度拆分
Rx 的优势是每个变量独立通知。把不相关的数据拆成多个 Rx,而不是塞进一个大 Map.obs:

// ❌ 差:一个 Map 管所有,任何一个值变都触发全部监听
var user = {}.obs;

// ✅ 好:字段独立,互不牵连
var name = ''.obs;
var avatar = ''.obs;

2、集合更新要用 Rx 自带方法
RxList/RxMap 提供了触发通知的更新方法,直接操作内部引用不会通知:

var list = <String>[].obs;

list.add('a');            // ✅ 触发通知
list.refresh();           // ✅ 手动触发(整体替换/内部修改后)
list.value.add('a');      // ❌ 只改了内部 List,不触发通知

3、Model 类字段响应式:用 Rx 包装成员

class User {
  RxString name = ''.obs;
  RxInt age = 0.obs;
}

四、依赖注入规范

1、优先用 Binding 管理页面级依赖,而不是页面里 Get.put
Binding 把「依赖什么」和「如何创建」集中声明,路由离开时自动回收,避免页面复用(如同一页面多实例进栈)时的冲突。

2、同一页面多实例用 tag 区分
详情页可能连续 push 多个实例,同一个 DetailController 会互相覆盖,必须用 tag 隔离:

Get.put(DetailController(), tag: 'product-100');
Get.find<DetailController>(tag: 'product-100');

GetBuilder<DetailController>(
  tag: 'product-100',
  builder: (ctrl) => ...,
)

3、Get.find 拿不到会直接抛异常
find 的前提是已注册。跨模块获取依赖时,先确认注册顺序(Binding 在路由表、全局在 main),不要在 find 之前 delete。

五、资源管理与常见坑

1、onClose 释放资源
StreamSubscription、Timer、TextEditingController、worker(ever/debounce 返回值)都要在 onClose 中销毁:

class SearchController extends GetxController {
  final input = TextEditingController();
  Worker? _debounce;

  @override
  void onInit() {
    super.onInit();
    _debounce = debounce<String>(
      keyword,
      (v) => doSearch(v),
      time: const Duration(milliseconds: 500),
    );
  }

  @override
  void onClose() {
    input.dispose();     // 释放输入控制器
    _debounce?.dispose(); // 释放 worker,否则回调还会继续触发
    super.onClose();
  }
}

2、smartManagement 回收机制
默认 SmartManagement.full 下,路由销毁时会自动 delete 未被使用的依赖。
被其它存活路由引用的实例不会误删;但使用 permanent: true 或全局 Controller 要谨慎,防止该释放的没释放。

3、GetBuilder 与 Rx 不要混用监听同一数据
GetBuilder 不监听 Rx 变量(只认 update()),Rx 变化不会刷新 GetBuilder;反过来 update() 也不会触发 Obx。同一数据源选定一种方案。

4、context 相关 API 优先换成 GetX 版本
业务层需要弹窗/Toast/导航时,用无 context 版本,避免把 BuildContext 传进 Controller:

Get.snackbar(...)      替代  ScaffoldMessenger.of(context).showSnackBar
Get.dialog(...)        替代  showDialog(context: ...)
Get.back()             替代  Navigator.pop(context)
Get.toNamed(...)       替代  Navigator.pushNamed(context, ...)

但不要因此在 Controller 里随意弹 UI:副作用仍应尽量收敛到 View 层(ever/once 监听 State 后由 View 触发),Controller 保持可测试性。

5、GetMaterialApp 只包一次
GetMaterialApp 必须是 App 根,路由表、国际化、主题都配置在这里;嵌套使用或与 MaterialApp 混用会导致路由和 Snackbar 行为异常。


行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.