目录
介绍
一、什么是 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 行为异常。
行者常至,为者常成!