源码地址
在 Flutter 中,Get
是来自 get
包的一个轻量级、功能强大的状态管理与路由框架,常用于:
- 状态管理
- 路由管理
- 依赖注入(DI)
- Snackbar / Dialog / BottomSheet 管理
- 本地化(多语言)
下面是 get
的常见用法介绍:
✅ 1. 安装
在 pubspec.yaml
中添加依赖:
dependencies:get: ^4.6.6
✅ 2. 基础设置
在 main.dart
使用 GetMaterialApp
替代 MaterialApp
:
import 'package:flutter/material.dart';
import 'package:get/get.dart';void main() {runApp(GetMaterialApp(home: HomePage(),));
}
✅ 3. 状态管理用法
3.1 使用 GetxController
管理状态:
class CounterController extends GetxController {var count = 0.obs;void increment() {count++;}
}
3.2 页面中使用:
class HomePage extends StatelessWidget {final CounterController c = Get.put(CounterController());Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("GetX Example")),body: Center(child: Obx(() => Text("Count: ${c.count}")),),floatingActionButton: FloatingActionButton(onPressed: c.increment,child: Icon(Icons.add),),);}
}
明白了!你问的是 Flutter 的 GetX 框架中状态管理的几种方式,包括如何定义状态、如何更新状态、以及它们的区别与用法示例。
✅ GetX 状态管理的三种主要方式
类型 | 特点 | 使用方式关键字 |
---|---|---|
响应式(Reactive) | 基于 .obs 变量 + Obx 小部件 | Rx / .obs + Obx |
简单状态(Simple) | 基于 GetBuilder 刷新 widget | GetBuilder |
混合状态(Worker) | 使用 ever /once 监听变化 | Workers |
🧪 方式一:响应式(Reactive State)Rx
+ Obx
✅ 使用场景:最常用,响应式自动刷新,无需手动更新。
🧵 示例:
class CounterController extends GetxController {var count = 0.obs;void increment() {count++;}
}class CounterPage extends StatelessWidget {final CounterController c = Get.put(CounterController());Widget build(BuildContext context) {return Scaffold(body: Center(child: Obx(() => Text("Count: ${c.count}"))),floatingActionButton: FloatingActionButton(onPressed: c.increment,child: Icon(Icons.add),),);}
}
✅ 特点:
- 自动刷新 UI(通过
Obx
) - 状态类型必须是
.obs
形式或Rx<Type>
类型 - 适合实时反应数据变化
🧪 方式二:简单状态(Simple State)使用 GetBuilder<T>
✅ 使用场景:性能高、不需要响应式,只在需要时手动刷新。
🧵 示例:
class CounterController extends GetxController {int count = 0;void increment() {count++;update(); // 手动通知刷新}
}class CounterPage extends StatelessWidget {Widget build(BuildContext context) {return GetBuilder<CounterController>(init: CounterController(),builder: (c) => Scaffold(body: Center(child: Text("Count: ${c!.count}")),floatingActionButton: FloatingActionButton(onPressed: c.increment,child: Icon(Icons.add),),),);}
}
✅ 特点:
update()
后GetBuilder
中的 widget 刷新- 性能更高,无响应式开销
- 不需要使用
.obs
🧪 方式三:混合监听 Worker(更高级)
✅ 使用场景:在状态变化时执行副作用逻辑(如网络请求、打印等)
🧵 示例:
class CounterController extends GetxController {var count = 0.obs;void onInit() {ever(count, (value) => print("count changed: $value"));super.onInit();}void increment() => count++;
}
✅ 常用 Worker 方法:
方法 | 说明 |
---|---|
ever() | 每次变化都会触发 |
once() | 第一次变化触发一次 |
debounce() | 用户停止输入一段时间后才触发(适合搜索) |
interval() | 一定时间内只触发一次(节流) |
🔄 状态更新对比
方式 | 自动刷新 | 手动调用 | 响应式变量 | 性能 |
---|---|---|---|---|
Obx | ✅ | ❌ | ✅ | 中等 |
GetBuilder | ❌ | ✅(update() ) | ❌ | 高 |
Worker | ✅ | ❌ | ✅ | 用于副作用 |
🧠 总结建议:
使用目的 | 推荐方式 |
---|---|
快速开发、响应式 UI | Rx + Obx |
高性能、低耦合局部刷新 | GetBuilder |
响应状态变化做副作用 | Workers 系列 |
Obx 会监听 所有.obs 的变化吗,还是可以指定,他是与某个Controller绑定吗
🎯 Obx
的监听机制:只监听它内部使用到的 .obs
变量
✅ 它不会监听所有 .obs
,只监听你在 Obx
函数体中实际使用到的 .obs
。
🔍 示例说明:
class MyController extends GetxController {var a = 0.obs;var b = 0.obs;
}
final c = Get.put(MyController());Obx(() => Text("${c.a}")); // ✅ 只会监听 c.a 的变化,不管 c.b
如果
c.b
改变了,Obx
不会重建,因为你没有在 Obx 里使用它。
🧠 你问的三个点详解:
1. ✅ Obx 会监听所有 .obs
吗?
❌不会。
只监听 Obx
函数内部使用到的 .obs
,不会管你定义了多少个 .obs
。
2. ✅ 可以指定监听变量吗?
✅是的,通过写在 Obx 内部来“指定”。
你只要在 Obx
里访问了某个 .obs
变量,它就自动监听它。
例如:
Obx(() => Text("${controller.username}")); // 只监听 username
3. ❓ Obx 是与某个 Controller 绑定的吗?
❌不是绑定关系,它只是“用了”某个 Controller 里的 .obs
。
你甚至可以在 Obx
中混用多个 controller 的 .obs
:
Obx(() => Text("${c1.a} - ${c2.b}")); // 同时监听 c1 和 c2 的状态
✅ 总结一句话:
Obx
监听谁,取决于你在它里面访问了哪些.obs
变量,与是否来自某个 Controller 无关。
如果你想实现多个 Obx
分别监听不同状态,避免无关刷新,这就是 GetX 的优势之一,写法也很清晰。
这里是一个完整示例,展示如何用多个 Obx
精确监听不同的 .obs
状态变量,从而避免不必要的重建 —— 每个 Obx
只响应自己的数据变化。
✅ 示例场景:两个计数器,两个 Obx
,互不干扰
import 'package:flutter/material.dart';
import 'package:get/get.dart';void main() {runApp(GetMaterialApp(home: CounterPage()));
}/// 控制器,包含两个独立的状态变量
class CounterController extends GetxController {var countA = 0.obs;var countB = 0.obs;void incrementA() => countA++;void incrementB() => countB++;
}
✅ 页面布局(两个 Obx
精准监听)
class CounterPage extends StatelessWidget {final controller = Get.put(CounterController());Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("多个 Obx 独立监听示例")),body: Column(mainAxisAlignment: MainAxisAlignment.center,children: [// 只监听 countAObx(() => Text("计数器 A:${controller.countA}",style: TextStyle(fontSize: 20))),ElevatedButton(onPressed: controller.incrementA,child: Text("A +1"),),SizedBox(height: 40),// 只监听 countBObx(() => Text("计数器 B:${controller.countB}",style: TextStyle(fontSize: 20))),ElevatedButton(onPressed: controller.incrementB,child: Text("B +1"),),],),);}
}
✅ 运行效果
- 点击「A +1」只会刷新第一个
Text
,第二个不动。 - 点击「B +1」只会刷新第二个
Text
,第一个不动。
🧠 总结要点
做法 | 原因/作用 |
---|---|
多个 Obx | 精准监听、独立更新 |
.obs 拆成多个变量 | 分离状态,避免串联刷新 |
不要在一个 Obx 里访问多个状态 | 否则它会“监听多个”,导致全部重建 |
如果你要优化一个页面性能,建议就是:
- 把状态拆得更小(多个
.obs
); - 把 UI 拆得更细(多个
Obx
); - 这样能避免不必要的 widget rebuild。
GetBuilder 会监听 所有.obs 的变化吗,还是可以指定,他是与某个Controller绑定吗
我们来精准拆解 GetBuilder<T>
的监听机制
✅ 结论先行
问题 | 答案 |
---|---|
GetBuilder<T> 会监听所有 .obs 的变化吗? | ❌ 不会。它不监听 .obs ,和 .obs 完全无关。 |
GetBuilder 可以指定监听变量吗? | ✅ 可以手动控制刷新范围,但不是通过变量监听,而是 id 区分。 |
它是与某个 Controller 绑定的吗? | ✅ 是的,T 就是绑定的 Controller 类型。 |
🧠 本质区别:GetBuilder
与 .obs
没有关系
GetBuilder
的刷新是由 controller.update()
主动触发的,和你有没有使用 .obs
完全无关。
✅ 示例说明:
class MyController extends GetxController {int countA = 0;int countB = 0;void incrementA() {countA++;update(['a']); // 指定 id = 'a' 的 GetBuilder 会刷新}void incrementB() {countB++;update(['b']); // 指定 id = 'b' 的 GetBuilder 会刷新}
}
class MyPage extends StatelessWidget {final controller = Get.put(MyController());Widget build(BuildContext context) {return Scaffold(body: Column(children: [GetBuilder<MyController>(id: 'a',builder: (_) => Text("A: ${_.countA}"),),GetBuilder<MyController>(id: 'b',builder: (_) => Text("B: ${_.countB}"),),ElevatedButton(onPressed: controller.incrementA, child: Text("A +1")),ElevatedButton(onPressed: controller.incrementB, child: Text("B +1")),]),);}
}
✅ update()
概要:
调用方式 | 刷新范围 |
---|---|
update() | 所有使用该 Controller 的 GetBuilder |
update(['a']) | 只刷新 id 为 'a' 的 GetBuilder |
update(['b', 'c']) | 同时刷新 'b' 和 'c' 的 |
✅ 总结对比表:Obx
vs GetBuilder
特性 | Obx (响应式) | GetBuilder (手动) |
---|---|---|
是否与 .obs 有关 | ✅ 是 | ❌ 无关 |
是否自动刷新 | ✅ 会自动监听 .obs 的变化 | ❌ 不会,需手动调用 update() |
是否能精细控制刷新区域 | ✅ 拆多个 Obx | ✅ 支持 id 精细控制 |
性能表现 | 中等(需响应式依赖追踪) | 高(只有调用 update() 才重建) |
与 Controller 绑定 | ❌ 没有绑定(只用了变量) | ✅ 强绑定,必须指定类型 T |
如果你是性能优先、UI固定、只需要响应某些点击/事件,推荐 GetBuilder
。
如果你需要动态响应式 UI(比如登录状态、购物车数量),用 Obx
更适合。
🔍 Worker 监听机制简述
Worker 是 GetX 中用来监听特定
.obs
状态变化并执行副作用操作的一组工具函数,例如ever
、once
、debounce
、interval
等。
✅ 问题逐条回答
问题 | 回答 |
---|---|
Worker 会监听所有 .obs 的变化吗? | ❌ 不会,只监听你显式传入的某个 .obs 变量。不会自动监听全部。 |
可以指定监听哪个 .obs 吗? | ✅ 必须指定监听哪个 .obs ,Worker 的第一个参数就是监听目标。 |
Worker 是否与某个 Controller 绑定? | ✅ 通常放在某个 Controller 中使用,但绑定的是 .obs ,不是 Controller 本身。 |
🧪 示例:监听某个 .obs
变量的变化
class MyController extends GetxController {var count = 0.obs;var username = ''.obs;void onInit() {super.onInit();// 每次 count 改变时打印ever(count, (value) => print("count changed: $value"));// 只监听 username 第一次改变once(username, (value) => print("username changed once: $value"));// 用户停止输入 800ms 后才触发(如搜索)debounce(username, (value) => print("debounced: $value"), time: Duration(milliseconds: 800));// 每 2 秒最多触发一次interval(count, (value) => print("interval: $value"), time: Duration(seconds: 2));}void increment() => count++;void setUsername(String name) => username.value = name;
}
🧠 总结:Worker 用法与绑定机制
项目 | 说明 |
---|---|
监听对象 | 必须手动传入某个 .obs (如 count , username ) |
可监听多个变量 | ✅ 可以在一个 controller 里设置多个 ever() 等,监听多个 .obs |
生命周期绑定 | 通常在 onInit() 中设置,自动随 controller 生命周期注销 |
与 Controller 关系 | ✅ 通常放在 controller 中,但不是监听整个 controller,仅监听你指定的 .obs |
✅ Worker 适用场景
场景 | 使用方法 |
---|---|
搜索输入防抖(停止输入才查) | debounce(textObs, callback) |
防止按钮频繁点击 | interval(buttonTapObs, callback) |
登录状态变化提示 | ever(isLoggedInObs, callback) |
页面加载后只响应一次(如埋点) | once(pageReadyObs, callback) |
好的,这里是一个完整示例:
使用 GetX 的 Worker 机制 来监听两个 .obs
:
- 用户名输入框 → 使用
debounce
实现 防抖搜索 - 登录状态 → 使用
ever
实现登录提示(弹 Snackbar)
✅ 项目结构预览
lib/
├── main.dart
└── controller.dart ← 状态 & Worker 逻辑
📄 controller.dart
import 'package:get/get.dart';
import 'package:flutter/material.dart';class AuthController extends GetxController {// 状态变量var username = ''.obs;var isLoggedIn = false.obs;void onInit() {super.onInit();// 防抖搜索:用户停止输入 800ms 后触发搜索逻辑debounce(username, (value) {print("🔍 执行搜索:$value");// 模拟调用搜索 API}, time: Duration(milliseconds: 800));// 登录状态变化提示ever(isLoggedIn, (status) {if (status == true) {Get.snackbar("登录成功", "欢迎你,${username.value} 🎉");} else {Get.snackbar("退出登录", "已成功退出 👋");}});}void login() {if (username.value.isNotEmpty) {isLoggedIn.value = true;}}void logout() {isLoggedIn.value = false;}
}
📄 main.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'controller.dart';void main() {runApp(GetMaterialApp(home: LoginPage()));
}class LoginPage extends StatelessWidget {final auth = Get.put(AuthController());Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("GetX Worker 示例")),body: Padding(padding: const EdgeInsets.all(24),child: Column(children: [TextField(decoration: InputDecoration(labelText: "用户名(用于模拟搜索)"),onChanged: (value) => auth.username.value = value,),SizedBox(height: 20),Obx(() => auth.isLoggedIn.value? Column(children: [Text("已登录为:${auth.username.value}",style: TextStyle(fontSize: 18)),SizedBox(height: 10),ElevatedButton(onPressed: auth.logout,child: Text("退出登录"),),],): ElevatedButton(onPressed: auth.login,child: Text("登录"),)),],),),);}
}
✅ 运行效果
- 输入用户名时,不会立刻打印“搜索”,而是停止输入 800ms 后触发一次模拟搜索(防抖)。
- 点击“登录”后,
isLoggedIn
为true
,会自动触发ever
,显示Snackbar
提示“登录成功”。
🧠 技术要点
功能 | 技术手段 |
---|---|
搜索输入防抖 | debounce(username, ...) |
登录状态变化提示 | ever(isLoggedIn, ...) |
Snackbar 弹窗 | Get.snackbar(...) |
状态绑定 UI | Obx(() => ...) |
控制器全局管理 | Get.put(AuthController()) |
扩展上一个 Worker 示例,加入以下两个功能:
✅ 目标功能扩展
1. ✅ 模拟异步搜索请求(带加载动画)
- 当用户输入停止后,模拟调接口(2秒)
- 显示搜索中动画
- 请求完成后显示「已搜索:XXX」
2. ✅ 登录成功自动跳转到欢迎页面
- 用户输入用户名,点击登录
- 弹出 Snackbar 提示
- 自动跳转到欢迎页面(
WelcomePage
)
📦 新状态变量(在 Controller 中添加)
var isSearching = false.obs;
var searchResult = ''.obs;
📄 controller.dart(更新后的)
import 'package:get/get.dart';
import 'package:flutter/material.dart';class AuthController extends GetxController {var username = ''.obs;var isLoggedIn = false.obs;var isSearching = false.obs;var searchResult = ''.obs;void onInit() {super.onInit();// 防抖搜索debounce(username, (val) async {if (val.toString().isEmpty) return;isSearching.value = true;searchResult.value = '';print("开始搜索:$val");// 模拟异步接口调用await Future.delayed(Duration(seconds: 2));searchResult.value = "搜索完成:$val";isSearching.value = false;}, time: Duration(milliseconds: 800));// 登录提示 & 自动跳转ever(isLoggedIn, (status) {if (status == true) {Get.snackbar("登录成功", "欢迎你,${username.value} 🎉");Future.delayed(Duration(milliseconds: 800), () {Get.off(WelcomePage()); // 跳转欢迎页});} else {Get.snackbar("退出登录", "已成功退出 👋");}});}void login() {if (username.value.isNotEmpty) {isLoggedIn.value = true;}}void logout() {isLoggedIn.value = false;}
}
📄 main.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'controller.dart';void main() {runApp(GetMaterialApp(home: LoginPage()));
}class LoginPage extends StatelessWidget {final auth = Get.put(AuthController());Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("GetX Worker 扩展示例")),body: Padding(padding: const EdgeInsets.all(24),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [TextField(decoration: InputDecoration(labelText: "用户名"),onChanged: (value) => auth.username.value = value,),SizedBox(height: 16),// 搜索中或结果Obx(() {if (auth.isSearching.value) {return Row(children: [CircularProgressIndicator(strokeWidth: 2),SizedBox(width: 8),Text("搜索中..."),],);} else if (auth.searchResult.value.isNotEmpty) {return Text(auth.searchResult.value,style: TextStyle(color: Colors.green));} else {return Container();}}),SizedBox(height: 32),Obx(() => auth.isLoggedIn.value? Column(crossAxisAlignment: CrossAxisAlignment.start,children: [Text("已登录为:${auth.username.value}",style: TextStyle(fontSize: 18)),SizedBox(height: 10),ElevatedButton(onPressed: auth.logout,child: Text("退出登录"),),],): ElevatedButton(onPressed: auth.login,child: Text("登录"),)),],),),);}
}class WelcomePage extends StatelessWidget {Widget build(BuildContext context) {final username = Get.find<AuthController>().username.value;return Scaffold(appBar: AppBar(title: Text("欢迎页面")),body: Center(child: Text("欢迎回来,$username!", style: TextStyle(fontSize: 24)),),);}
}
✅ 运行效果
操作 | 效果 |
---|---|
输入用户名 | 停止输入 800ms 后开始「搜索」,2秒后显示搜索结果 |
点击登录按钮 | 弹出「登录成功」提示,800ms 后自动跳转欢迎页 |
欢迎页 | 展示当前用户名 |
🧠 技术亮点
功能 | 技术手段 |
---|---|
防抖搜索 | debounce(obs, callback) |
登录提示 | ever(isLoggedIn, callback) |
页面跳转 | Get.off() |
状态绑定显示 | Obx(() => ...) |
异步处理加载动画 | .obs + Future + CircularProgressIndicator |
是否还需要加上:
- ✅ 登录失败提示(用户名为空时)
- ✅ 登录后的 token 保存 / 本地持久化
- ✅ 从登录页自动恢复登录状态
✅ 4. 路由管理(导航)
GetX 的路由(导航)管理功能非常强大、简洁,无需 context
,还支持命名路由、无命名路由、参数传递、动画控制等。
📦 一、基础配置:使用 GetMaterialApp
在 main.dart
中使用:
void main() {runApp(GetMaterialApp(initialRoute: '/',getPages: AppRoutes.routes,));
}
📁 二、定义路由表(推荐结构)
创建 routes.dart
文件:
import 'package:get/get.dart';
import 'home_page.dart';
import 'detail_page.dart';class AppRoutes {static final routes = [GetPage(name: '/', page: () => HomePage()),GetPage(name: '/detail', page: () => DetailPage()),];
}
🚀 三、导航跳转方式
✅ 1. 普通跳转(非命名)
Get.to(DetailPage());
✅ 2. 命名路由跳转
Get.toNamed('/detail');
✅ 3. 返回上一页
Get.back();
✅ 4. 替换当前页(不能返回)
Get.off(DetailPage());
Get.offNamed('/detail');
✅ 5. 清空历史并跳转(常用于登录成功)
Get.offAllNamed('/home');
🎯 四、参数传递方式
✅ 方式一:通过 arguments
传递参数(推荐)
🔁 传参:
Get.toNamed('/detail', arguments: {'id': 123, 'title': '测试'});
🧾 接收:
final args = Get.arguments as Map;
print(args['id']); // 123
✅ 方式二:通过 URL 参数(路径参数)
📥 定义路由时设置参数:
GetPage(name: '/detail/:id',page: () => DetailPage(),
)
🔁 传参:
Get.toNamed('/detail/888');
🧾 接收:
final id = Get.parameters['id']; // "888"
✅ 也支持 query:
Get.toNamed('/detail/888?title=测试');
final title = Get.parameters['title']; // "测试"
✅ 五、完整例子
📄 main.dart
void main() {runApp(GetMaterialApp(initialRoute: '/',getPages: [GetPage(name: '/', page: () => HomePage()),GetPage(name: '/detail/:id', page: () => DetailPage()),],));
}
📄 home_page.dart
class HomePage extends StatelessWidget {Widget build(BuildContext context) {return Scaffold(body: Center(child: ElevatedButton(onPressed: () => Get.toNamed('/detail/123?title=你好'),child: Text("去详情页"),),),);}
}
📄 detail_page.dart
class DetailPage extends StatelessWidget {Widget build(BuildContext context) {final id = Get.parameters['id'];final title = Get.parameters['title'];return Scaffold(appBar: AppBar(title: Text("详情页")),body: Center(child: Text("ID: $id, 标题: $title"),),);}
}
🧠 常用跳转方法对比总结
方法 | 功能 |
---|---|
Get.to(Widget()) | 跳转新页面 |
Get.toNamed('/path') | 命名路由跳转 |
Get.off(...) | 替换当前页面 |
Get.offAll(...) | 清空栈跳转,常用于登录成功 |
Get.back() | 返回上一页 |
Get.arguments | 获取参数(Map) |
Get.parameters['id'] | 获取 URL 参数 |
在 GetX 中,每个路由都可以独立配置动画,通过 GetPage
的以下属性实现:
✅ 一、使用内建动画配置
你可以通过 transition
和 transitionDuration
快速配置内建动画:
GetPage(name: '/detail',page: () => DetailPage(),transition: Transition.rightToLeftWithFade, // 动画类型transitionDuration: Duration(milliseconds: 400), // 动画时间
)
🎬 内建动画类型一览(Transition 枚举):
动画类型 | 效果描述 |
---|---|
Transition.fade | 淡入淡出 |
Transition.rightToLeft | 从右向左滑动进入 |
Transition.leftToRight | 从左向右滑动进入 |
Transition.upToDown | 从上往下 |
Transition.downToUp | 从下往上 |
Transition.rightToLeftWithFade | 滑动 + 淡入淡出 |
Transition.zoom | 缩放 |
Transition.topLevel | 立体层叠感(较强) |
✅ 二、自定义动画 customTransition
如果内建动画不满足需求,可以自定义动画:
🔧 1. 创建自定义动画类:
class MyCustomTransition extends CustomTransition {Widget buildTransition(BuildContext context,Curve curve,Alignment alignment,Animation<double> animation,Animation<double> secondaryAnimation,Widget child,) {return SlideTransition(position: Tween<Offset>(begin: Offset(0, 1), // 从底部进来end: Offset.zero,).animate(animation),child: child,);}
}
📌 2. 应用在 GetPage
中:
GetPage(name: '/detail',page: () => DetailPage(),customTransition: MyCustomTransition(),transitionDuration: Duration(milliseconds: 500),
)
✅ 三、全局默认过渡动画(不推荐)
如果你希望所有页面默认使用统一动画,可以在 GetMaterialApp
中配置:
GetMaterialApp(defaultTransition: Transition.fade,transitionDuration: Duration(milliseconds: 300),
)
⚠️ 缺点:所有页面一刀切,不灵活。
✅ 四、页面跳转时单独设置动画(临时跳转)
Get.to(DetailPage(),transition: Transition.zoom,duration: Duration(milliseconds: 400),curve: Curves.easeInOut,
);
🎯 总结:路由动画配置选型
方式 | 优点 | 使用场景 |
---|---|---|
transition + duration | 简洁、常规页面跳转动画 | 大多数页面跳转 |
customTransition | 灵活自定义复杂动画 | 自定义 slide、fade、组合动画 |
defaultTransition | 快速统一默认动画 | 小项目统一风格 |
Get.to() 传入动画参数 | 单次临时跳转自定义 | 特定跳转需要额外动画时 |
✅ 5. 依赖注入
GetX 的依赖注入(Dependency Injection, 简称 DI)非常强大且简单,常用于控制器、服务类的自动注册与全局获取,避免你手动管理生命周期和传递 context
。
🧠 为什么使用 Get 的依赖注入?
- 不用手动传对象
- 生命周期自动管理
- 支持懒加载、永久实例、局部注入
- 比
Provider
简单很多
✅ 1. 基本用法:Get.put()
(立即注入)
📌 注册依赖
final controller = Get.put(MyController());
- 立即创建并注册
- 可在任何地方调用
Get.find<MyController>()
来获取
✅ 2. 懒加载:Get.lazyPut()
Get.lazyPut<MyController>(() => MyController());
- 在第一次使用时才创建
- 更节省内存,适合大项目中很多控制器
✅ 3. 永久依赖:Get.put(..., permanent: true)
Get.put(MyService(), permanent: true);
- 永久存在,
Get.reset()
也不会清除 - 适合如网络层、用户配置等全局服务
✅ 4. 获取依赖:Get.find<T>()
final c = Get.find<MyController>();
- 在任意位置获取,不需要 context
- 如果未注册,会抛错
✅ 5. 删除依赖:Get.delete<T>()
Get.delete<MyController>();
- 销毁注册的依赖,释放内存
✅ 6. 结合页面使用示例
👇 创建一个 Controller
class CounterController extends GetxController {var count = 0.obs;void increment() => count++;
}
👇 页面注册 & 使用(推荐在页面内 Get.put()
)
class CounterPage extends StatelessWidget {final controller = Get.put(CounterController());Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("计数器")),body: Center(child: Obx(() => Text("Count: ${controller.count}")),),floatingActionButton: FloatingActionButton(onPressed: controller.increment,child: Icon(Icons.add),),);}
}
✅ 7. 自动依赖绑定:使用 Bindings
适用于路由时自动注入依赖。
创建 Bindings 类:
class CounterBinding extends Bindings {void dependencies() {Get.lazyPut<CounterController>(() => CounterController());}
}
配置到路由:
GetPage(name: '/counter',page: () => CounterPage(),binding: CounterBinding(),
)
使用时跳转:
Get.toNamed('/counter'); // 会自动执行绑定
🧠 总结依赖注入方法
方法 | 用途 |
---|---|
Get.put() | 立即注入 |
Get.lazyPut() | 懒加载注入,首次使用才创建 |
Get.putAsync() | 异步创建实例 |
Get.find<T>() | 获取已注入实例 |
Get.delete<T>() | 删除实例 |
permanent: true | 保留永久实例(不被清除) |
Bindings | 自动依赖注入,推荐配合路由使用 |
以下是适合中大型项目的 GetX 推荐目录结构 + 依赖注入(DI)配置方案,可直接用于你实际项目中。
🗂 推荐项目结构
lib/
├── main.dart
├── app/
│ ├── routes/
│ │ ├── app_pages.dart ← 所有页面路由配置(含绑定)
│ │ └── app_routes.dart ← 所有路由名定义
│ ├── bindings/
│ │ ├── auth_binding.dart ← 登录相关依赖
│ │ └── global_binding.dart ← 全局一次性绑定(如网络服务)
│ ├── controllers/
│ │ ├── auth_controller.dart
│ │ └── home_controller.dart
│ ├── views/
│ │ ├── login_page.dart
│ │ └── home_page.dart
│ └── services/
│ ├── api_service.dart
│ └── storage_service.dart
🧠 1. 控制器定义(如 auth_controller.dart
)
class AuthController extends GetxController {var isLoggedIn = false.obs;void login(String username) {isLoggedIn.value = true;}void logout() {isLoggedIn.value = false;}
}
⚙️ 2. 单个 Binding 示例(如 auth_binding.dart
)
class AuthBinding extends Bindings {void dependencies() {Get.lazyPut<AuthController>(() => AuthController());}
}
⚙️ 3. 全局 Binding(如 global_binding.dart
)
class GlobalBinding extends Bindings {void dependencies() {// 注册全局服务Get.put<ApiService>(ApiService(), permanent: true);Get.put<StorageService>(StorageService(), permanent: true);}
}
🔁 4. 路由配置(app_pages.dart)
import 'package:get/get.dart';
import '../views/login_page.dart';
import '../views/home_page.dart';
import '../bindings/auth_binding.dart';class AppPages {static final routes = [GetPage(name: '/login',page: () => LoginPage(),binding: AuthBinding(),),GetPage(name: '/home',page: () => HomePage(),),];
}
🧭 5. 启动入口(main.dart)
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'app/routes/app_pages.dart';
import 'app/bindings/global_binding.dart';void main() {runApp(GetMaterialApp(initialRoute: '/login',getPages: AppPages.routes,initialBinding: GlobalBinding(), // 注册全局依赖debugShowCheckedModeBanner: false,));
}
✅ 跳转方式示例
Get.toNamed('/home');
控制器在跳转页面时会自动注入(如果该页面设置了
binding:
)
🚀 小贴士:常见依赖注入场景与方式
场景 | 推荐写法 |
---|---|
登录页面需要用到 AuthController | binding: AuthBinding() |
首页需要多个 controller | binding: BindingsBuilder(() => {...}) |
全局单例服务(如网络、缓存) | Get.put(Service(), permanent: true) |
获取控制器实例 | final auth = Get.find<AuthController>(); |
🔚 总结
GetX 的依赖注入建议这样使用:
类型 | 使用方式 | 说明 |
---|---|---|
页面级 Controller | lazyPut + Binding | 路由进入时自动注入 |
全局服务 | put(..., permanent: true) | 启动时注入,全程共享 |
自定义 Service | 单独建 services/ 目录 | 网络、数据库、存储类都放这里 |
中央注册点 | 用 GlobalBinding 做一次性全局注入 | 避免在 main.dart 重复注入依赖 |
✅ 6. Snackbar / Dialog / BottomSheet
GetX 提供了非常方便的 Snackbar、Dialog 和 BottomSheet 管理方法,支持无 Context 调用,语法简洁,且自带动画和丰富参数。
1. Snackbar
基础用法
Get.snackbar('标题','内容信息',snackPosition: SnackPosition.BOTTOM, // 顶部还是底部duration: Duration(seconds: 3), // 显示时间backgroundColor: Colors.blueGrey,colorText: Colors.white,
);
常用参数
参数 | 说明 |
---|---|
title | 标题文本 |
message | 内容文本 |
snackPosition | 显示位置(TOP 或 BOTTOM) |
duration | 显示时间 |
backgroundColor | 背景颜色 |
colorText | 文字颜色 |
icon | 左侧图标 |
mainButton | 右侧按钮(Widget) |
2. Dialog(弹窗)
简单对话框
Get.defaultDialog(title: '提示',middleText: '你确定要删除吗?',textConfirm: '确认',textCancel: '取消',onConfirm: () {print('确认删除');Get.back();},onCancel: () {print('取消删除');},
);
自定义内容对话框
Get.dialog(AlertDialog(title: Text('自定义标题'),content: Text('这是自定义内容'),actions: [TextButton(onPressed: () => Get.back(),child: Text('关闭'),)],),
);
3. BottomSheet(底部弹窗)
简单底部弹窗
Get.bottomSheet(Container(color: Colors.white,padding: EdgeInsets.all(16),child: Wrap(children: [ListTile(leading: Icon(Icons.photo),title: Text('照片'),onTap: () => Get.back(),),ListTile(leading: Icon(Icons.music_note),title: Text('音乐'),onTap: () => Get.back(),),],),),backgroundColor: Colors.white,shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20)),),
);
4. 关闭弹窗或 Snackbar
Get.back(); // 关闭当前弹窗、Snackbar、BottomSheet 等
5. 结合示例
ElevatedButton(onPressed: () {Get.snackbar('Hi', '这是一个消息提示');},child: Text('显示Snackbar'),
);ElevatedButton(onPressed: () {Get.defaultDialog(title: '确认',middleText: '是否删除该条数据?',textConfirm: '是',textCancel: '否',onConfirm: () {print('删除');Get.back();},);},child: Text('显示Dialog'),
);ElevatedButton(onPressed: () {Get.bottomSheet(Container(height: 200,color: Colors.white,child: Center(child: Text('这是一个底部弹窗')),),);},child: Text('显示BottomSheet'),
);
下面是一个完整 Flutter 页面示例,集成 GetX 的 Snackbar、Dialog、BottomSheet,带按钮交互,包含关闭逻辑和动画配置,方便直接拿去用。
import 'package:flutter/material.dart';
import 'package:get/get.dart';class GetUIExamplePage extends StatelessWidget {Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('GetX Snackbar/Dialog/BottomSheet 示例'),),body: Padding(padding: const EdgeInsets.all(20),child: Column(children: [ElevatedButton(onPressed: () {Get.snackbar('提示','这是一条消息提示',snackPosition: SnackPosition.BOTTOM,duration: Duration(seconds: 4),backgroundColor: Colors.blueGrey.shade700,colorText: Colors.white,icon: Icon(Icons.info, color: Colors.white),mainButton: TextButton(onPressed: () => Get.back(),child: Text('关闭', style: TextStyle(color: Colors.white)),),);},child: Text('显示 Snackbar'),),SizedBox(height: 20),ElevatedButton(onPressed: () {Get.defaultDialog(title: '确认操作',middleText: '确定要删除这条记录吗?',textConfirm: '确认',textCancel: '取消',barrierDismissible: false,onConfirm: () {Get.back();Get.snackbar('删除', '记录已删除', snackPosition: SnackPosition.BOTTOM);},onCancel: () => Get.back(),);},child: Text('显示 Dialog'),),SizedBox(height: 20),ElevatedButton(onPressed: () {Get.bottomSheet(Container(decoration: BoxDecoration(color: Colors.white,borderRadius: BorderRadius.vertical(top: Radius.circular(20)),),padding: EdgeInsets.all(16),child: Wrap(children: [ListTile(leading: Icon(Icons.photo),title: Text('相册'),onTap: () {Get.back();Get.snackbar('选择', '点击了相册', snackPosition: SnackPosition.BOTTOM);},),ListTile(leading: Icon(Icons.camera_alt),title: Text('拍照'),onTap: () {Get.back();Get.snackbar('选择', '点击了拍照', snackPosition: SnackPosition.BOTTOM);},),ListTile(leading: Icon(Icons.cancel),title: Text('取消'),onTap: () => Get.back(),),],),),isDismissible: true,enableDrag: true,shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20)),),);},child: Text('显示 BottomSheet'),),],),),);}
}
说明
- Snackbar 带关闭按钮和图标,位置在底部,自动消失
- Dialog 有确认和取消按钮,且点击遮罩不可关闭(
barrierDismissible: false
) - BottomSheet 带圆角,上滑可拖拽关闭,包含几个选项点击时关闭并提示
你只需
Get.to(() => GetUIExamplePage());
✅ 7. 国际化(多语言)
GetX 内置了强大的本地化(国际化)支持,让你快速实现多语言切换,且使用简单灵活。
📦 基础使用步骤
1. 创建语言翻译类,继承 Translations
import 'package:get/get.dart';class Messages extends Translations {Map<String, Map<String, String>> get keys => {'en_US': {'hello': 'Hello World','login': 'Login',},'zh_CN': {'hello': '你好,世界','login': '登录',},};
}
2. 在 GetMaterialApp
中配置
GetMaterialApp(translations: Messages(), // 绑定翻译类locale: Locale('en', 'US'), // 默认语言fallbackLocale: Locale('en', 'US'), // 找不到翻译时的兜底语言home: MyHomePage(),
);
3. 页面中使用 .tr
获取对应语言文本
Text('hello'.tr), // 自动根据当前语言显示文本
ElevatedButton(onPressed: () => print('login'.tr),child: Text('login'.tr),
),
4. 动态切换语言
// 切换为中文
Get.updateLocale(Locale('zh', 'CN'));// 切换为英语
Get.updateLocale(Locale('en', 'US'));
✅ 完整示例
import 'package:flutter/material.dart';
import 'package:get/get.dart';void main() {runApp(GetMaterialApp(translations: Messages(),locale: Locale('en', 'US'),fallbackLocale: Locale('en', 'US'),home: HomePage(),));
}class Messages extends Translations {Map<String, Map<String, String>> get keys => {'en_US': {'hello': 'Hello World', 'login': 'Login'},'zh_CN': {'hello': '你好,世界', 'login': '登录'},};
}class HomePage extends StatelessWidget {Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('hello'.tr)),body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [Text('login'.tr),SizedBox(height: 20),ElevatedButton(onPressed: () => Get.updateLocale(Locale('zh', 'CN')),child: Text('切换到中文'),),ElevatedButton(onPressed: () => Get.updateLocale(Locale('en', 'US')),child: Text('Switch to English'),),],),),);}
}
🧠 注意事项
keys
里每个 key 是语言_国家
格式,必须严格写对.tr
是String
的扩展方法,直接调用即可- 语言切换后,Get 会自动通知所有使用
.tr
的 Widget 更新 - 可以结合持久化保存用户选择的语言,下次启动时自动加载
完整的 GetX 本地化示例:
- 多语言 JSON 文件分离管理
- 从 JSON 读取语言包
- 语言切换后自动刷新 UI
- 语言偏好保存到本地,下次启动自动加载
1. 准备工作:在 assets/lang/
目录放语言 JSON 文件
assets/lang/en_US.json
assets/lang/zh_CN.json
示例 en_US.json
{"hello": "Hello World","login": "Login","logout": "Logout"
}
示例 zh_CN.json
{"hello": "你好,世界","login": "登录","logout": "退出登录"
}
2. 修改 pubspec.yaml
,声明资源文件
flutter:assets:- assets/lang/en_US.json- assets/lang/zh_CN.json
3. 创建 translation_service.dart
,实现从 JSON 加载翻译
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';class TranslationService extends Translations {static Locale? locale;static Locale fallbackLocale = Locale('en', 'US');// 用于缓存翻译Mapfinal Map<String, Map<String, String>> _translations = {};TranslationService() {_loadTranslations();}// 从 assets 加载所有语言 JSON 文件Future<void> _loadTranslations() async {final locales = ['en_US', 'zh_CN'];for (var loc in locales) {final jsonString =await rootBundle.loadString('assets/lang/$loc.json');final Map<String, dynamic> jsonMap = json.decode(jsonString);_translations[loc] = jsonMap.map((key, value) => MapEntry(key, value.toString()));}}Map<String, Map<String, String>> get keys => _translations;// 切换语言static void changeLocale(Locale newLocale) {locale = newLocale;Get.updateLocale(newLocale);}
}
注意:这里用
async
读取,建议在main()
里先初始化好翻译资源。
4. 修改 main.dart
,初始化语言资源并读取用户偏好
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'translation_service.dart';
import 'package:shared_preferences/shared_preferences.dart';void main() async {WidgetsFlutterBinding.ensureInitialized();final prefs = await SharedPreferences.getInstance();String? languageCode = prefs.getString('language_code') ?? 'en';String? countryCode = prefs.getString('country_code') ?? 'US';TranslationService.locale = Locale(languageCode, countryCode);runApp(MyApp());
}class MyApp extends StatelessWidget {final TranslationService _translationService = TranslationService();Widget build(BuildContext context) {return GetMaterialApp(translations: _translationService,locale: TranslationService.locale,fallbackLocale: TranslationService.fallbackLocale,home: HomePage(),);}
}
5. 创建一个示例 HomePage
,支持切换语言并保存偏好
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'translation_service.dart';class HomePage extends StatelessWidget {Future<void> _changeLanguage(Locale locale) async {TranslationService.changeLocale(locale);final prefs = await SharedPreferences.getInstance();await prefs.setString('language_code', locale.languageCode);await prefs.setString('country_code', locale.countryCode ?? '');}Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('hello'.tr),),body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [Text('login'.tr, style: TextStyle(fontSize: 24)),SizedBox(height: 20),ElevatedButton(onPressed: () => _changeLanguage(Locale('zh', 'CN')),child: Text('切换到中文'),),ElevatedButton(onPressed: () => _changeLanguage(Locale('en', 'US')),child: Text('Switch to English'),),],),),);}
}
6. 依赖添加
在 pubspec.yaml
加入:
dependencies:get: ^4.6.5shared_preferences: ^2.0.15
小结
- 语言文本放 JSON 文件,方便维护
TranslationService
负责加载 JSON 并提供翻译SharedPreferences
保存用户语言偏好,应用启动时读取并设置- 页面使用
.tr
获取文本,自动根据当前语言刷新 - 语言切换时调用
TranslationService.changeLocale()
并保存
GetX 中 Controller 回收机制
在使用 GetX 的 Controller(例如 GetxController
、Bindings
绑定的 Controller) 时,生命周期管理(即“回收”)非常关键,不当使用可能会造成内存泄漏或状态异常。
下面是关于 Controller 回收机制的说明,以及你在项目中应注意的关键问题。
✅ GetX 中 Controller 回收机制
1. 默认行为:Get.put() 注册的是单例
Get.put(MyController());
- 控制器将常驻内存,不会自动回收
- 调用
Get.delete<MyController>()
或Get.reset()
才会释放 permanent: true
时,即使手动调用Get.reset()
,也不会删除
2. 按需释放:Get.lazyPut()
Get.lazyPut(() => MyController());
- 懒加载,首次使用时才创建实例
- 默认会绑定页面,当页面被销毁时自动回收
3. 自动回收:使用 GetBuilder
、Get.put
并指定 tag
/fenix: false
Get.lazyPut(() => MyController(), fenix: false); // 页面销毁即销毁 controller
🔁 Controller 生命周期方法
你可以在 Controller 中覆盖这些方法:
class MyController extends GetxController {void onInit() {super.onInit();print('Controller 初始化');}void onReady() {super.onReady();print('页面渲染完成');}void onClose() {print('Controller 被销毁');super.onClose();}
}
🧠 实战注意事项
场景 | 推荐写法 | 注意事项 |
---|---|---|
页面间跳转带状态 | Get.lazyPut(() => Controller()) | 会随页面销毁 |
多页面共享状态 | Get.put(Controller(), permanent: true) | 手动释放 Get.delete() |
临时弹窗/小组件 | 使用 Get.create(() => Controller()) | 每次调用都创建新实例 |
获取已存在实例 | Get.find<Controller>() | 没有注册会报错 |
🧹 手动释放 Controller
Get.delete<MyController>();
- 如果你用的是
Get.put()
,Controller 不会自动销毁,必须手动释放 - 可加在
onClose()
、dispose()
、WillPopScope
中
🐞 常见坑 & 解决
❌ 控制器被重复创建
// 错误示例:每次都创建新 controller
Get.put(MyController()); // 多次执行
✅ 解决:
// 用 Get.putIfAbsent,避免重复创建
Get.put<MyController>(MyController(), permanent: false);
❌ Controller 没有销毁,状态不一致
✅ 检查是否用了 permanent: true
,如果不再需要,应手动:
Get.delete<MyController>();
✅ 建议的写法总结
目的 | 推荐写法 |
---|---|
页面内独立 controller | Get.lazyPut(() => Controller()) |
共享全局状态 | Get.put(() => Controller(), permanent: true) |
确保生命周期自动管理 | 使用 Bindings 注册 controller |
页面关闭主动释放 | Get.delete<Controller>() |
如果你在使用 GetX 做状态管理/DI/本地化,但不使用 Get.to()
/ Get.off()
等 GetX 路由 API,而是继续用 Flutter 原生的 Navigator.push()
,也是完全可以的——但你需要注意以下几点,否则会失去 GetX 的一些功能或引起生命周期问题。
✅ 总体策略
如果你使用
Navigator.push()
,但控制器是通过Get.put()
/Get.lazyPut()
提供的,你必须 手动管理 Controller 生命周期 或 手动注入绑定逻辑,否则可能出现重复注入或无法回收。
✅ 正确使用方式说明
✅ 场景 1:页面不需要独立 Controller(或使用全局 Controller)
你可以正常使用原生跳转:
Navigator.push(context,MaterialPageRoute(builder: (_) => HomePage()),
);
这时候只要你页面中用的是:
final authController = Get.find<AuthController>();
就可以继续享受 GetX 的状态管理、.obs
等。
✅ 场景 2:页面需要自己的 Controller + 生命周期管理
⚠️ 由于你没有用
GetPage()
路由定义,GetX 无法帮你自动绑定/释放 Controller,你就需要手动注册 + 销毁。
✅ 推荐写法:
class DetailPage extends StatelessWidget {final controller = Get.put(DetailController()); // 手动注册Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('详情')),body: Center(child: Obx(() => Text('value: ${controller.count}')),),);}
}
🔁 页面关闭后,在合适的地方释放:
void dispose() {Get.delete<DetailController>();super.dispose();
}
- 如果你用的是
StatelessWidget
,可以用WidgetsBindingObserver
+Get.delete()
或者改为StatefulWidget
✅ 场景 3:自己实现一个 NavigatorWrapper
做中间处理
你也可以封装一层 Navigator,用于兼容 Flutter 原生跳转 + GetX 注入生命周期:
void navigateWithInjection<T>(BuildContext context,Widget page, {required Bindings binding,
}) {binding.dependencies(); // 手动注入绑定Navigator.push(context,MaterialPageRoute(builder: (_) => page,),).then((_) {// 页面返回后手动释放Get.delete<T>();});
}
使用:
navigateWithInjection<DetailController>(context,DetailPage(),binding: BindingsBuilder(() {Get.put(DetailController());}),
);
🚫 不推荐的混用写法(示例)
// 页面 A 使用 Flutter Navigator.push
Navigator.push(context, MaterialPageRoute(builder: (_) => PageB()));// 页面 B 用了 GetBuilder 绑定了 Controller,但没有 GetPage 注入,也没手动注入
GetBuilder<MyController>(builder: (_) => ...)
⚠️ 这会导致:
MyController
根本没被注册(或重复注册)- 页面退出后不会释放
- 控制器是全局状态?还是每次新建?逻辑混乱
✅ 最推荐方案总结
目标 | 推荐方案 |
---|---|
使用 Get 的所有功能(路由、DI、生命周期) | 使用 Get.to() + GetPage + binding |
继续使用原生路由 + 使用 Controller | 手动 Get.put() + Get.delete() |
页面中不涉及独立 Controller | 可直接用 Navigator.push() + Get.find() 获取全局依赖 |
自定义兼容方案 | 封装 push + binding/deletion 工具方法 |
👉 总结一句话:
如果你选择使用 Flutter 原生导航,你必须自己负责控制器的注册与释放,GetX 不会自动帮你做这些了。
在使用 GetX 的 GetxController
时,理解并正确使用其生命周期方法对资源管理、网络请求、控制器重用等场景非常关键。
🧭 GetxController 生命周期方法一览(按执行顺序)
方法名 | 触发时机 | 作用 |
---|---|---|
onInit() | Controller 被创建后第一次调用 | 初始化数据、订阅、加载缓存等 |
onReady() | Widget 渲染完毕(可获取 context) | 适合启动动画、发送请求、打开 dialog 等 |
onClose() | Controller 被销毁前调用 | 清理资源(如取消订阅、定时器、监听器等) |
dispose() | 仅用于 GetxService ,一般不用 | 与 Flutter 原生 Widget 的 dispose 相同 |
✅ 推荐用法说明
onInit()
- 用于初始化变量、Rx监听器、计时器、数据拉取等
- 不要访问 context,它还没准备好
void onInit() {super.onInit();debounce(searchTerm, (_) => fetchResults(), time: Duration(milliseconds: 500));
}
onReady()
- 适合访问 UI 或调用依赖 context 的逻辑(比如打开 dialog、动画、focus)
- 会在
widget tree
build 完成后执行一次
void onReady() {super.onReady();Future.delayed(Duration(milliseconds: 300), () {Get.snackbar('提示', '页面加载完成');});
}
onClose()
- 回收资源、取消监听、关闭 stream、关闭计时器、断开 WebSocket 等都放这里
- 页面关闭、Controller 被销毁时自动调用
late Timer timer;
void onInit() {timer = Timer.periodic(Duration(seconds: 1), (_) => print('tick'));super.onInit();
}
void onClose() {timer.cancel();super.onClose();
}
⚠️ 常见误区
错误情况 | 原因 | 解决方案 |
---|---|---|
onClose() 不调用 | Controller 没有被释放(如 permanent: true ) | 改用非 permanent,或手动 Get.delete<>() |
在 onInit() 里访问 context | context 尚未可用 | 使用 onReady() |
订阅 .obs 没有清理 | 内存泄漏 | 用 ever() 、debounce() 时在 onClose() 里调用 dispose() |
Controller 只用了一次却没销毁 | 忘记用 lazyPut() /绑定页面,或 Get.put() 没 delete | 用绑定系统或在页面返回时手动 Get.delete() |
🌟 典型 Controller 生命周期例子
class LoginController extends GetxController {final RxString username = ''.obs;late Worker _worker;void onInit() {super.onInit();// 输入防抖_worker = debounce(username, (_) {print("用户名变化: $username");}, time: Duration(milliseconds: 500));}void onReady() {super.onReady();print('登录页就绪,可以显示动画或提示');}void onClose() {_worker.dispose();print('LoginController 已销毁');super.onClose();}
}
✅ 最佳实践总结
你要做什么 | 放在哪个生命周期中 |
---|---|
初始化变量、绑定 Rx 监听 | onInit() |
动画/弹窗/访问 context | onReady() |
清理 Rx Worker、Timer、Stream | onClose() |
控制器不释放,生命周期不触发 | 检查是否用了 permanent 或未 delete |
我们来继续扩展你的文档内容,添加一节关于 “多个页面共用同一个 Controller” 的处理方式与注意事项,将如下内容追加到你现有文档末尾:
🔄 多个页面共用同一个 Controller:处理方式与注意事项
在大型应用中,为了保持状态一致、避免重复逻辑,我们常常希望多个页面共用一个 Controller,例如登录状态、购物车、用户信息等。
✅ 实现方式
最常用的方式是使用 Get.put()
或 Get.lazyPut()
注册为全局 Controller:
// 在 GlobalBinding 或 main 中注入
Get.put<UserController>(UserController(), permanent: true);
在多个页面中使用:
final userController = Get.find<UserController>();
✅ 可选方式:使用 Tag 区分多个实例(不推荐用于共享场景)
除非你需要多个实例互不干扰,否则不要用 tag:
Get.put(UserController(), tag: 'A');
Get.put(UserController(), tag: 'B');
🧠 注意事项
⚠️ 问题 | 说明与建议 |
---|---|
Controller 被重复创建 | 避免在每个页面中都写 Get.put() ,应通过 Get.find() 获取已存在实例 |
生命周期混乱,onClose() 不调用 | 若用了 permanent: true 或未被销毁,onClose() 不会触发 |
控制器数据被提前销毁 | 避免将共享 Controller 用 lazyPut 且未设置 fenix: true |
同步多个页面 UI 更新 | 使用 .obs 、Obx() 、GetBuilder() 保证响应式更新 |
修改数据后部分页面未更新 | 检查是否遗漏 .obs 或未正确包裹 UI |
✅ 推荐 Controller 注册方式
场景 | 推荐注入方式 |
---|---|
多页面共享状态(如用户信息) | Get.put(UserController(), permanent: true) |
页面独立状态管理 | Get.lazyPut(() => PageController()) |
动态创建多个实例 | Get.put(..., tag: 'xxx') |
💡 示例:用户控制器在多个页面共享
// controller
class UserController extends GetxController {var username = ''.obs;
}// 页面 A
Obx(() => Text('用户名:${Get.find<UserController>().username.value}'))// 页面 B
Get.find<UserController>().username.value = '新名字';
此时 A、B 页面都会自动同步更新。
好的,我来为你的文档添加一节完整示例,展示:
✅ 多个页面共享一个 Controller
✅ 页面间通过 GetX 路由跳转
✅ 动画过渡
✅ 控制器数据联动更新
🧪 示例:多个页面共享同一个 Controller + 路由动画 + 数据联动
🎯 场景说明
- 有两个页面:
ProfilePage
和EditNamePage
- 它们共用
UserController
,编辑名字后自动同步到另一个页面 - 路由跳转带有动画
🧬 Step 1:UserController
class UserController extends GetxController {var username = '张三'.obs;
}
🖼️ Step 2:ProfilePage(显示用户名)
class ProfilePage extends StatelessWidget {final userController = Get.find<UserController>();Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('用户信息')),body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [Obx(() => Text('用户名:${userController.username.value}',style: TextStyle(fontSize: 24))),SizedBox(height: 20),ElevatedButton(onPressed: () {Get.toNamed('/edit');},child: Text('编辑用户名'),),],),),);}
}
✏️ Step 3:EditNamePage(修改用户名)
class EditNamePage extends StatelessWidget {final userController = Get.find<UserController>();final TextEditingController textController = TextEditingController();Widget build(BuildContext context) {textController.text = userController.username.value;return Scaffold(appBar: AppBar(title: Text('编辑用户名')),body: Padding(padding: const EdgeInsets.all(16.0),child: Column(children: [TextField(controller: textController),SizedBox(height: 20),ElevatedButton(onPressed: () {userController.username.value = textController.text;Get.back(); // 返回上一页},child: Text('保存'),)],),),);}
}
🧭 Step 4:路由配置(含动画)
GetMaterialApp(initialRoute: '/profile',getPages: [GetPage(name: '/profile',page: () => ProfilePage(),),GetPage(name: '/edit',page: () => EditNamePage(),transition: Transition.downToUp,transitionDuration: Duration(milliseconds: 400),),],initialBinding: BindingsBuilder(() {Get.put(UserController(), permanent: true); // 全局共享}),
);
✅ 效果预期
- 打开
ProfilePage
显示用户名 - 点击“编辑用户名”进入
EditNamePage
(带动画) - 修改并保存后返回
ProfilePage
,页面立即更新
如何使用多个 Controller 实例,并包括创建、管理、销毁、避免冲突等实践。
🧬 多个 Controller 实例的管理方式
在某些场景中(如多个 tab、多个子组件、多个动态生成的 item),你可能需要为同一个 Controller 创建多个独立实例。GetX 提供两种方式来实现:
✅ 方式一:使用 tag
区分多个实例
GetX 的
tag
就像“命名空间”,可以让你为同一个 Controller 类型创建多个独立的实例。
🔧 注册多个实例
Get.put(OrderController(), tag: 'order1');
Get.put(OrderController(), tag: 'order2');
📦 获取实例
final order1 = Get.find<OrderController>(tag: 'order1');
final order2 = Get.find<OrderController>(tag: 'order2');
✅ 方式二:使用 Get.create()
(每次都新建)
Get.create()
不会缓存 Controller,每次调用都创建一个新实例,适合临时组件或弹窗用。
Get.create(() => TempController()); // 每次调用都是新的
使用时:
final tempController = Get.put(TempController());
⚠️ 注意事项与最佳实践
问题 | 说明与建议 |
---|---|
❌ 用 Get.put() 多次注册无 tag | 会抛异常或覆盖原实例,建议加 tag 区分 |
✅ 控制器按需使用后释放 | 用完后手动 Get.delete<OrderController>(tag: 'xxx') |
✅ tag 命名唯一且可追踪 | 建议统一格式,如 "chat_user_$id" 或 "product_$index" |
✅ 使用 BindingsBuilder 创建 | 支持多 tag 注入,保持结构清晰 |
🌟 示例:多个订单页面使用不同的控制器实例
📦 Controller:
class OrderController extends GetxController {final String orderId;OrderController(this.orderId);var status = ''.obs;void onInit() {super.onInit();status.value = '正在加载订单 $orderId';}
}
🖼️ 页面中使用:
class OrderPage extends StatelessWidget {final String orderId;OrderPage(this.orderId);Widget build(BuildContext context) {final controller = Get.put(OrderController(orderId), tag: orderId);return Scaffold(appBar: AppBar(title: Text('订单 $orderId')),body: Obx(() => Text(controller.status.value)),);}
}
🧭 路由跳转:
Get.to(() => OrderPage('order_001'));
Get.to(() => OrderPage('order_002'));
每个页面拥有各自的 Controller 实例,互不干扰。
✅ 总结:使用多个 Controller 实例的建议
场景 | 推荐方法 |
---|---|
同一类型 Controller 多实例 | 使用 tag |
临时或短生命周期 Controller | 使用 Get.create() |
页面绑定多个 Controller 实例 | 用 BindingsBuilder() + tag |
实例用完释放 | 手动 Get.delete<T>(tag: ...) |
下面是「带多个 Tab,每个 Tab 使用独立 Controller 实例」的完整示例。
🧪 示例:Tab 页面中每个 Tab 使用独立的 Controller 实例
🎯 场景说明
- 页面有多个 Tab,每个 Tab 显示独立数据(如:推荐、热门、最新)
- 每个 Tab 用一个独立的
TabController
- 所有 Tab 使用相同类型的
NewsController
,通过tag
区分 - 数据互不干扰,生命周期由页面控制
🧬 Step 1:创建 Controller
class NewsController extends GetxController {final String category;NewsController(this.category);var articles = <String>[].obs;void onInit() {super.onInit();fetchArticles();}void fetchArticles() {// 模拟网络加载articles.value = List.generate(5, (index) => '$category 新闻 $index');}
}
🖼️ Step 2:主 Tab 页面
class NewsTabPage extends StatelessWidget {final tabs = ['推荐', '热门', '最新'];Widget build(BuildContext context) {return DefaultTabController(length: tabs.length,child: Scaffold(appBar: AppBar(title: Text('新闻中心'),bottom: TabBar(tabs: tabs.map((t) => Tab(text: t)).toList(),),),body: TabBarView(children: tabs.map((category) {final tag = 'news_$category';// 每个 tab 注入自己的 controller(只注入一次)if (!Get.isRegistered<NewsController>(tag: tag)) {Get.put(NewsController(category), tag: tag);}return NewsTabContent(tag: tag);}).toList(),),),);}
}
🧩 Step 3:Tab 内容组件
class NewsTabContent extends StatelessWidget {final String tag;const NewsTabContent({required this.tag});Widget build(BuildContext context) {final controller = Get.find<NewsController>(tag: tag);return Obx(() => ListView.builder(itemCount: controller.articles.length,itemBuilder: (_, i) => ListTile(title: Text(controller.articles[i]),),));}
}
🧹 Step 4:页面销毁时释放 Controller(可选)
你可以在页面 pop 时手动释放所有 tab 的 controller:
void dispose() {for (final tab in ['推荐', '热门', '最新']) {Get.delete<NewsController>(tag: 'news_$tab');}super.dispose();
}
或者设置 fenix: false
时自动释放(只要不用 permanent)。
✅ 小结
优势 | 实现方式 |
---|---|
每个 Tab 独立逻辑与状态 | Get.put(..., tag: category) |
相同 Controller 类型复用结构 | 通过 tag 实现实例区分 |
生命周期清晰,资源可回收 | 页面退出手动 Get.delete(tag: ...) |
源码地址