Flutter狀態管理終極方案GetX第二篇——狀態管理

說狀態管理到底在說些什么

一個應用的狀態就是當這個應用運行時存在于內存中的所有內容。當然許多狀態,例如紋理、動畫狀態等,框架本身會替開發者管理,所以對于狀態更合適的定義是“當你需要重建用戶界面時所需要的數據”,我們需要自己管理的狀態可以分為兩種概念類型:短時 (ephemeral) 狀態和應用 (app) 狀態。

短時狀態

短時狀態是可以完全包含在一個獨立 widget 中的狀態,也成為局部狀態。

  • 一個 PageView 組件中的當前頁面
  • 一個復雜動畫中當前進度
  • 一個 BottomNavigationBar 中當前被選中的 tab
  • 一個文本框顯示的內容

應用狀態

如果在應用中的多個部分之間共享一個非短時的狀態,并且在用戶會話期間保留這個狀態,我們稱之為應用狀態(有時也稱共享狀態)。

  • 用戶選項
  • 登錄信息
  • 一個社交應用中的通知
  • 一個電商應用中的購物車
  • 一個新聞應用中的文章已讀/未讀狀態

為什么選擇 GetX 做狀態管理?

開發者一直致力于業務邏輯分離的概念,Flutter 也有利用 BLoc 、Provider 衍生的 MVC、MVVM 等架構模式,但是這幾種方案的狀態管理均使用了上下文(context),需要上下文來尋找InheritedWidget,這種解決方案限制了狀態管理必須在父子代的 widget 樹中,業務邏輯也會對 View 產生較強依賴。

而 GetX 因為不需要上下文,突破了InheritedWidget的限制,我們可以在全局和模塊間共享狀態,這正是 BLoc 、Provider 等框架的短板。

另外 GetX 控制器也是有生命周期的,例如當我們需要業務層進行 APIREST 時,我們可以不依賴于界面中的任何東西。可以使用onInit來啟動http調用,當數據到達賦值給變量后,利用 GetX 響應式的特性,使用該變量的 Widgets 將在界面中自動更新。這樣在 UI層只需要寫界面,除了用戶事件(比如點擊按鈕)之外,不需要向業務邏輯層發送任何東西。


簡單使用

對于以前使用過 ChangeNotifier 的同學來說,可以把GetxController當做ChangeNotifier,我們使用計數器示例來演示一下基本使用:

class SimpleController extends GetxController {
  int _counter = 0;
  int get counter => _counter;

  void increment() {
    _counter++;
    update();
  }
}

這是一個控制器,有 UI 需要的數據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

這看起來和別狀態管理框架并無不同,有時我們只想重新 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強大的一點的就表現出來了,按鈕和文本并不在父子組件,并且和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已經被刪除。

局部更新

多種狀態可以分別更新,不需要為每個狀態創建一個類。

再添加一個變量:

  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 數組,這樣就只更新這個 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 ,然后以流的方式發送數據。在 GetX 可以實現同樣的功能,并且實現起來只有幾個單詞,不需要為每個觀察的對象創建一個 StreamController ,也不需要創建 StreamBuilder。

var name = '新垣結衣';

下面簡單的一個后綴就可以把一個變量變得可觀察,變量每次改變的時候,使用它的小部件就會被更新:

var name = '新垣結衣'.obs;

就這么簡單,這個變量已經是響應式的了。然后通過 Obx 或者 GetX 包裹并使用響應式變量的控件,在變量改變的時候就會被更新:

Obx (() => Text (controller.name));

下面寫個計算器的例子:

 final count1 = 0.obs;
 final count2 = 0.obs;

.obs就實現了一個被觀察者,他們不再是 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種方法把變量變成可觀察的:

  1. 第一種是使用 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>({});

  1. 第二種是使用 Rx,規定泛型 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種方法:

  1. 可以將我們的類值轉換為 obs
class RxUser {
  final name = "Camila".obs;
  final age = 18.obs;
}
  1. 或者可以將整個類轉換為一個可觀察的類。
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還提供了精細的狀態控制。我們可以根據特定的條件對一個事件進行條件控制(比如將一個對象添加到List中):

// 第一個參數:條件,必須返回true或false。
// 第二個參數:如果條件為真,則為新的值。
list.addIf(item < limit, item);

響應式編程雖好,可不要貪杯。因為響應式對 RAM 的消耗比較大,因為他們的實現都是流,如果創建一個有80個對象的 List ,每個對象都有幾個流,打開dart inspect,查看一個 StreamBuilder 的消耗量,我們就會明白這不是一個好的方法。而 GetBuilder 在 RAM 中是非常高效的,幾乎沒有比他更高效的方法。所以這些使用方式在使用過程中要斟酌。

Workers

響應式不只這些好處,還有一個 Workers ,將協助我們在事件發生時觸發特定的回調,也就是 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 ,去實現寫一堆對代碼才能實現的功能。比如防抖函數,在搜索的時候使用,節流函數,在點擊事件的時候使用。

跨路由

上面演示過在同一個頁面兄弟組件跨組件使用,接下來實現下不同頁面跨組件使用,首先在CrossOnePageput 一個 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 進去的,現在打印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'

現在返回第一個頁面,GetX 已經給我們打印了:

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'

buildput 的控制器已經銷毀為 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 路由的狀態管理

GetX雖然各個功能均可單獨引用使用,但是狀態管理和路由是搭配的,如果沒有使用 route_manager 組件,那么狀態管理的生命周期就會失效。putController在不使用的時候不會再被刪除,而變成了應用狀態常駐內存里。

如果項目的路由暫時不能使用 GetX 替換,那么怎么使用狀態管理呢,很簡單,封裝一個自動刪除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();
  }
}

使用很簡單:

  • 創建對應的GetBindingGetxController和 Page ,
  • 對應的 Page 修改為繼承 GetDisposeView
  • 實現binding()方法并返回第一步創建的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的控件,比上面的使用更簡單,不需要創建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();
  }
}

使用:

  • 創建對應的GetxController和 Page ,

  • 對應的 Page 修改為繼承 GetDisposeView

  • 實現setController()方法并返回第一步創建的put第一步創建的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());
      }
    }
    
    
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容