GetX
GetX 是一个多功能框架,它把状态管理、路由导航、依赖注入三大功能打包在一起,主打一个字——快。代码量极少,上手极简,适合快速出活。
GetX 的名字来自
Get——一个单词搞定状态、路由、依赖,少写代码多干活。
为什么要学 GetX?
|| 其他方案 | GetX 的不同 | |---------|-----------| | 只做状态管理 | 状态管理 + 路由 + 依赖注入,一包搞定 | | 需要 BuildContext | 不依赖 BuildContext,随处可用 | | 样板代码多 | 响应式写法极简,.obs 一行搞定 | | 需要顶层包裹 | 部分用法无需顶层配置 |
适合谁?
GetX 学习成本最低,适合想快速出原型或小型项目的开发者。但它的设计理念与 Flutter 官方推荐有所不同,在大型项目或团队协作中需要谨慎评估。建议先学 Provider 或 Riverpod 理解 Flutter 状态管理的基本思路,再来看 GetX。
版本说明
本文基于 get ^4.6.6 编写。GetX 4.x 是当前主流版本,API 相对稳定。
安装
flutter pub add get安装后,需要将 MaterialApp 替换为 GetMaterialApp,以启用 GetX 的路由和依赖注入功能:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(
// ─── ★ GetMaterialApp ──────────────
const GetMaterialApp(
home: MyApp(),
),
// ─── ☆ GetMaterialApp ──────────────
);
}TIP
如果只用响应式状态管理(.obs + Obx),不使用 GetX 的路由和依赖注入,可以不替换 MaterialApp。但推荐使用 GetMaterialApp 以获得完整功能。
两种响应式方式
GetX 提供两种响应式状态管理方式,各有适用场景:
|| 方式 | 核心思路 | 适用场景 | |------|--------|---------| | 响应式(.obs) | 数据变了自动刷新 | 简单状态,需要精细更新 | | 简单状态(GetBuilder) | 手动调用 update() 刷新 | 不需要自动监听,手动控制更新 |
下面逐一讲解。
一、响应式状态管理(.obs)
这是 GetX 最具特色的方式——用 .obs 把普通变量变成「可观察的」,UI 会自动响应变化。
快速上手:计数器
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(const GetMaterialApp(home: CounterPage()));
}
// ─── ★ 定义控制器 ──────────────
class CounterController extends GetxController {
final count = 0.obs; // .obs 把 int 变成可观察的响应式变量
void increment() => count++; // 修改后自动通知 UI
}
// ─── ☆ 定义控制器 ──────────────
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
// 通过 Get.find() 或 Get.put() 获取控制器
final controller = Get.put(CounterController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('GetX 计数器')),
body: Center(
// ─── ★ Obx 监听变化 ──────────────
child: Obx(() => Text(
'计数: ${controller.count.value}',
style: const TextStyle(fontSize: 48),
)),
// ─── ☆ Obx 监听变化 ──────────────
),
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
child: const Icon(Icons.add),
),
);
}
}.obs 的类型
.obs 可以用在多种基本类型和集合上:
// 基本类型
final name = ''.obs; // RxString
final count = 0.obs; // RxInt
final price = 0.0.obs; // RxDouble
final isActive = false.obs; // RxBool
// 集合类型
final items = <String>[].obs; // RxList<String>
final user = <String, dynamic>{}.obs; // RxMap<String, dynamic>
// 自定义对象
final user = User(name: '张三', age: 25).obs; // Rx<User>读取和修改值
// 读取值:用 .value
print(count.value); // 0
print(name.value); // '张三'
// 修改值
count.value = 10; // 直接赋值
name.value = '李四'; // 直接赋值
// RxInt 支持直接运算(无需 .value)
count++; // 等价于 count.value++
count += 5; // 等价于 count.value += 5
// RxList / RxMap 支持直接操作
items.add('苹果'); // 直接 add,不需要 .value
items.removeAt(0); // 直接操作
user['name'] = '王五'; // 直接赋值自定义对象必须用 .value 修改
对于自定义对象(如 Rx<User>),必须通过 .value 重新赋值,不能只修改对象内部的属性:
// ❌ 错误:修改属性不会触发更新
final user = User(name: '张三', age: 25).obs;
user.value.name = '李四'; // UI 不会更新!
// ✅ 正确:创建新对象赋值
user.value = User(name: '李四', age: 25); // UI 会更新
// ✅ 正确:使用 update 方法(推荐)
user.update((val) {
val!.name = '李四'; // update 内部会自动触发通知
});Obx — 自动响应 UI
Obx 是 GetX 的响应式 Widget,它会自动追踪内部使用的 .obs 变量,变量变化时自动重建:
Obx(() => Text('计数: ${controller.count.value}'))Obx 的工作原理
Obx执行内部的 builder 函数- 执行过程中,所有被读取的
.obs变量都会被自动记录 - 当任何一个
.obs变量变化时,Obx自动重新执行 builder - 不需要手动订阅或取消订阅
二、简单状态管理(GetBuilder)
GetBuilder 是 GetX 的另一种方式——不会自动监听,你需要手动调用 update() 来触发刷新。
适用场景
| 场景 | 推荐方式 |
|---|---|
| 需要自动响应变化 | .obs + Obx |
| 只在特定时机刷新 | GetBuilder + update() |
| 状态变化不频繁 | GetBuilder(性能更好) |
| 不需要响应式特性 | GetBuilder |
基本用法
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class CounterController extends GetxController {
int count = 0; // 普通 int,不是 .obs
void increment() {
count++;
update(); // 👈 手动通知 UI 刷新
}
}
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
final controller = Get.put(CounterController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('GetBuilder 计数器')),
body: Center(
// ─── ★ GetBuilder 监听 ──────────────
child: GetBuilder<CounterController>(
builder: (ctrl) => Text(
'计数: ${ctrl.count}',
style: const TextStyle(fontSize: 48),
),
),
// ─── ☆ GetBuilder 监听 ──────────────
),
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
child: const Icon(Icons.add),
),
);
}
}update() 的条件刷新
update() 可以传入 ID,只刷新特定的 GetBuilder:
class UserController extends GetxController {
String name = '张三';
int age = 25;
void updateName(String newName) {
name = newName;
update(['name']); // 只刷新监听 'name' 的 GetBuilder
}
void updateAge(int newAge) {
age = newAge;
update(['age']); // 只刷新监听 'age' 的 GetBuilder
}
}
// 在 Widget 中指定监听的 ID
GetBuilder<UserController>(
id: 'name', // 👈 只响应 update(['name'])
builder: (ctrl) => Text('姓名: ${ctrl.name}'),
),
GetBuilder<UserController>(
id: 'age', // 👈 只响应 update(['age'])
builder: (ctrl) => Text('年龄: ${ctrl.age}'),
),三、Obx vs GetBuilder 选择指南
|| | Obx | GetBuilder | |---|---------|-----------| | 触发方式 | .obs 变量变化自动触发 | 手动调用 update() | | 数据类型 | 必须用 .obs 包装 | 普通变量即可 | | 重建粒度 | 精确到单个 .obs 变量 | 整个 GetBuilder 区域 | | 性能 | 略低(响应式有额外开销) | 略高(无自动监听开销) | | 适用 | 数据频繁变化、需要自动响应 | 不频繁变化、手动控制刷新时机 |
一句话选择
数据经常变、想让 UI 自动跟着变 → Obx;想自己控制什么时候刷新 → GetBuilder。
四、GetxController — 控制器生命周期
GetxController 是 GetX 状态管理的核心,它承载业务逻辑和状态数据。
生命周期方法
class MyController extends GetxController {
@override
void onInit() {
super.onInit();
// 控制器初始化时调用(类似 initState)
print('控制器初始化');
}
@override
void onReady() {
super.onReady();
// 在 onInit 之后,Widget 渲染完成后调用
// 适合做页面加载后的数据请求
print('页面已渲染完成');
}
@override
void onClose() {
// 控制器被销毁时调用(类似 dispose)
// 释放资源、取消订阅等
print('控制器被销毁');
super.onClose();
}
}生命周期流程:
创建控制器 → onInit() → onReady() → [正常使用] → onClose() → 销毁获取控制器的方式
// 1️⃣ Get.put — 立即创建并注册(最常用)
final controller = Get.put(CounterController());
// 2️⃣ Get.lazyPut — 懒加载,首次使用时才创建
Get.lazyPut<CounterController>(() => CounterController());
// 3️⃣ Get.find — 获取已注册的控制器
final controller = Get.find<CounterController>();
// 4️⃣ Get.create — 每次获取都创建新实例
Get.create<CounterController>(() => CounterController());|| 方式 | 何时创建 | 实例数量 | 适用场景 | |------|---------|---------|---------| | Get.put | 立即 | 单例 | 大多数场景 | | Get.lazyPut | 首次 find 时 | 单例 | 不确定是否会用到 | | Get.find | 不创建,只查找 | — | 获取已注册的控制器 | | Get.create | 每次获取时 | 多实例 | 需要独立实例的场景 |
五、依赖注入与控制器绑定
全局注册
在 GetMaterialApp 之前注册,全局可用:
void main() {
Get.put(UserController());
Get.put(CartController());
runApp(const GetMaterialApp(home: MyApp()));
}页面级绑定(推荐)
使用 Bindings 让控制器与页面绑定——页面打开时创建,页面关闭时销毁:
// 1️⃣ 定义 Binding
class HomeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<HomeController>(() => HomeController());
}
}
// 2️⃣ 在路由中绑定
Get.to(() => const HomePage(), binding: HomeBinding());TIP
页面级绑定是推荐做法,可以避免控制器常驻内存,页面关闭时自动释放。
使用 GetView 简化代码
如果一个页面只使用一个控制器,可以用 GetView 代替 StatelessWidget,省去手动获取控制器的步骤:
// ─── ★ GetView 简化 ──────────────
class HomePage extends GetView<HomeController> {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
// 直接用 controller,不需要 Get.put 或 Get.find
return Obx(() => Text('计数: ${controller.count.value}'));
}
}
// ─── ☆ GetView 简化 ──────────────GetView 内部自动调用了 Get.find<T>(),所以需要确保控制器已被注册(通过 Get.put 或 Bindings)。
使用 GetWidget(不常用)
GetWidget 与 GetView 类似,区别是它会缓存控制器实例,不会随 Widget 重建而重新获取。适合控制器需要跨 Widget 共享的场景。大多数情况下用 GetView 即可。
六、GetX 路由导航
GetX 自带路由管理,不依赖 BuildContext,随处可用。
基本导航
// 跳转到新页面
Get.to(() => const SecondPage());
// 带参数跳转
Get.to(() => const DetailPage(), arguments: {'id': 42});
// 在目标页面获取参数
final args = Get.arguments; // {'id': 42}
// 返回上一页
Get.back();
// 返回并携带结果
Get.back(result: '返回的数据');
// 替换当前页面
Get.off(() => const LoginPage());
// 清空所有页面并跳转(适合登录后跳主页)
Get.offAll(() => const HomePage());命名路由
// 在 GetMaterialApp 中定义路由
GetMaterialApp(
getPages: [
GetPage(name: '/', page: () => const HomePage()),
GetPage(name: '/detail/:id', page: () => const DetailPage()),
],
);
// 使用命名路由跳转
Get.toNamed('/detail/42');
// 获取路径参数
final id = Get.parameters['id']; // '42'七、完整示例:待办事项 App
综合前面所学,用 GetX 做一个待办事项应用:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(const GetMaterialApp(home: TodoPage()));
}
// ============ 数据模型 ============
class Todo {
final String title;
final bool done;
const Todo({required this.title, this.done = false});
Todo copyWith({String? title, bool? done}) =>
Todo(title: title ?? this.title, done: done ?? this.done);
}
// ============ 状态管理 ============
class TodoController extends GetxController {
// 使用 .obs 响应式列表
final todos = <Todo>[].obs;
// 派生状态:未完成数量(自动响应 todos 变化)
int get uncompletedCount => todos.where((t) => !t.done).length;
void add(String title) {
todos.add(Todo(title: title));
}
void toggle(int index) {
todos[index] = todos[index].copyWith(done: !todos[index].done);
}
void remove(int index) {
todos.removeAt(index);
}
}
// ============ UI ============
class TodoPage extends StatelessWidget {
const TodoPage({super.key});
// 注册控制器
final controller = Get.put(TodoController());
final _controller = TextEditingController();
void _addTodo() {
final text = _controller.text.trim();
if (text.isEmpty) return;
controller.add(text);
_controller.clear();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// ─── ★ Obx 监听派生状态 ──────────────
title: Obx(() => Text('待办事项 (${controller.uncompletedCount} 未完成)')),
// ─── ☆ Obx 监听派生状态 ──────────────
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: '输入待办事项...',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _addTodo(),
),
),
const SizedBox(width: 8),
ElevatedButton(onPressed: _addTodo, child: const Text('添加')),
],
),
),
Expanded(
// ─── ★ Obx 监听列表变化 ──────────────
child: Obx(() {
final todos = controller.todos;
if (todos.isEmpty) {
return const Center(child: Text('暂无待办事项'));
}
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
leading: Checkbox(
value: todo.done,
onChanged: (_) => controller.toggle(index),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.done
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => controller.remove(index),
),
);
},
);
}),
// ─── ☆ Obx 监听列表变化 ──────────────
),
],
),
);
}
}八、完整示例:购物车(GetBuilder 版)
用 GetBuilder 实现购物车,展示手动控制刷新的方式:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(const GetMaterialApp(home: CartPage()));
}
// ========== 数据模型 ==========
class Product {
final String name;
final int price;
const Product({required this.name, required this.price});
}
// ========== 状态管理 ==========
class CartController extends GetxController {
final List<Product> _items = [];
List<Product> get items => List.unmodifiable(_items);
int get count => _items.length;
int get totalPrice => _items.fold<int>(0, (sum, item) => sum + item.price);
void add(Product product) {
_items.add(product);
update(); // 手动通知刷新
}
void removeAt(int index) {
_items.removeAt(index);
update();
}
void clear() {
_items.clear();
update();
}
}
// ========== UI ==========
class CartPage extends StatelessWidget {
const CartPage({super.key});
final controller = Get.put(CartController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// ─── ★ GetBuilder 监听 ──────────────
title: GetBuilder<CartController>(
builder: (ctrl) => Text('购物车 (${ctrl.count})'),
),
// ─── ☆ GetBuilder 监听 ──────────────
),
body: GetBuilder<CartController>(
builder: (ctrl) {
if (ctrl.items.isEmpty) {
return const Center(child: Text('购物车是空的'));
}
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: ctrl.items.length,
itemBuilder: (context, index) {
final item = ctrl.items[index];
return ListTile(
title: Text(item.name),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('¥${item.price}'),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => controller.removeAt(index),
),
],
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'总计: ¥${ctrl.totalPrice}',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
],
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => controller.add(
const Product(name: '苹果', price: 5),
),
child: const Icon(Icons.add),
),
);
}
}九、GetX 工具方法速查
SnackBar / Dialog / BottomSheet
GetX 提供了不依赖 BuildContext 的弹窗方法:
// SnackBar
Get.snackbar(
'提示',
'操作成功',
snackPosition: SnackPosition.BOTTOM,
);
// Dialog
Get.dialog(
AlertDialog(
title: const Text('确认'),
content: const Text('确定要删除吗?'),
actions: [
TextButton(onPressed: () => Get.back(), child: const Text('取消')),
TextButton(onPressed: () => Get.back(), child: const Text('确定')),
],
),
);
// BottomSheet
Get.bottomSheet(
Container(
padding: const EdgeInsets.all(16),
child: const Text('底部弹窗内容'),
),
);国际化与主题切换
// 切换主题
Get.changeTheme(ThemeData.dark());
Get.changeTheme(ThemeData.light());
// 国际化
// 需要在 GetMaterialApp 中配置 translations 和 locale
Get.updateLocale(const Locale('zh', 'CN'));GetX vs Provider vs Riverpod 快速对比
|| 对比项 | Provider | Riverpod | GetX | |-------|----------|----------|------| | 状态管理方式 | ChangeNotifier | Provider / NotifierProvider | .obs / GetBuilder | | 依赖 BuildContext | 是 | 否 | 否 | | 学习成本 | ⭐⭐ | ⭐⭐⭐ | ⭐ | | 样板代码量 | 中等 | 中等 | 极少 | | 额外功能 | 无 | 无 | 路由 + 依赖注入 + 弹窗 | | 编译时安全 | 弱 | 强 | 弱 | | 社区认可度 | 官方推荐 | 社区推荐 | 有争议 |
常见问题
1. Obx 中没有使用 .obs 变量会怎样?
Obx 的 builder 中必须至少使用一个 .obs 变量,否则会报错:
// ❌ 错误:Obx 内没有 .obs 变量
Obx(() => Text('固定文本')) // 报错!
// ✅ 正确:Obx 内使用了 .obs 变量
Obx(() => Text('计数: ${controller.count.value}'))
// ✅ 如果不需要响应式,直接用普通 Widget
Text('固定文本')2. 控制器获取报错 "Controller not found"?
确保在使用前已注册控制器:
// ❌ 错误:还没注册就获取
final controller = Get.find<MyController>(); // 报错!
// ✅ 正确:先注册再获取
Get.put(MyController());
final controller = Get.find<MyController>();3. .obs 自定义对象修改后 UI 不更新?
自定义对象必须整体赋值或使用 update 方法:
// ❌ 错误:只修改属性
final user = User(name: '张三', age: 25).obs;
user.value.name = '李四'; // UI 不会更新
// ✅ 正确方式一:整体赋值
user.value = User(name: '李四', age: 25);
// ✅ 正确方式二:使用 update 方法
user.update((val) {
val!.name = '李四';
});4. 页面退出后控制器还在内存中?
如果没有使用 Bindings 或 Get.delete(),控制器会一直存活。推荐使用页面级绑定:
Get.to(() => const DetailPage(), binding: DetailBinding());
// 页面退出后,控制器自动销毁或者手动删除:
Get.delete<MyController>();5. Obx 和 GetBuilder 能混用吗?
可以。同一个控制器中,.obs 变量用 Obx 监听,普通变量用 GetBuilder 监听。但不建议对同一变量同时使用两种方式,容易造成混乱。
6. 什么时候该用 GetX,什么时候不该用?
| 场景 | 推荐 |
|---|---|
| 快速原型 / 个人项目 / 小型 App | ✅ GetX |
| 需要极简代码、快速出活 | ✅ GetX |
| 大型项目 / 团队协作 | ❌ 考虑 Riverpod / Bloc |
| 遵循 Flutter 官方设计理念 | ❌ Provider / Riverpod |
| 需要严格的类型安全 | ❌ Riverpod |
