前言
使用Bloc的時候,有一個讓我至今為止十分在意的問題,無法真正的跨頁面互動!在反覆的查閱官方文件後,使用一個全域性Bloc的方式,實現了“偽”跨頁面互動,詳細可檢視:flutter_bloc使用解析;fish_redux的廣播機制是可以比較完美的實現跨頁面互動的,我也寫了一篇幾萬字文章介紹如何使用該框架:fish_redux使用詳解,redux層次劃分是比較細的,寫起來會很費勁;最近嘗試了GetX相關功能,解決了我的相當一部分痛點
把整篇文章寫完後,我馬上把自己的一個demo裡面所有Bloc程式碼全用GetX替換,且去掉了Fluro框架;感覺用Getx雖然會省掉大量的模板程式碼,但還是有些重複工作:建立資料夾,建立幾個必備檔案,寫那些必須要寫的初始化程式碼和類;略微繁瑣,為了對得起GetX給我開發帶來的巨大便利,我就花了一些時間,給它寫了一個外掛! 上面這重複的程式碼,檔案,資料夾統統能一鍵生成!
GetX相關優勢
依賴注入
- GetX是透過依賴注入的方式,儲存相應的XxxGetxController;已經脫離了InheritedWidget那一套玩法,自己手動去管理這些例項,使用場景被大大擴充
- 簡單的思路,卻能產生深遠的影響:優雅的跨頁面功能便是基於這種設計而實現的、獲取例項無需BuildContext、GetBuilder自動化的處理及其減少了入參等等
跨頁面互動
- 這絕對是GetX的一個優點!對於複雜的生產環境,跨頁面互動的場景,實在太常見了,GetX的跨頁面互動,實現的也較為優雅
路由管理
- getx內部實現了路由管理,而且用起來,非常簡單!bloc沒實現路由管理,我不得不找一個star量高的路由框架,就選擇了fluro,但是不得不吐槽下,fluro用起來真的很折磨人,每次新建一個頁面,最讓我抗拒的就是去寫fluro路由程式碼,橫跨幾個檔案來回寫,頭皮發麻
- GetX實現了動態路由傳參,也就是說直接在命名路由上拼引數,然後能拿到這些拼在路由上的引數,也就是說用flutter寫H5,直接能透過Url傳值,OMG!可以無腦捨棄複雜的fluro了
- 實現了全域性BuildContext
- 國際化,主題實現
如果深度使用過Provider,Bloc這類依賴InheritedWidget建立起的狀態管理框架;再看看GetX內部實現思想,就能發現,他們已經是倆種體系的東西了
對此,我來丟擲一些問題:InheritedWidget存在什麼缺點?為什麼其資料傳遞和路由設計思想對立?為什麼getx使用依賴注入?getx的obx自動重新整理黑魔法是個什麼鬼?
- 對這些感興趣的小夥伴,可以看看:Flutter GetX深度剖析 | 我們終將走出自己的路(萬字圖文)
下來將全面的介紹GetX的使用,文章也不分篇水閱讀量了,力求一文寫清楚,方便大家隨時查閱
準備
引入
- 首先匯入GetX的外掛
# getx 狀態管理框架 https://pub.flutter-io.cn/packages/get
# 非空安全最後一個版本(flutter 2.0之前版本)
get: ^3.26.0
# 空安全版本 最新版本請檢視 https://pub.flutter-io.cn/packages/get
get: ^4.3.8
GetX地址
- Github:jonataslaw/getx
- Pub:get
主入口配置
- 只需要將
MaterialApp
改成GetMaterialApp
即可
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
home: CounterGetPage(),
);
}
}
- 各模組導包,均使用下面包即可
import 'package:get/get.dart';
外掛
這個getx程式碼生成外掛,我花了不少精力去完善,功能已經比較齊全了,希望對大家有所幫助。
歡迎大家提issue,提issue之前,請務必認真檢視文件:GetX程式碼生成IDEA外掛,超詳細功能講解,確保想提的需求,在本外掛裡面未被實現;上次有個老哥給我連開三個issue,提的需求都是早已實現的功能。。。
說明
外掛地址
- Github:getx_template
- Jetbrains:getx_template
外掛的功能含義
Model:生成GetX的模式
- Default:預設模式,生成三個檔案:state,logic,view
- Easy:簡單模式,生成倆個檔案:logic,view
- Module Name:模組的名稱,請使用大駝峰或小駝峰命名
- 外掛詳細功能說明,請查閱:GetX程式碼生成IDEA外掛,超詳細功能講解
安裝
- 在設定裡面選擇:Plugins ---> 輸入“getx”搜尋 ---> 選擇名字為:“GeX” ---> 然後安裝 ---> 最後記得點選下“Apply”
效果圖
- 生成模板程式碼彈窗
- 提供字尾名修改,也支援了持久化
Alt + Enter : 可以選擇包裹Widget,有四種可選:GetBuilder、GetBuilder(Auto Dispose),Obx、GetX,大大方便開發喲(^U^)ノ~YO
- 如果你發現某個頁面,你的GetXController無法回收,可以使用 GetBuilder(Auto Dispose)Wrap 你的 Widget
快捷程式碼片段提示:我自己寫了很多,也有一部分直接引用:getx-snippets-intelliJ
- 輸入 getx 字首便有提示
計數器
效果圖
- 體驗一下
實現
首先,當然是實現一個簡單的計數器,來看GetX怎麼將邏輯層和介面層解耦的
來使用外掛生成下簡單檔案
- 模式選擇:Easy
- 功能選擇:useFolder
來看下生成的預設程式碼,預設程式碼十分簡單,詳細解釋放在倆種狀態管理裡
- logic
import 'package:get/get.dart';
class CounterGetLogic extends GetxController {
}
- view
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'logic.dart';
class CounterGetPage extends StatelessWidget {
final logic = Get.put(CounterGetLogic());
@override
Widget build(BuildContext context) {
return Container();
}
}
簡單狀態管理
GetBuilder:這是一個極其輕巧的狀態管理器,佔用資源極少!
logic:先來看看logic層
- 因為是處理頁面邏輯的,加上Controller單詞過長,也防止和Flutter自帶的一些控制元件控制器弄混,所以該層用
logic
結尾,這裡就定為了logic
層 - 當然這點隨個人意向,寫Event,Controller均可(外掛生成程式碼,支援自定義通用字尾)
- 因為是處理頁面邏輯的,加上Controller單詞過長,也防止和Flutter自帶的一些控制元件控制器弄混,所以該層用
class CounterEasyLogic extends GetxController {
var count = 0;
void increase() {
++count;
update();
}
}
- view
class CounterEasyPage extends StatelessWidget {
final logic = Get.put(CounterEasyLogic());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('計數器-簡單式')),
body: Center(
child: GetBuilder<CounterEasyLogic>(builder: (logic) {
return Text(
'點選了 ${logic.count} 次',
style: TextStyle(fontSize: 30.0),
);
}),
),
floatingActionButton: FloatingActionButton(
onPressed: () => logic.increase(),
child: Icon(Icons.add),
),
);
}
}
分析下:GetBuilder這個方法
- init:雖然上述程式碼沒用到,但是,這個引數是存在在GetBuilder中的,因為在載入變數的時候就使用
Get.put()
生成了CounterEasyGetLogic
物件,GetBuilder會自動查詢該物件,所以,就可以不使用init引數 - builder:方法引數,擁有一個入參,型別便是GetBuilder所傳入泛型的型別
- initState,dispose等:GetBuilder擁有StatefulWidget所有周期回撥,可以在相應回撥內做一些操作
- init:雖然上述程式碼沒用到,但是,這個引數是存在在GetBuilder中的,因為在載入變數的時候就使用
響應式狀態管理
當資料來源變化時,將自動執行重新整理元件的方法
logic層
- 這裡變數數值後寫
.obs
操作,是說明定義了該變數為響應式變數,當該變數數值變化時,頁面的重新整理方法將自動重新整理 - 基礎型別,List,類都可以加
.obs
,使其變成響應式變數
- 這裡變數數值後寫
class CounterRxLogic extends GetxController {
var count = 0.obs;
///自增
void increase() => ++count;
}
- view層
class CounterRxPage extends StatelessWidget {
final logic = Get.put(CounterRxLogic());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('計數器-響應式')),
body: Center(
child: Obx(() {
return Text(
'點選了 ${logic.count.value} 次',
style: TextStyle(fontSize: 30.0),
);
}),
),
floatingActionButton: FloatingActionButton(
onPressed: () => logic.increase(),
child: Icon(Icons.add),
),
);
}
}
- 可以發現重新整理元件的方法極其簡單:
Obx()
,這樣可以愉快的到處寫定點重新整理操作了 Obx()方法重新整理的條件
- 只有當響應式變數的值發生變化時,才會會執行重新整理操作,當某個變數初始值為:“test”,再賦值為:“test”,並不會執行重新整理操作
- 當你定義了一個響應式變數,該響應式變數改變時,包裹該響應式變數的Obx()方法才會執行重新整理操作,其它的未包裹該響應式變數的Obx()方法並不會執行重新整理操作,Cool!
來看下如果把整個類物件設定成響應型別,如何實現更新操作呢?
- 下面解釋來自官方README文件
- 這裡嘗試了下,將整個類物件設定為響應型別,當你改變了類其中一個變數,然後執行更新操作,
只要包裹了該響應類變數的Obx(),都會實行重新整理操作
,將整個類設定響應型別,需要結合實際場景使用
// model
// 我們將使整個類成為可觀察的,而不是每個屬性。
class User{
User({this.name = '', this.age = 0});
String name;
int age;
}
// controller
final user = User().obs;
//當你需要更新user變數時。
user.update( (user) { // 這個引數是你要更新的類本身。
user.name = 'Jonny';
user.age = 18;
});
// 更新user變數的另一種方式。
user(User(name: 'João', age: 35));
// view
Obx(()=> Text("Name ${user.value.name}: Age: ${user.value.age}"));
// 你也可以不使用.value來訪問模型值。
user().name; // 注意是user變數,而不是類變數(首字母是小寫的)。
總結
分析
Obx是配合Rx響應式變數使用、GetBuilder是配合update使用:請注意,這完全是倆套定點重新整理控制元件的方案
- 區別:前者響應式變數變化,Obx自動重新整理;後者需要使用update手動呼叫重新整理
- 每一個響應式變數,都需要生成對應的
GetStream
,佔用資源大於基本資料型別,會對記憶體造成一定壓力 GetBuilder
內部實際上是對StatefulWidget的封裝,所以佔用資源極小
使用場景
- 一般來說,對於大多數場景都是可以使用響應式變數的
- 但是,在一個包含了大量物件的List,都使用響應式變數,將生成大量的
GetStream
,必將對記憶體造成較大的壓力,該情況下,就要考慮使用簡單狀態管理了 總的來說:推薦GetBuilder和update配合的寫法
GetBuilder內建回收GetxController的功能,能避免一些無法自動回收GetxController的坑爹問題
使用GetBuilder的自動回收:GetBuilder需要設定assignId: true;或使用外掛一鍵Wrap Widget:GetBuilder(Auto Dispose)
- 使用Obx,相關變數定義初始化以及實體更新和常規寫法不同,會對初次接觸該框架的人,造成很大的困擾
- getx的IDEA外掛現已支援一鍵Wrap Widget生成GetBuilder,可以一定程度上提升你的開發效率
跨頁面互動
跨頁面互動,在複雜的場景中,是非常重要的功能,來看看GetX怎麼實現跨頁面事件互動的
效果圖
- 體驗一下
- Cool,這才是真正的跨頁面互動!下級頁面能隨意呼叫上級頁面事件,且關閉頁面後,下次重進,資料也很自然重置了(全域性Bloc不會重置,需要手動重置)
實現
頁面一
常規程式碼
logic
- 這裡的自增事件,是供其它頁面呼叫的,該頁面本身沒使用
class GetJumpOneLogic extends GetxController {
var count = 0;
///跳轉到跨頁面
void toJumpTwo() {
Get.toNamed(RouteConfig.getJumpTwo, arguments: {'msg': '我是上個頁面傳遞過來的資料'});
}
///跳轉到跨頁面
void increase() {
count = ++count;
update();
}
}
view
- 此處就一個顯示文字和跳轉功能
class GetJumpOnePage extends StatelessWidget {
/// 使用Get.put()例項化你的類,使其對當下的所有子路由可用。
final logic = Get.put(GetJumpOneLogic());
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(title: Text('跨頁面-One')),
floatingActionButton: FloatingActionButton(
onPressed: () => logic.toJumpTwo(),
child: const Icon(Icons.arrow_forward_outlined),
),
body: Center(
child: GetBuilder<GetJumpOneLogic>(
builder: (logic) {
return Text('跨頁面-Two點選了 ${logic.count} 次',
style: TextStyle(fontSize: 30.0));
},
),
),
);
}
}
頁面二
這個頁面就是重點了
logic
- 將演示怎麼呼叫前一個頁面的事件
- 怎麼接收上個頁面資料
- 請注意,
GetxController
包含比較完整的生命週期回撥,可以在onInit()
接受傳遞的資料;如果接收的資料需要重新整理到介面上,請在onReady
回撥裡面接收資料操作,onReady
是在addPostFrameCallback
回撥中呼叫,重新整理資料的操作在onReady
進行,能保證介面是初始載入完畢後才進行頁面重新整理操作的
class GetJumpTwoLogic extends GetxController {
var count = 0;
var msg = '';
@override
void onReady() {
var map = Get.arguments;
msg = map['msg'];
update();
super.onReady();
}
///跳轉到跨頁面
void increase() {
count = ++count;
update();
}
}
view
- 加號的點選事件,點選時,能實現倆個頁面資料的變換
- 重點來了,這裡透過
Get.find()
,獲取到了之前例項化GetXController,獲取某個模組的GetXController後就很好做了,可以透過這個GetXController去呼叫相應的事件,也可以透過它,拿到該模組的資料!
class GetJumpTwoPage extends StatelessWidget {
final oneLogic = Get.find<GetJumpOneLogic>();
final twoLogic = Get.put(GetJumpTwoLogic());
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(title: Text('跨頁面-Two')),
floatingActionButton: FloatingActionButton(
onPressed: () {
oneLogic.increase();
twoLogic.increase();
},
child: const Icon(Icons.add),
),
body: Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
//計數顯示
GetBuilder<GetJumpTwoLogic>(
builder: (logic) {
return Text('跨頁面-Two點選了 ${twoLogic.count} 次',
style: TextStyle(fontSize: 30.0));
},
),
//傳遞資料
GetBuilder<GetJumpTwoLogic>(
builder: (logic) {
return Text('傳遞的資料:${twoLogic.msg}',
style: TextStyle(fontSize: 30.0));
},
),
]),
),
);
}
}
總結
GetX這種的跨頁面互動事件,真的是非常簡單了,侵入性也非常的低,不需要在主入口配置什麼,在複雜的業務場景下,這樣簡單的跨頁面互動方式,就能實現很多事了
進階吧!計數器
我們可能會遇到過很多複雜的業務場景,在複雜的業務場景下,單單某個模組關於變數的初始化操作可能就非常多,在這個時候,如果還將state(狀態層)和logic(邏輯層)寫在一起,維護起來可能看的比較暈
這裡將狀態層和邏輯層進行一個拆分,這樣在稍微大一點的專案裡使用GetX,也能保證結構足夠清晰了!
在這裡就繼續用計數器舉例吧!
實現
此處需要劃分三個結構了:state(狀態層),logic(邏輯層),view(介面層)
這裡使用外掛生成下模板程式碼
- Model:選擇Default(預設)
- Function:useFolder(預設選中)
來看下生成的模板程式碼
- state
class GetCounterHighState {
GetCounterHighState() {
///Initialize variables
}
}
- logic
import 'package:get/get.dart';
import 'state.dart';
class GetCounterHighLogic extends GetxController {
final GetCounterHighState state = GetCounterHighState();
}
- view
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'logic.dart';
class GetCounterHighPage extends StatelessWidget {
final logic = Get.put(GetCounterHighLogic());
final state = Get.find<GetCounterHighLogic>().state;
@override
Widget build(BuildContext context) {
return Container();
}
}
為什麼寫成這樣三個模組,需要把State單獨提出來,請速速瀏覽下方
改造
state
- 這裡使用劃分出來的state層,來統一管理所有的狀態變數
- 涉及到狀態變數定義和Logic層徹底分開
class GetCounterHighState {
late int count;
GetCounterHighState() {
count = 0;
}
}
logic
- 邏輯層就比較簡單,需要注意的是:開始時需要例項化狀態類
class GetCounterHighLogic extends GetxController {
final GetCounterHighState state = GetCounterHighState();
///自增
void increase() {
state.count = ++state.count;
update();
}
}
view
- 實際上view層,和之前的幾乎沒區別,區別的是把狀態層給獨立出來了
- 因為
CounterHighGetLogic
被例項化,所以直接使用Get.find<CounterHighGetLogic>()
就能拿到剛剛例項化的邏輯層,然後拿到state,使用單獨的變數接收下 - ok,此時:logic只專注於觸發事件互動,state只專注資料
class GetCounterHighPage extends StatelessWidget {
final logic = Get.put(GetCounterHighLogic());
final state = Get.find<GetCounterHighLogic>().state;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('計數器-進階版')),
body: Center(
child: GetBuilder<GetCounterHighLogic>(
builder: (logic) {
return Text(
'點選了 ${state.count} 次',
style: TextStyle(fontSize: 30.0),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => logic.increase(),
child: Icon(Icons.add),
),
);
}
}
對比
看了上面的改造,螢幕前的你可能想吐槽了:坑比啊,之前簡簡單單的邏輯層,被拆成倆個,還搞得這麼麻煩,你是猴子請來的逗比嗎?
大家先別急著吐槽,當業務過於複雜,state層,也是會維護很多東西的,讓我們看看下面的一個小栗子,下面例項程式碼是不能直接執行的,想看詳細執行程式碼,請檢視專案:flutter_use
- state
class MainState {
///選擇index
late int selectedIndex;
///控制是否展開
late bool isUnfold;
///是否縮放
late bool isScale;
///分類按鈕資料來源
late List<BtnInfo> list;
///Navigation的item資訊
late List<BtnInfo> itemList;
///PageView頁面
late List<Widget> pageList;
late PageController pageController;
MainState() {
//初始化index
selectedIndex = 0;
//預設不展開
isUnfold = false;
//預設不縮放
isScale = false;
//PageView頁面
pageList = [
KeepAlivePage(FunctionPage()),
KeepAlivePage(ExamplePage()),
KeepAlivePage(SettingPage()),
];
//item欄目
itemList = [
BtnInfo(
title: "功能",
icon: Icon(Icons.bubble_chart),
),
BtnInfo(
title: "範例",
icon: Icon(Icons.opacity),
),
BtnInfo(
title: "設定",
icon: Icon(Icons.settings),
),
];
//頁面控制器
pageController = PageController();
}
}
- logic
class MainLogic extends GetxController {
final state = MainState();
@override
void onInit() {
///初始化應用資訊
InitConfig.initApp(Get.context);
super.onInit();
}
///切換tab
void switchTap(int index) {
state.selectedIndex = index;
state.pageController.jumpToPage(index);
update();
}
///是否展開側邊欄
void onUnfold(bool isUnfold) {
state.isUnfold = !state.isUnfold;
update();
}
///是否縮放
void onScale(bool isScale) {
state.isScale = !state.isScale;
update();
initWindow(scale: isScale ? 1.25 : 1.0);
}
}
- view
class MainPage extends StatelessWidget {
final MainLogic logic = Get.put(MainLogic());
final MainState state = Get.find<MainLogic>().state;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Row(children: [
///側邊欄區域
GetBuilder<MainLogic>(
builder: (logic) {
return SideNavigation(
selectedIndex: state.selectedIndex,
isUnfold: state.isUnfold,
isScale: state.isScale,
sideItems: state.itemList,
//點選item
onItem: (index) => logic.switchTap(index),
//展開側邊欄
onUnfold: (isUnfold) => logic.onUnfold(isUnfold),
//縮放整體佈局
onScale: (isScale) => logic.onScale(isScale),
);
},
),
///Expanded佔滿剩下的空間
Expanded(
child: PageView.builder(
physics: NeverScrollableScrollPhysics(),
itemCount: state.pageList.length,
itemBuilder: (context, index) => state.pageList[index],
controller: state.pageController,
),
)
]),
);
}
}
從上面可以看出,state層裡面的狀態已經較多了,當某些模組涉及到大量的:提交表單資料,跳轉資料,展示資料等等,state層的程式碼會相當的多,相信我,真的是非常多,一旦業務發生變更,還要經常維護修改,就蛋筒了
在複雜的業務下,將狀態層(state)和業務邏輯層(logic)分開,絕對是個明智的舉動
最後
- 該模組的效果圖就不放了,和上面計數器效果一模一樣,想體驗一下,可點選:體驗一下
- 簡單的業務模組,可以使用倆層結構:logic,view;複雜的業務模組,推薦使用三層結構:state,logic,view
Binding的使用
說明
大家可能發現了,外掛上增加了addBinding的功能
再加上getx的demo也用了binding,想必各位靚仔就非常想使用這個功能
這個功能實際的作用非常簡單
- 統一管理單模組使用的GetXController
- binding模組需要在getx路由頁面進行繫結;進入頁面的時候,統一懶注入binding模組的GetXController
這樣做當然有好處
- 可以統一管理複雜模組的多個GetXController
請注意
不建議在Get.to()方法裡面進行binding繫結
- 如果存在多個頁面跳轉到存在binding頁面,你的每個Get.to()方法都需要繫結
- 這樣極其容易出bug,對後面接盤的人,十分不友好
- 使用binding,你理應使用getx的命名路由
鄭重申明:不使用binding,並不會對功能有任何的影響
使用
首先必須搭建好路由模組
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
initialRoute: RouteConfig.testOne,
getPages: RouteConfig.getPages,
);
}
}
class RouteConfig {
static const String testOne = "/testOne";
static const String testTwo = "/testOne/testTwo";
static final List<GetPage> getPages = [
GetPage(
name: testOne,
page: () => TestOnePage(),
binding: TestOneBinding(),
),
GetPage(
name: testTwo,
page: () => TestTwoPage(),
binding: TestTwoBinding(),
),
];
}
建立頁面模組
- 選中addBinding功能
- 建立TestOne
///logic層
class TestOneLogic extends GetxController {
void jump() => Get.toNamed(RouteConfig.testTwo);
}
///view層
class TestOnePage extends StatelessWidget {
final logic = Get.find<TestOneLogic>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('頁面一')),
body: Center(child: Text('頁面一', style: TextStyle(fontSize: 30.0))),
floatingActionButton: FloatingActionButton(
onPressed: () => logic.jump(),
child: Icon(Icons.arrow_forward),
),
);
}
}
///binding層
class TestOneBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => TestOneLogic());
}
}
- TestTwo
///logic層
class TestTwoLogic extends GetxController {
}
///view層
class TestTwoPage extends StatelessWidget {
final logic = Get.find<TestTwoLogic>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('頁面二')),
body: Center(child: Text('頁面二', style: TextStyle(fontSize: 30.0))),
);
}
}
///binding層
class TestTwoBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => TestTwoLogic());
}
}
總結
這邊我寫了一個極其簡單的範例,僅僅是個跳轉頁面的功能,我覺得,應該可以展示binding的功能了
- 就是統一管理某個模組需要注入的多個GetXController
- 請注意,該注入是懶注入,只有使用了 find + 對應的泛型,才會被真正的注入的getx的全域性map例項裡
實際上,手動寫binding檔案,還是有點麻煩,寫了binding,view層的使用也需要做相應的變動
鐵汁們,為了幫你們節省點開發時間,這點浪費你們生命且沒什麼技術含量的事情,已經在外掛裡幫你完成
- 有需要的,選中addBinding功能即可
- GetPage裡面繫結binding的操作,只能麻煩你們自己動下手了,專案結構千變萬化,這玩意沒法定位
路由管理
GetX實現了一套用起來十分簡單的路由管理,可以使用一種極其簡單的方式導航,也可以使用命名路由導航
關於簡單路由和命名路由的區別
- 簡單路由:十分簡單,看下下面的例子
Get.to(SomePage());
命名路由
- 在web上,可以直接透過命名的url直接導航頁面
- 實現路由攔截的操作,舉一個官方文件的例子:很輕鬆的實現了一個未登入,跳轉登入頁面功能
GetStorage box = GetStorage();
GetMaterialApp(
getPages: [
GetPage(name: '/', page:(){
return box.hasData('token') ? Home() : Login();
})
]
)
簡單路由
- 主入口配置
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
home: MainPage(),
);
}
}
路由的相關使用
- 使用是非常簡單,使用Get.to()之類api即可,此處簡單演示,詳細api說明,放在本節結尾
//跳轉新頁面
Get.to(SomePage());
命名路由導航
這裡是推薦使用命名路由導航的方式
- 統一管理起了所有頁面
- 在app中可能感受不到,但是在web端,載入頁面的url地址就是命名路由你所設定字串,也就是說,在web中,可以直接透過url導航到相關頁面
下面說明下,如何使用
- 首先,在主入口出配置下
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
initialRoute: RouteConfig.main,
getPages: RouteConfig.getPages,
);
}
}
RouteConfig類
- 下面是我的相關頁面,和其對映的頁面,請根據自己的頁面進行相關編寫
class RouteConfig {
///主頁面
static const String main = "/";
///演示SmartDialog控制元件 喜馬拉雅 dialog頁面
static const String smartDialog = "/smartDialog";
static const String himalaya = "/himalaya";
static const String dialog = "/dialog";
///bloc計數器模組 Bloc跨頁面傳遞事件
static const String blCubitCounterPage = "/blCubitCounterPage";
static const String blBlocCounterPage = "/blBlocCounterPage";
static const String cubitSpanOne = "/cubitSpanOne";
static const String cubitSpanTwo = "/cubitSpanOne/cubitSpanTwo";
static const String streamPage = "/streamPage";
static const String blCustomBuilderPage = "/blCustomBuilderPage";
static const String counterEasyCPage = "/counterEasyCPage";
///測試佈局頁面
static const String testLayout = "/testLayout";
///GetX 計數器 跨頁面互動
static const String getCounterRx = "/getCounterRx";
static const String getCounterEasy = "/counterEasyGet";
static const String getCounterHigh = "/counterHighGet";
static const String getJumpOne = "/jumpOne";
static const String getJumpTwo = "/jumpOne/jumpTwo";
static const String getCounterBinding = "/getCounterBinding";
static const String counterEasyXBuilderPage = "/counterEasyXBuilder";
static const String counterEasyXEbxPage = "/counterEasyXEbx";
///Provider
static const String proEasyCounterPage = "/proEasyCounterPage";
static const String proHighCounterPage = "/proHighCounterPage";
static const String proSpanOnePage = "/proSpanOnePage";
static const String proSpanTwoPage = "/proSpanOnePage/proSpanTwoPage";
static const String testNotifierPage = "/testNotifierPage";
static const String customBuilderPage = "/customBuilderPage";
static const String counterEasyPPage = "/counterEasyPPage";
static const String counterGlobalEasyPPage = "/counterGlobalEasyPPage";
///別名對映頁面
static final List<GetPage> getPages = [
GetPage(name: main, page: () => MainPage()),
GetPage(name: dialog, page: () => DialogPage()),
GetPage(name: blCubitCounterPage, page: () => BlCubitCounterPage()),
GetPage(name: blBlocCounterPage, page: () => BlBlocCounterPage()),
GetPage(name: streamPage, page: () => StreamPage()),
GetPage(name: blCustomBuilderPage, page: () => BlCustomBuilderPage()),
GetPage(name: counterEasyCPage, page: () => CounterEasyCPage()),
GetPage(name: testLayout, page: () => TestLayoutPage()),
GetPage(name: smartDialog, page: () => SmartDialogPage()),
GetPage(name: cubitSpanOne, page: () => CubitSpanOnePage()),
GetPage(name: cubitSpanTwo, page: () => CubitSpanTwoPage()),
GetPage(name: getCounterRx, page: () => GetCounterRxPage()),
GetPage(name: getCounterEasy, page: () => GetCounterEasyPage()),
GetPage(name: getCounterHigh, page: () => GetCounterHighPage()),
GetPage(name: getJumpOne, page: () => GetJumpOnePage()),
GetPage(name: getJumpTwo, page: () => GetJumpTwoPage()),
GetPage(
name: getCounterBinding,
page: () => GetCounterBindingPage(),
binding: GetCounterBinding(),
),
GetPage(name: counterEasyXBuilderPage, page: () => EasyXCounterPage()),
GetPage(name: counterEasyXEbxPage, page: () => EasyXEbxCounterPage()),
GetPage(name: himalaya, page: () => HimalayaPage()),
GetPage(name: proEasyCounterPage, page: () => ProEasyCounterPage()),
GetPage(name: proHighCounterPage, page: () => ProHighCounterPage()),
GetPage(name: proSpanOnePage, page: () => ProSpanOnePage()),
GetPage(name: proSpanTwoPage, page: () => ProSpanTwoPage()),
GetPage(name: testNotifierPage, page: () => TestNotifierPage()),
GetPage(name: customBuilderPage, page: () => CustomBuilderPage()),
GetPage(name: counterEasyPPage, page: () => CounterEasyPPage()),
GetPage(name: counterGlobalEasyPPage, page: () => CounterGlobalEasyPPage()),
];
}
路由API
請注意命名路由,只需要在api結尾加上Named
即可,舉例:
- 預設:Get.to(SomePage());
- 命名路由:Get.toNamed(“/somePage”);
詳細Api介紹,下面內容來自GetX的README文件,進行了相關整理
- 導航到新的頁面
Get.to(NextScreen());
Get.toNamed("/NextScreen");
- 關閉SnackBars、Dialogs、BottomSheets或任何你通常會用Navigator.pop(context)關閉的東西
Get.back();
- 進入下一個頁面,但沒有返回上一個頁面的選項(用於SplashScreens,登入頁面等)
Get.off(NextScreen());
Get.offNamed("/NextScreen");
- 進入下一個介面並取消之前的所有路由(在購物車、投票和測試中很有用)
Get.offAll(NextScreen());
Get.offAllNamed("/NextScreen");
- 傳送資料到其它頁面
只要傳送你想要的引數即可。Get在這裡接受任何東西,無論是一個字串,一個Map,一個List,甚至一個類的例項。
Get.to(NextScreen(), arguments: 'Get is the best');
Get.toNamed("/NextScreen", arguments: 'Get is the best');
在你的類或控制器上:
print(Get.arguments);
//print out: Get is the best
- 要導航到下一條路由,並在返回後立即接收或更新資料
var data = await Get.to(Payment());
var data = await Get.toNamed("/payment");
- 在另一個頁面上,傳送前一個路由的資料
Get.back(result: 'success');
// 並使用它,例:
if(data == 'success') madeAnything();
- 如果你不想使用GetX語法,只要把 Navigator(大寫)改成 navigator(小寫),你就可以擁有標準導航的所有功能,而不需要使用context,例如:
// 預設的Flutter導航
Navigator.of(context).push(
context,
MaterialPageRoute(
builder: (BuildContext context) {
return HomePage();
},
),
);
// 使用Flutter語法獲得,而不需要context。
navigator.push(
MaterialPageRoute(
builder: (_) {
return HomePage();
},
),
);
// get語法
Get.to(HomePage());
動態網頁連結
- 這是一個非常重要的功能,在web端,可以
保證透過url傳引數到頁面
裡
Get提供高階動態URL,就像在Web上一樣。Web開發者可能已經在Flutter上想要這個功能了,Get也解決了這個問題。
Get.offAllNamed("/NextScreen?device=phone&id=354&name=Enzo");
在你的controller/bloc/stateful/stateless類上:
print(Get.parameters['id']);
// out: 354
print(Get.parameters['name']);
// out: Enzo
你也可以用Get輕鬆接收NamedParameters。
void main() {
runApp(
GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(
name: '/',
page: () => MyHomePage(),
),
GetPage(
name: '/profile/',
page: () => MyProfile(),
),
//你可以為有引數的路由定義一個不同的頁面,也可以為沒有引數的路由定義一個不同的頁面,但是你必須在不接收引數的路由上使用斜槓"/",就像上面說的那樣。
GetPage(
name: '/profile/:user',
page: () => UserProfile(),
),
GetPage(
name: '/third',
page: () => Third(),
transition: Transition.cupertino
),
],
)
);
}
傳送命名路由資料
Get.toNamed("/profile/34954");
在第二個頁面上,透過引數獲取資料
print(Get.parameters['user']);
// out: 34954
現在,你需要做的就是使用Get.toNamed()來導航你的命名路由,不需要任何context(你可以直接從你的BLoC或Controller類中呼叫你的路由),當你的應用程式被編譯到web時,你的路由將出現在URL中。
資源釋放
關於GetxController的資源釋放,這個欄目的內容相當重要!
資源未釋放的場景
在我們使用GetX的時候,可能沒什麼GetxController未被釋放的感覺,這種情況,是因為我們一般都是用了getx的那一套路由跳轉api(Get.to、Get.toName...)之類:使用Get.toName,肯定需要使用GetPage;如果使用Get.to,是不需要在GetPage中註冊的,Get.to的內部有一個新增到GetPageRoute的操作
透過上面會在GetPage註冊可知,說明在我們跳轉頁面的時候,GetX會拿你到頁面資訊儲存起來,加以管理,下面倆種場景會導致GetxController無法釋放
GetxController可被自動釋放的條件
- GetPage+Get.toName配套使用,可釋放
- 直接使用Get.to,可釋放
GetxController無法被自動釋放場景
- 未使用GetX提供的路由跳轉:直接使用原生路由api的跳轉操作
- 這樣會直接導致GetX無法感知對應頁面GetxController的生命週期,會導致其無法釋放
Navigator.push(
context,
MaterialPageRoute(builder: (context) => XxxxPage()),
);
由此,可從上面可以看出,GetxController無法被釋放的場景:不使用GetX路由
最優解
這裡有個最優解方案,就算你不使用Getx路由,也能很輕鬆回收各個頁面的GetXController,感謝 @法的空間 在評論裡指出
- 手動讓getx感知路由
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage,
///此處配置下!
navigatorObservers: [GetXRouterObserver()],
);
}
}
///自定義這個關鍵類!!!!!!
class GetXRouterObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
RouterReportManager.reportCurrentRoute(route);
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) async {
RouterReportManager.reportRouteDispose(route);
}
}
講真的,這個原理其實很簡單,但是思路很有趣;大家點進reportCurrentRoute
和 reportRouteDispose
這倆個方法,大概就知道是怎麼回事了
reportCurrentRoute
就是讓當前的路由標定給GetX- 當我們進入一個頁面的時候,相應GetXController會進行初始化,最終會呼叫
_startController<S>({String? tag})
方法 _startController
中會呼叫RouterReportManager.appendRouteByCreate(i)
,將注入的GetXController都儲存起來- 儲存在一個map中,key為當前路由
route
,value為HashSet,可以儲存多個GetXController - ok,路由關閉的時候,在
reportRouteDispose
方法中回收,key為當前route
,遍歷value中所有的GetXController回收 - 我giao,基於這種思路,大家能幹很多事了!!!
折中方案
如果上面的最優解沒法幫你解決GetXController的回收問題,你可能就遇到特殊的場景了,一般來說,分析分析你自己的程式碼,基本都能分析出來
如果懶得分析原因,就試試下面這種折中方案吧;顆粒度極小,針對單頁面維度解決
StatefulWidget方案
這邊我模擬了上面場景,寫了一個解決方案
- 第一個頁面跳轉
Navigator.push(
Get.context,
MaterialPageRoute(builder: (context) => AutoDisposePage()),
);
演示頁面
- 這地方地方必須要使用StatefulWidget,因為在這種情況,無法感知生命週期,就需要使用StatefulWidget生命週期
- 在dispose回撥處,把當前GetxController從整個GetxController管理鏈中刪除即可
class AutoDisposePage extends StatefulWidget {
@override
_AutoDisposePageState createState() => _AutoDisposePageState();
}
class _AutoDisposePageState extends State<AutoDisposePage> {
final AutoDisposeLogic logic = Get.put(AutoDisposeLogic());
@override
Widget build(BuildContext context) {
return BaseScaffold(
appBar: AppBar(title: const Text('計數器-自動釋放')),
body: Center(
child: Obx(
() => Text('點選了 ${logic.count.value} 次',
style: TextStyle(fontSize: 30.0)),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => logic.increase(),
child: const Icon(Icons.add),
),
);
}
@override
void dispose() {
Get.delete<AutoDisposeLogic>();
super.dispose();
}
}
class AutoDisposeLogic extends GetxController {
var count = 0.obs;
///自增
void increase() => ++count;
}
看到這,你可能會想,啊這!怎麼這麼麻煩,我怎麼還要寫StatefulWidget,好麻煩!
各位放心,這個問題,我也想到了,我特地在外掛裡面加上了自動回收的功能
如果你寫的頁面無法被回收,記得勾選autoDispose
- 怎麼判斷頁面的GetxController是否能被回收呢?實際上很簡單,上面的未被釋放的場景已經描述的比較清楚了,不清楚的話,就再看看
來看下程式碼,default模式一樣可以的
- view
class AutoDisposePage extends StatefulWidget {
@override
_AutoDisposePageState createState() => _AutoDisposePageState();
}
class _AutoDisposePageState extends State<AutoDisposePage> {
final AutoDisposeLogic logic = Get.put(AutoDisposeLogic());
@override
Widget build(BuildContext context) {
return Container();
}
@override
void dispose() {
Get.delete<AutoDisposeLogic>();
super.dispose();
}
}
- logic
class AutoDisposeLogic extends GetxController {
}
最佳化StatefulWidget方案
上面的是個通用解決方法,你不需要額外的引入任何其它的東西;但是這種方案用到了StatefulWidget,程式碼多了一大坨,讓我有點膈應
鄙人有著相當的強迫症,想了很久,從外部入手,我就寫了一個通用控制元件,來對相應的GetXController進行回收
GetBindWidget
- 本控制元件含義:將GetXController和當前頁面的生命週期繫結,頁面關閉時,自動回收
- 該控制元件可以回收單個GetXController(bind引數),可以加上對應tag(tag引數);也可以回收多個GetXController(binds),可以加上多個tag(tags引數,請和binds 一 一 對應;無tag的GetXController的,tag可以寫成空字元:"")
import 'package:flutter/material.dart';
import 'package:get/get.dart';
/// GetBindWidget can bind GetxController, and when the page is disposed,
/// it can automatically destroy the bound related GetXController
///
///
/// Sample:
///
/// class SampleController extends GetxController {
/// final String title = 'My Awesome View';
/// }
///
/// class SamplePage extends StatelessWidget {
/// final controller = Get.put(SampleController());
///
/// @override
/// Widget build(BuildContext context) {
/// return GetBindWidget(
/// bind: controller,
/// child: Container(),
/// );
/// }
/// }
class GetBindWidget extends StatefulWidget {
const GetBindWidget({
Key? key,
this.bind,
this.tag,
this.binds,
this.tags,
required this.child,
}) : assert(
binds == null || tags == null || binds.length == tags.length,
'The binds and tags arrays length should be equal\n'
'and the elements in the two arrays correspond one-to-one',
),
super(key: key);
final GetxController? bind;
final String? tag;
final List<GetxController>? binds;
final List<String>? tags;
final Widget child;
@override
_GetBindWidgetState createState() => _GetBindWidgetState();
}
class _GetBindWidgetState extends State<GetBindWidget> {
@override
Widget build(BuildContext context) {
return widget.child;
}
@override
void dispose() {
_closeGetXController();
_closeGetXControllers();
super.dispose();
}
///Close GetxController bound to the current page
void _closeGetXController() {
if (widget.bind == null) {
return;
}
var key = widget.bind.runtimeType.toString() + (widget.tag ?? '');
GetInstance().delete(key: key);
}
///Batch close GetxController bound to the current page
void _closeGetXControllers() {
if (widget.binds == null) {
return;
}
for (var i = 0; i < widget.binds!.length; i++) {
var type = widget.binds![i].runtimeType.toString();
if (widget.tags == null) {
GetInstance().delete(key: type);
} else {
var key = type + (widget.tags?[i] ?? '');
GetInstance().delete(key: key);
}
}
}
}
- 使用非常的簡單
/// 回收單個GetXController
class TestPage extends StatelessWidget {
final logic = Get.put(TestLogic());
@override
Widget build(BuildContext context) {
return GetBindWidget(
bind: logic,
child: Container(),
);
}
}
/// 回收多個GetXController
class TestPage extends StatelessWidget {
final logicOne = Get.put(TestLogic(), tag: 'one');
final logicTwo = Get.put(TestLogic());
final logicThree = Get.put(TestLogic(), tag: 'three');
@override
Widget build(BuildContext context) {
return GetBindWidget(
binds: [logicOne, logicTwo, logicThree],
tags: ['one', '', 'three'],
child: Container(),
);
}
}
/// 回收日誌
[GETX] Instance "TestLogic" has been created with tag "one"
[GETX] Instance "TestLogic" with tag "one" has been initialized
[GETX] Instance "TestLogic" has been created
[GETX] Instance "TestLogic" has been initialized
[GETX] Instance "TestLogic" has been created with tag "three"
[GETX] Instance "TestLogic" with tag "three" has been initialized
[GETX] "TestLogicone" onDelete() called
[GETX] "TestLogicone" deleted from memory
[GETX] "TestLogic" onDelete() called
[GETX] "TestLogic" deleted from memory
[GETX] "TestLogicthree" onDelete() called
[GETX] "TestLogicthree" deleted from memory
一些問題彙總
如果使用中,有比較坑的問題,希望大家在評論裡提出來,我會在這個欄目彙總一下
- 無法跳轉重複頁面
- 另一種表現形式:使用Get.to(Get.toName)在系統Dialog上跳轉頁面,未關閉Dialog;返回,再跳轉,會出現無法跳轉的情況
debug了下to方法內部的執行,發現他用了一個preventDuplicates引數,限制跳轉重複頁面
為什麼這樣做?
- 優點:能解決多次點選跳轉按鈕,跳轉多個重複頁面的問題
- 缺點:限制了複雜業務跳轉重複頁面的場景
當然上面的缺點也不算是缺點,畢竟已經給了引數可以控制
- 跳轉重複頁面,可以這樣寫
Get.to(XxxxPage(), preventDuplicates: false);
// 或者
Get.toNamed('xxx', preventDuplicates: false);
- 使用PageView時,所有PageView頁面控制器,全被初始化問題
大家使用PageView,新增PageView頁面,PageView頁面用GetX構成,會發現所有的PageView頁面控制器全被初始化了!並不是切換到某個頁面時,對應頁面的控制器才被初始化!
PageView切換到某個頁面的時候,才會呼叫對應Page頁面的build方法;對於PageView頁面,控制器的注入過程,不能寫在類中了,需要將其移入到build方法中初始化。
- 正常頁面,注入寫法(非PageView頁面)
class CounterEasyGetPage extends StatelessWidget {
final CounterEasyGetLogic logic = Get.put(CounterEasyGetLogic());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('計數器-簡單式')),
body: Center(
child: GetBuilder<CounterEasyGetLogic>(
builder: (logicGet) => Text(
'點選了 ${logicGet.count} 次',
style: TextStyle(fontSize: 30.0),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => logic.increase(),
child: const Icon(Icons.add),
),
);
}
}
- PageView頁面,初始化位置必須調整
class CounterEasyGetPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final CounterEasyGetLogic logic = Get.put(CounterEasyGetLogic());
return Scaffold(
appBar: AppBar(title: const Text('計數器-簡單式')),
body: Center(
child: GetBuilder<CounterEasyGetLogic>(
builder: (logicGet) => Text(
'點選了 ${logicGet.count} 次',
style: TextStyle(fontSize: 30.0),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => logic.increase(),
child: const Icon(Icons.add),
),
);
}
}
- 大家如果覺得手動移太麻煩的話,也可以選中外掛的 isPageView 功能
最後
相關地址
- 文中DEMO地址:flutter_use
- GetX外掛地址:getx_template
Windows:Windows平臺安裝包
- 密碼:xdd666
系列文章
引流了,手動滑稽.png
- IDEA外掛:GetX程式碼生成IDEA外掛,超詳細功能講解(透過現象看本質)
- GetX原理:Flutter GetX深度剖析 | 我們終將走出自己的路(萬字圖文)
- 告別克蘇魯程式碼山:Flutter 改善套娃地獄問題(仿喜馬拉雅PC頁面舉例)
- 讓Dialog擁有更多可能:這一次,解決Flutter Dialog的各種痛點!