說狀態(tài)管理到底在說些什么
一個應用的狀態(tài)就是當這個應用運行時存在于內存中的所有內容卒稳。當然許多狀態(tài),例如紋理、動畫狀態(tài)等键菱,框架本身會替開發(fā)者管理,所以對于狀態(tài)更合適的定義是“當你需要重建用戶界面時所需要的數(shù)據(jù)”今布,我們需要自己管理的狀態(tài)可以分為兩種概念類型:短時 (ephemeral) 狀態(tài)和應用 (app) 狀態(tài)经备。
短時狀態(tài)
短時狀態(tài)是可以完全包含在一個獨立 widget 中的狀態(tài),也成為局部狀態(tài)部默。
- 一個 PageView 組件中的當前頁面
- 一個復雜動畫中當前進度
- 一個 BottomNavigationBar 中當前被選中的 tab
- 一個文本框顯示的內容
應用狀態(tài)
如果在應用中的多個部分之間共享一個非短時的狀態(tài)侵蒙,并且在用戶會話期間保留這個狀態(tài),我們稱之為應用狀態(tài)(有時也稱共享狀態(tài))傅蹂。
- 用戶選項
- 登錄信息
- 一個社交應用中的通知
- 一個電商應用中的購物車
- 一個新聞應用中的文章已讀/未讀狀態(tài)
為什么選擇 GetX 做狀態(tài)管理纷闺?
開發(fā)者一直致力于業(yè)務邏輯分離的概念,F(xiàn)lutter 也有利用 BLoc 份蝴、Provider 衍生的 MVC犁功、MVVM 等架構模式,但是這幾種方案的狀態(tài)管理均使用了上下文(context)搞乏,需要上下文來尋找InheritedWidget
波桩,這種解決方案限制了狀態(tài)管理必須在父子代的 widget 樹中,業(yè)務邏輯也會對 View 產(chǎn)生較強依賴请敦。
而 GetX 因為不需要上下文镐躲,突破了InheritedWidget
的限制,我們可以在全局和模塊間共享狀態(tài)侍筛,這正是 BLoc 萤皂、Provider 等框架的短板。
另外 GetX 控制器也是有生命周期的匣椰,例如當我們需要業(yè)務層進行 APIREST 時裆熙,我們可以不依賴于界面中的任何東西。可以使用onInit
來啟動http調用入录,當數(shù)據(jù)到達賦值給變量后蛤奥,利用 GetX 響應式的特性,使用該變量的 Widgets 將在界面中自動更新僚稿。這樣在 UI層只需要寫界面凡桥,除了用戶事件(比如點擊按鈕)之外,不需要向業(yè)務邏輯層發(fā)送任何東西蚀同。
簡單使用
對于以前使用過 ChangeNotifier
的同學來說缅刽,可以把GetxController
當做ChangeNotifier
,我們使用計數(shù)器示例來演示一下基本使用:
class SimpleController extends GetxController {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
update();
}
}
這是一個控制器蠢络,有 UI 需要的數(shù)據(jù)counter
和用戶點擊一次加1的方法衰猛。
在 UI 層一個展示的文本和一個按鈕:
class SimplePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('SimplePage--build');
return GetBuilder<SimpleController>(
init: SimpleController(),
builder: (controller) {
return Scaffold(
appBar: AppBar(title: Text('Simple')),
body: Center(
child: Text(controller.counter.toString()),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
controller.increment();
},
child: Icon(Icons.add),
),
);
});
}
}
使用了GetBuilder
這個 Widget 包裹了頁面,在 init
初始化SimpleController
,然后每次點擊刹孔,都會更新builder
對應的 Widget 啡省,GetxController
通過update()
更新GetBuilder
。
這看起來和別狀態(tài)管理框架并無不同芦疏,有時我們只想重新 build 需要變化的部分冕杠,遵循最小原則,那么我們改下GetBuilder
的位置酸茴,只包裹 Text
:
class SimplePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('SimplePage--build');
return Scaffold(
appBar: AppBar(title: Text('Simple')),
body: Center(
child: GetBuilder<SimpleController>(
init: SimpleController(),
builder: (controller) {
return Text(controller.counter.toString());
}),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
controller.increment();
},
child: Icon(Icons.add),
),
);
}
}
因為controlle
作用域問題,此時按鈕里面的 controller
會找不到兢交,GetX強大的一點的就表現(xiàn)出來了薪捍,按鈕和文本并不在父子組件,并且和GetBuilder
不在一個作用域配喳,但是我們依然能正確得到:
onPressed: () {
Get.find<SimpleController>().increment();
// controller..increment();
},
GetxController
也有生命周期的:
class SimpleController extends GetxController {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
update();
}
@override
void onInit() {
super.onInit();
print('SimpleController--onInit');
}
@override
void onReady() {
super.onReady();
print('SimpleController--onReady');
}
@override
void onClose() {
super.onClose();
print('SimpleController--onClose');
}
}
之前在這里打印了一句:
class SimplePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('SimplePage--build');
return Scaffold(
酪穿。。晴裹。
再次打開這個頁面被济,控制臺輸出:
flutter: SimplePage--build
flutter: SimpleController--onInit
[GETX] "SimpleController" has been initialized
flutter: SimpleController--onReady
SimplePage-build->SimpleController-onInit->SimpleController-onReady
推出當前頁面返回:
[GETX] CLOSE TO ROUTE /SimplePage
flutter: SimpleController--onClose
[GETX] "SimpleController" onClose() called
[GETX] "SimpleController" deleted from memory
[GETX] Instance "SimpleController" already removed.
可以看到SimpleController
已經(jīng)被刪除。
局部更新
多種狀態(tài)可以分別更新涧团,不需要為每個狀態(tài)創(chuàng)建一個類只磷。
再添加一個變量:
int _counter = 0;
int get counter => _counter;
String _name = "Lili";
String get firstName => _name;
void increment() {
_counter++;
_name = WordPair.random().asPascalCase;
update(['counter']);
}
void changeName() {
_counter++;
_name = WordPair.random().asPascalCase;
update(['name']);
}
兩個方法分別改變兩個變量,但是注意update(['counter']
里添加了 id 數(shù)組泌绣,這樣就只更新這個 id 對應的GetBuilder
:
GetBuilder<SimpleAdvancedController>(
id: 'counter',
builder: (ctl) => Text(ctl.counter.toString()),
),
SizedBox(
height: 50,
),
GetBuilder<SimpleAdvancedController>(
id: 'name',
builder: (ctl) => Text(ctl.firstName),
),
響應式刷新
我們都用過 StreamControllers 钮追,然后以流的方式發(fā)送數(shù)據(jù)。在 GetX 可以實現(xiàn)同樣的功能阿迈,并且實現(xiàn)起來只有幾個單詞,不需要為每個觀察的對象創(chuàng)建一個 StreamController 元媚,也不需要創(chuàng)建 StreamBuilder。
var name = '新垣結衣';
下面簡單的一個后綴就可以把一個變量變得可觀察,變量每次改變的時候刊棕,使用它的小部件就會被更新:
var name = '新垣結衣'.obs;
就這么簡單炭晒,這個變量已經(jīng)是響應式的了。然后通過 Obx 或者 GetX 包裹并使用響應式變量的控件甥角,在變量改變的時候就會被更新:
Obx (() => Text (controller.name));
下面寫個計算器的例子:
final count1 = 0.obs;
final count2 = 0.obs;
.obs
就實現(xiàn)了一個被觀察者网严,他們不再是 int 類型,而是 RxInt 類型蜈膨。對應的小部件也不再是GetBuilder
了屿笼,而是下面兩種:
GetX<SumController>(
builder: (_) {
print("count1 rebuild");
return Text(
'${_.count1}',
style: TextStyle(fontWeight: FontWeight.bold),
);
},
),
Obx(() => Text(
'${Get.find<SumController>().count2}',
style: TextStyle(fontWeight: FontWeight.bold),
)),
因為是響應式,不再需要update
,每次更改值翁巍,都自動刷新驴一。但是更神奇的是,他們的運算和也是響應式的:
int get sum => count1.value + count2.value;
只要更新count1
或者count2
使用sum
的小部件也會更改:
Obx(() => Text(
'${Get.find<SumController>().sum}',
style: TextStyle(fontWeight: FontWeight.bold),
)),
非常簡單的使用方式灶壶,不是嗎肝断?除了使用.obs
還有2種方法把變量變成可觀察的:
- 第一種是使用 Rx{Type}。
// 建議使用初始值驰凛,但不是強制性的
final name = RxString('');
final isLogged = RxBool(false);
final count = RxInt(0);
final balance = RxDouble(0.0);
final items = RxList<String>([]);
final myMap = RxMap<String, int>({});
- 第二種是使用 Rx胸懈,規(guī)定泛型 Rx<Type>。
final name = Rx<String>('');
final isLogged = Rx<Bool>(false);
final count = Rx<Int>(0);
final balance = Rx<Double>(0.0);
final number = Rx<Num>(0)
final items = Rx<List<String>>([]);
final myMap = Rx<Map<String, int>>({});
// 自定義類 - 可以是任何類
final user = Rx<User>();
將一個對象轉變成可觀察的恰响,也有2種方法:
- 可以將我們的類值轉換為 obs
class RxUser {
final name = "Camila".obs;
final age = 18.obs;
}
- 或者可以將整個類轉換為一個可觀察的類趣钱。
class User {
User({String name, int age});
var name;
var age;
}
//實例化時。
final user = User(name: "Camila", age: 18).obs;
注意胚宦,轉化為可觀察的變量后首有,它的類型不再是原生類型,所以取值不能用變量本身枢劝,而是.value
當然 GetX 也提供了 api 簡化對 int井联、List 的操作。此外您旁,Get還提供了精細的狀態(tài)控制烙常。我們可以根據(jù)特定的條件對一個事件進行條件控制(比如將一個對象添加到List中):
// 第一個參數(shù):條件,必須返回true或false鹤盒。
// 第二個參數(shù):如果條件為真蚕脏,則為新的值。
list.addIf(item < limit, item);
響應式編程雖好昨悼,可不要貪杯蝗锥。因為響應式對 RAM 的消耗比較大,因為他們的實現(xiàn)都是流率触,如果創(chuàng)建一個有80個對象的 List 终议,每個對象都有幾個流,打開dart inspect,查看一個 StreamBuilder 的消耗量穴张,我們就會明白這不是一個好的方法细燎。而 GetBuilder 在 RAM 中是非常高效的,幾乎沒有比他更高效的方法皂甘。所以這些使用方式在使用過程中要斟酌玻驻。
Workers
響應式不只這些好處,還有一個 Workers 偿枕,將協(xié)助我們在事件發(fā)生時觸發(fā)特定的回調璧瞬,也就是 RxJava 的一些操作符;
@override
onInit() {
super.onInit();
/// 每次更改都會回調
ever(count1, (_) => print("$_ has been changed"));
/// 第一次更改回調
once(count1, (_) => print("$_ was changed once"));
/// 更改后3秒回調
debounce(count1, (_) => print("debouce$_"), time: Duration(seconds: 3));
///3秒內更新回調一次
interval(count1, (_) => print("interval $_"), time: Duration(seconds: 3));
}
我們可以利用 Workers ,去實現(xiàn)寫一堆對代碼才能實現(xiàn)的功能。比如防抖函數(shù)仿村,在搜索的時候使用酱床,節(jié)流函數(shù)需五,在點擊事件的時候使用。
跨路由
上面演示過在同一個頁面兄弟組件跨組件使用,接下來實現(xiàn)下不同頁面跨組件使用,首先在CrossOnePage
里 put
一個 Controller:
class CrossOnePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
CrossOneController controller = Get.put(CrossOneController());
...
}}
然后在另一個頁面CrossTwoPage
访诱,打印下上一個頁面put
的控制器:
CheetahButton('打印CrossOneController的age', () {
print(Get.find<CrossOneController>().age);
}),
正常輸出。
那么CrossOneController
的生命周期多久呢韩肝?如果像第一個頁面一樣是在build
里 put 的触菜,那么當前頁面退出就銷毀了。如果是成員變量哀峻,那么當前頁面的引用銷毀才會銷毀:
class CrossTwoPage extends StatelessWidget {
final CrossTwoSecondController controller = Get.put(CrossTwoSecondController());
@override
Widget build(BuildContext context) {
Get.put(CrossTwoController());
return Scaffold(
appBar: AppBar(title: Text('CrossTwoPage')),
body: Container(
child: Column(
children: [
CheetahButton('打印CrossTwoController', () {
print(Get.find<CrossTwoController>());
}),
CheetahButton('CrossTwoSecondController', () {
print(Get.find<CrossTwoSecondController>());
}),
CheetahButton('打印CrossOneController的age', () {
print(Get.find<CrossOneController>().age);
}),
],
)),
);
}
}
CrossTwoSecondController
是成員變量玫氢,CrossTwoController
是在build
的時候 put 進去的,現(xiàn)在打印2個控制器谜诫,都能打印出來:
[GETX] "CrossTwoSecondController" has been initialized
[GETX] GOING TO ROUTE /CrossTwoPage
[GETX] "CrossTwoController" has been initialized
I/flutter (16952): Instance of 'CrossTwoController'
I/flutter (16952): Instance of 'CrossTwoSecondController'
現(xiàn)在返回第一個頁面,GetX 已經(jīng)給我們打印了:
GETX] CLOSE TO ROUTE /CrossTwoPage
[GETX] "CrossTwoController" onClose() called
[GETX] "CrossTwoController" deleted from memory
然后我們在第一個頁面點擊按鈕攻旦,分別打印頁面CrossTwoPage
的2個控制器:
════════ Exception caught by gesture ═══════════════════════════════════════════
"CrossTwoController" not found. You need to call "Get.put(CrossTwoController())" or "Get.lazyPut(()=>CrossTwoController())"
════════════════════════════════════════════════════════════════════════════════
I/flutter (16952): Instance of 'CrossTwoSecondController'
在build
里 put
的控制器已經(jīng)銷毀為 null 了喻旷,另一個依然存在,那是不是這種不會銷毀呢牢屋?因為第一個頁面的路由依然持有第二個頁面且预,第二個頁面的實例還在內存中,所以控制器作為成員變量依然存在烙无,退出第一個頁面锋谐,自然就銷毀了:
[GETX] CLOSE TO ROUTE /CrossOnePage
[GETX] "CrossOneController" onClose() called
[GETX] "CrossOneController" deleted from memory
[GETX] "CrossTwoSecondController" onClose() called
[GETX] "CrossTwoSecondController" deleted from memory
不使用 GetX 路由的狀態(tài)管理
GetX雖然各個功能均可單獨引用使用,但是狀態(tài)管理和路由是搭配的截酷,如果沒有使用 route_manager
組件涮拗,那么狀態(tài)管理的生命周期就會失效。put
的Controller
在不使用的時候不會再被刪除,而變成了應用狀態(tài)常駐內存里三热。
如果項目的路由暫時不能使用 GetX 替換鼓择,那么怎么使用狀態(tài)管理呢,很簡單就漾,封裝一個自動刪除Controller
的控件即可呐能,因為習慣使用GetBinding
,待可以替換為 GetX 路由的時候直接帶上GetBinding
抑堡,所以封裝了一個GetBinding
的控件和一個不使用GetBinding
的控件:
abstract class GetBindingView<T extends GetxController>
extends StatefulWidget {
final String? tag = null;
T get controller => GetInstance().find<T>(tag: tag);
@protected
Widget build(BuildContext context);
@protected
Bindings? binding();
@override
_AutoDisposeState createState() => _AutoDisposeState<T>();
}
class _AutoDisposeState<S extends GetxController>
extends State<GetBindingView> {
_AutoDisposeState();
@override
Widget build(BuildContext context) {
return widget.build(context);
}
@override
void initState() {
super.initState();
widget.binding()?.dependencies();
}
@override
void dispose() {
Get.delete<S>();
super.dispose();
}
}
使用很簡單:
- 創(chuàng)建對應的
GetBinding
摆出、GetxController
和 Page , - 對應的 Page 修改為繼承
GetDisposeView
首妖, - 實現(xiàn)
binding()
方法并返回第一步創(chuàng)建的GetBinding
偎漫。
class BingPagePage extends GetBindingView<BingPageController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('BingPage Page')),
body: Container(
child: Obx(()=>Container(child: Text(controller.obj),)),
),
);
}
@override
Bindings? binding() =>BingPageBinding();
}
接下來就可以像使用 GetView
一樣使用了,如果以后替換了 GetX 路由悯搔,只需要把 GetDisposeView
替換為GetView
骑丸。
下面是一個不使用GetBinding
的控件,比上面的使用更簡單妒貌,不需要創(chuàng)建GetBinding
:
abstract class GetDisposeView<T extends GetxController> extends StatefulWidget {
final String? tag = null;
T get controller => GetInstance().find<T>(tag: tag);
@protected
Widget build(BuildContext context);
@protected
void setController();
@override
_AutoDisposeState createState() => _AutoDisposeState<T>();
}
class _AutoDisposeState<S extends GetxController>
extends State<GetDisposeView> {
_AutoDisposeState();
@override
Widget build(BuildContext context) {
return widget.build(context);
}
@override
void initState() {
super.initState();
widget.setController();
}
@override
void dispose() {
Get.delete<S>();
super.dispose();
}
}
使用:
創(chuàng)建對應的
GetxController
和 Page 通危,對應的 Page 修改為繼承
GetDisposeView
,-
實現(xiàn)
setController()
方法并返回第一步創(chuàng)建的put
第一步創(chuàng)建的GetxController
對象灌曙。class AutoDisposePage extends GetDisposeView<BingPageController> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Auto Dispose Page')), body: Container( child: Obx(()=>Container(child: Text(controller.obj),)), ), ); } @override void setController() { Get.put(BingPageController()); } }