說狀態(tài)管理到底在說些什么
一個(gè)應(yīng)用的狀態(tài)就是當(dāng)這個(gè)應(yīng)用運(yùn)行時(shí)存在于內(nèi)存中的所有內(nèi)容。當(dāng)然許多狀態(tài),例如紋理、動(dòng)畫狀態(tài)等,框架本身會(huì)替開發(fā)者管理,所以對于狀態(tài)更合適的定義是“當(dāng)你需要重建用戶界面時(shí)所需要的數(shù)據(jù)”,我們需要自己管理的狀態(tài)可以分為兩種概念類型:短時(shí) (ephemeral) 狀態(tài)和應(yīng)用 (app) 狀態(tài)。
短時(shí)狀態(tài)
短時(shí)狀態(tài)是可以完全包含在一個(gè)獨(dú)立 widget 中的狀態(tài),也成為局部狀態(tài)。
- 一個(gè) PageView 組件中的當(dāng)前頁面
- 一個(gè)復(fù)雜動(dòng)畫中當(dāng)前進(jìn)度
- 一個(gè) BottomNavigationBar 中當(dāng)前被選中的 tab
- 一個(gè)文本框顯示的內(nèi)容
應(yīng)用狀態(tài)
如果在應(yīng)用中的多個(gè)部分之間共享一個(gè)非短時(shí)的狀態(tài),并且在用戶會(huì)話期間保留這個(gè)狀態(tài),我們稱之為應(yīng)用狀態(tài)(有時(shí)也稱共享狀態(tài))。
- 用戶選項(xiàng)
- 登錄信息
- 一個(gè)社交應(yīng)用中的通知
- 一個(gè)電商應(yīng)用中的購物車
- 一個(gè)新聞應(yīng)用中的文章已讀/未讀狀態(tài)
為什么選擇 GetX 做狀態(tài)管理?
開發(fā)者一直致力于業(yè)務(wù)邏輯分離的概念,F(xiàn)lutter 也有利用 BLoc 、Provider 衍生的 MVC、MVVM 等架構(gòu)模式,但是這幾種方案的狀態(tài)管理均使用了上下文(context),需要上下文來尋找InheritedWidget,這種解決方案限制了狀態(tài)管理必須在父子代的 widget 樹中,業(yè)務(wù)邏輯也會(huì)對 View 產(chǎn)生較強(qiáng)依賴。
而 GetX 因?yàn)椴恍枰舷挛?,突破?code>InheritedWidget的限制,我們可以在全局和模塊間共享狀態(tài),這正是 BLoc 、Provider 等框架的短板。
另外 GetX 控制器也是有生命周期的,例如當(dāng)我們需要業(yè)務(wù)層進(jìn)行 APIREST 時(shí),我們可以不依賴于界面中的任何東西??梢允褂?code>onInit來啟動(dòng)http調(diào)用,當(dāng)數(shù)據(jù)到達(dá)賦值給變量后,利用 GetX 響應(yīng)式的特性,使用該變量的 Widgets 將在界面中自動(dòng)更新。這樣在 UI層只需要寫界面,除了用戶事件(比如點(diǎn)擊按鈕)之外,不需要向業(yè)務(wù)邏輯層發(fā)送任何東西。
簡單使用
對于以前使用過 ChangeNotifier 的同學(xué)來說,可以把GetxController當(dāng)做ChangeNotifier,我們使用計(jì)數(shù)器示例來演示一下基本使用:
class SimpleController extends GetxController {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
update();
}
}
這是一個(gè)控制器,有 UI 需要的數(shù)據(jù)counter和用戶點(diǎn)擊一次加1的方法。
在 UI 層一個(gè)展示的文本和一個(gè)按鈕:
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這個(gè) Widget 包裹了頁面,在 init初始化SimpleController,然后每次點(diǎn)擊,都會(huì)更新builder對應(yīng)的 Widget ,GetxController通過update()更新GetBuilder。
這看起來和別狀態(tài)管理框架并無不同,有時(shí)我們只想重新 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),
),
);
}
}
因?yàn)?code>controlle作用域問題,此時(shí)按鈕里面的 controller會(huì)找不到,GetX強(qiáng)大的一點(diǎn)的就表現(xiàn)出來了,按鈕和文本并不在父子組件,并且和GetBuilder不在一個(gè)作用域,但是我們依然能正確得到:
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(
。。。
再次打開這個(gè)頁面,控制臺(tái)輸出:
flutter: SimplePage--build
flutter: SimpleController--onInit
[GETX] "SimpleController" has been initialized
flutter: SimpleController--onReady
SimplePage-build->SimpleController-onInit->SimpleController-onReady
推出當(dāng)前頁面返回:
[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)可以分別更新,不需要為每個(gè)狀態(tài)創(chuàng)建一個(gè)類。
再添加一個(gè)變量:
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']);
}
兩個(gè)方法分別改變兩個(gè)變量,但是注意update(['counter']里添加了 id 數(shù)組,這樣就只更新這個(gè) id 對應(yīng)的GetBuilder:
GetBuilder<SimpleAdvancedController>(
id: 'counter',
builder: (ctl) => Text(ctl.counter.toString()),
),
SizedBox(
height: 50,
),
GetBuilder<SimpleAdvancedController>(
id: 'name',
builder: (ctl) => Text(ctl.firstName),
),
響應(yīng)式刷新
我們都用過 StreamControllers ,然后以流的方式發(fā)送數(shù)據(jù)。在 GetX 可以實(shí)現(xiàn)同樣的功能,并且實(shí)現(xiàn)起來只有幾個(gè)單詞,不需要為每個(gè)觀察的對象創(chuàng)建一個(gè) StreamController ,也不需要?jiǎng)?chuàng)建 StreamBuilder。
var name = '新垣結(jié)衣';
下面簡單的一個(gè)后綴就可以把一個(gè)變量變得可觀察,變量每次改變的時(shí)候,使用它的小部件就會(huì)被更新:
var name = '新垣結(jié)衣'.obs;
就這么簡單,這個(gè)變量已經(jīng)是響應(yīng)式的了。然后通過 Obx 或者 GetX 包裹并使用響應(yīng)式變量的控件,在變量改變的時(shí)候就會(huì)被更新:
Obx (() => Text (controller.name));
下面寫個(gè)計(jì)算器的例子:
final count1 = 0.obs;
final count2 = 0.obs;
.obs就實(shí)現(xiàn)了一個(gè)被觀察者,他們不再是 int 類型,而是 RxInt 類型。對應(yīng)的小部件也不再是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),
)),
因?yàn)槭琼憫?yīng)式,不再需要update,每次更改值,都自動(dòng)刷新。但是更神奇的是,他們的運(yùn)算和也是響應(yīng)式的:
int get sum => count1.value + count2.value;
只要更新count1或者count2使用sum的小部件也會(huì)更改:
Obx(() => Text(
'${Get.find<SumController>().sum}',
style: TextStyle(fontWeight: FontWeight.bold),
)),
非常簡單的使用方式,不是嗎?除了使用.obs還有2種方法把變量變成可觀察的:
- 第一種是使用 Rx{Type}。
// 建議使用初始值,但不是強(qiáng)制性的
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>();
將一個(gè)對象轉(zhuǎn)變成可觀察的,也有2種方法:
- 可以將我們的類值轉(zhuǎn)換為 obs
class RxUser {
final name = "Camila".obs;
final age = 18.obs;
}
- 或者可以將整個(gè)類轉(zhuǎn)換為一個(gè)可觀察的類。
class User {
User({String name, int age});
var name;
var age;
}
//實(shí)例化時(shí)。
final user = User(name: "Camila", age: 18).obs;
注意,轉(zhuǎn)化為可觀察的變量后,它的類型不再是原生類型,所以取值不能用變量本身,而是.value
當(dāng)然 GetX 也提供了 api 簡化對 int、List 的操作。此外,Get還提供了精細(xì)的狀態(tài)控制。我們可以根據(jù)特定的條件對一個(gè)事件進(jìn)行條件控制(比如將一個(gè)對象添加到List中):
// 第一個(gè)參數(shù):條件,必須返回true或false。
// 第二個(gè)參數(shù):如果條件為真,則為新的值。
list.addIf(item < limit, item);
響應(yīng)式編程雖好,可不要貪杯。因?yàn)轫憫?yīng)式對 RAM 的消耗比較大,因?yàn)樗麄兊膶?shí)現(xiàn)都是流,如果創(chuàng)建一個(gè)有80個(gè)對象的 List ,每個(gè)對象都有幾個(gè)流,打開dart inspect,查看一個(gè) StreamBuilder 的消耗量,我們就會(huì)明白這不是一個(gè)好的方法。而 GetBuilder 在 RAM 中是非常高效的,幾乎沒有比他更高效的方法。所以這些使用方式在使用過程中要斟酌。
Workers
響應(yīng)式不只這些好處,還有一個(gè) Workers ,將協(xié)助我們在事件發(fā)生時(shí)觸發(fā)特定的回調(diào),也就是 RxJava 的一些操作符;
@override
onInit() {
super.onInit();
/// 每次更改都會(huì)回調(diào)
ever(count1, (_) => print("$_ has been changed"));
/// 第一次更改回調(diào)
once(count1, (_) => print("$_ was changed once"));
/// 更改后3秒回調(diào)
debounce(count1, (_) => print("debouce$_"), time: Duration(seconds: 3));
///3秒內(nèi)更新回調(diào)一次
interval(count1, (_) => print("interval $_"), time: Duration(seconds: 3));
}
我們可以利用 Workers ,去實(shí)現(xiàn)寫一堆對代碼才能實(shí)現(xiàn)的功能。比如防抖函數(shù),在搜索的時(shí)候使用,節(jié)流函數(shù),在點(diǎn)擊事件的時(shí)候使用。
跨路由
上面演示過在同一個(gè)頁面兄弟組件跨組件使用,接下來實(shí)現(xiàn)下不同頁面跨組件使用,首先在CrossOnePage里 put 一個(gè) Controller:
class CrossOnePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
CrossOneController controller = Get.put(CrossOneController());
...
}}
然后在另一個(gè)頁面CrossTwoPage,打印下上一個(gè)頁面put的控制器:
CheetahButton('打印CrossOneController的age', () {
print(Get.find<CrossOneController>().age);
}),
正常輸出。
那么CrossOneController的生命周期多久呢?如果像第一個(gè)頁面一樣是在build里 put 的,那么當(dāng)前頁面退出就銷毀了。如果是成員變量,那么當(dāng)前頁面的引用銷毀才會(huì)銷毀:
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的時(shí)候 put 進(jìn)去的,現(xiàn)在打印2個(gè)控制器,都能打印出來:
[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)在返回第一個(gè)頁面,GetX 已經(jīng)給我們打印了:
GETX] CLOSE TO ROUTE /CrossTwoPage
[GETX] "CrossTwoController" onClose() called
[GETX] "CrossTwoController" deleted from memory
然后我們在第一個(gè)頁面點(diǎn)擊按鈕,分別打印頁面CrossTwoPage的2個(gè)控制器:
════════ 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 了,另一個(gè)依然存在,那是不是這種不會(huì)銷毀呢?因?yàn)榈谝粋€(gè)頁面的路由依然持有第二個(gè)頁面,第二個(gè)頁面的實(shí)例還在內(nèi)存中,所以控制器作為成員變量依然存在,退出第一個(gè)頁面,自然就銷毀了:
[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雖然各個(gè)功能均可單獨(dú)引用使用,但是狀態(tài)管理和路由是搭配的,如果沒有使用 route_manager 組件,那么狀態(tài)管理的生命周期就會(huì)失效。put的Controller在不使用的時(shí)候不會(huì)再被刪除,而變成了應(yīng)用狀態(tài)常駐內(nèi)存里。
如果項(xiàng)目的路由暫時(shí)不能使用 GetX 替換,那么怎么使用狀態(tài)管理呢,很簡單,封裝一個(gè)自動(dòng)刪除Controller的控件即可,因?yàn)榱?xí)慣使用GetBinding,待可以替換為 GetX 路由的時(shí)候直接帶上GetBinding,所以封裝了一個(gè)GetBinding的控件和一個(gè)不使用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)建對應(yīng)的
GetBinding、GetxController和 Page , - 對應(yīng)的 Page 修改為繼承
GetDisposeView, - 實(shí)現(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 。
下面是一個(gè)不使用GetBinding的控件,比上面的使用更簡單,不需要?jiǎng)?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)建對應(yīng)的
GetxController和 Page ,對應(yīng)的 Page 修改為繼承
GetDisposeView,-
實(shí)現(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()); } }