Flutter狀態(tài)管理終極方案GetX第二篇——狀態(tài)管理

說狀態(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種方法把變量變成可觀察的:

  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胸懈,規(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種方法:

  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還提供了精細的狀態(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)下不同頁面跨組件使用,首先在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 進去的,現(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'

buildput 的控制器已經(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)管理的生命周期就會失效。putController在不使用的時候不會再被刪除,而變成了應用狀態(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());
      }
    }
    
    
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末菊碟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子在刺,更是在濱河造成了極大的恐慌逆害,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蚣驼,死亡現(xiàn)場離奇詭異魄幕,居然都是意外死亡,警方通過查閱死者的電腦和手機颖杏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門纯陨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人留储,你說我怎么就攤上這事翼抠。” “怎么了获讳?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵阴颖,是天一觀的道長。 經(jīng)常有香客問我丐膝,道長量愧,這世上最難降的妖魔是什么钾菊? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮侠畔,結果婚禮上结缚,老公的妹妹穿的比我還像新娘。我一直安慰自己软棺,他們只是感情好红竭,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著喘落,像睡著了一般茵宪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瘦棋,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天稀火,我揣著相機與錄音,去河邊找鬼赌朋。 笑死凰狞,一個胖子當著我的面吹牛,可吹牛的內容都是我干的沛慢。 我是一名探鬼主播赡若,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼团甲!你這毒婦竟也來了逾冬?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤躺苦,失蹤者是張志新(化名)和其女友劉穎身腻,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體匹厘,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡嘀趟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了愈诚。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片去件。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖扰路,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情倔叼,我是刑警寧澤汗唱,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站丈攒,受9級特大地震影響哩罪,放射性物質發(fā)生泄漏授霸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一际插、第九天 我趴在偏房一處隱蔽的房頂上張望碘耳。 院中可真熱鬧,春花似錦框弛、人聲如沸辛辨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽斗搞。三九已至,卻和暖如春慷妙,著一層夾襖步出監(jiān)牢的瞬間僻焚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工膝擂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留虑啤,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓架馋,卻偏偏與公主長得像狞山,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子绩蜻,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容