源碼地址
在 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: ...) |
源碼地址