全栈工程师?
嗯.....目前并不是!

Flutter状态管理终极方案GetX第二篇——状态管理

GetX状态管理

说状态管理到底在说些什么

Flutter 应用是声明式的,这也就意味着 Flutter 构建的用户界面就是应用的当前状态。当 Flutter 应用的状态发生改变时(例如,用户在设置界面中点击了一个开关选项)改变了状态,这将会触发用户界面的重绘。

一个应用的状态就是当这个应用运行时存在于内存中的所有内容。当然许多状态,例如纹理、动画状态等,框架本身会替我管理,所以对于状态更合适的定义是“当任何时候你需要重建你的用户界面时你所需要的数据”,我们需要自己管理的状态可以分为两种概念类型:短时 (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层只需要写界面,除了用户事件(比如点击按钮)之外,不需要向业务逻辑层发送任何东西。


GetX 的状态管理

Get具有两个不同的状态管理器: 简单状态管理器(称为GetBuilder)。 响应式状态管理器(称为GetX)。 除此之外还有一个最为简单的响应式状态管理器:Obx;

简单状态管理器

使用 StatefulWidget 意味着我们在非必要地存储整个屏幕的状态,StatefulWidget 类是一个比StatelessWidget 大的类,它将分配更多的内存,这在一个或两个类之间可能不会产生很大的区别,但是我们的应用会拥有上百个 Widget。

GetBuilder 状态管理器非常轻巧且易于使用。我们不再需要StatefulWidget。除非需要使用诸如 TickerProviderStateMixin 之类的 mixin,否则完全不需要将 StatefulWidget 与 GetX 一起使用。

我们可以直接从 GetBuilder 调用 StatefulWidget 的所有方法。例如,如果需要调用initState()或dispose()方法,则可以直接调用它们。

GetBuilder<Controller>(
  initState: (_) => Controller.to.fetchApi(),
  dispose: (_) => Controller.to.closeStreams(),
  builder: (s) => Text('${s.username}'),
),
响应式状态管理器

当我们需要对正在更新的内容进行更多控制时,GetX()在合适不过了。 GetBuilder 是一个简单的状态更新程序(类似于setState()),只需几行代码即可完成。重点是使CPU影响最小,并且仅实现一个目的:状态重建,并花费尽可能少的资源。 响应式状态管理器里面的一切都是由 Stream 数据流操作的。可以使用rxDart并可以订阅数据的改变。

var name = '新垣结衣';

把一个变量变得可观察,每次改变,使用它的小部件都会更新:

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

就这么简单,这个变量已经是响应式了。每次改变,下面的小部件就会更新。

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

上面的代码在后台做了什么?创造了 String 类型的 Stream,分配了初始值 新垣结衣,.obs把字符串变成了 RxString ,当其中的字符串改变,就会通知到使用它的小部件重建。

简单使用

对于以前使用过 ChangeNotifier 的同学来说,可以把GetxController当做ChangeNotifier,我们使用计数器示例来演示一下基本使用:


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SimpleController</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">GetxController</span> </span>{
  <span class="hljs-built_in">int</span> _counter = <span class="hljs-number">0</span>;
  <span class="hljs-built_in">int</span> <span class="hljs-keyword">get</span> counter => _counter;

  <span class="hljs-keyword">void</span> increment() {
    _counter++;
    update();
  }
}

这是一个控制器,有 UI 需要的数据counter和用户点击一次加1的方法。

在 UI 层一个展示的文本和一个按钮:

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SimplePage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'SimplePage--build'</span>);
    <span class="hljs-keyword">return</span> GetBuilder<SimpleController>(
        init: SimpleController(),
        builder: (controller) {
          <span class="hljs-keyword">return</span> Scaffold(
            appBar: AppBar(title: Text(<span class="hljs-string">'Simple'</span>)),
            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();
          <span class="hljs-comment">// controller..increment();</span>
        },

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] <span class="hljs-string">SimpleController</span> has been initialized
flutter: SimpleController--onReady
SimplePage-build->SimpleController-onInit->SimpleController-onReady

返回:

[GETX] CLOSE TO ROUTE /SimplePage
flutter: SimpleController--onClose
[GETX] <span class="hljs-string">SimpleController</span> onClose() called
[GETX] <span class="hljs-string">SimpleController</span> deleted from memory
[GETX] Instance <span class="hljs-string">SimpleController</span> 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),
          ),

响应式更新

我们都用过 StreamController ,然后以流的方式发送数据。在 GetX 可以实现同样的功能,并且实现起来只有几个单词,不需要为每个观察的对象创建一个 StreamController ,也不需要创建 StreamBuilder。

下面写个计算器的例子:

 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。
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 ,去实现写一堆对代码才能实现的功能。比如防抖函数,在搜索的时候使用,节流函数,在点击事件的时候使用。

跨路由

上面演示过在同一个页面兄弟组件跨组件使用,这里实现下不同页面跨组件,首先在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 进去的,现在打印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

然后我们在第一个页面点击按钮,分别打印页面2的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 的控制器已经销毁为 null 了,另一个依然存在,那是不是这种不会销毁呢?因为第一个页面的路由依然持有第二个页面,第二个页面的实例还在内存中,所以控制器作为成员变量依然存在,退出第一个页面,自然就销毁了:

[GETX] CLOSE TO ROUTE /CrossOnePage
[GETX] CrossOneController onClose() called
[GETX] CrossOneController deleted from memory
[GETX] CrossTwoSecondController onClose() called
[GETX] CrossTwoSecondController deleted from memory

flutter plugin not installed this adds flutter specific functionalityflutter教程flutter教程

版权声明:本文采用知识共享 署名4.0国际许可协议 [BY-NC-SA] 进行授权
文章名称:《Flutter状态管理终极方案GetX第二篇——状态管理》
文章链接:https://www.pmbear.com/flutter%e7%8a%b6%e6%80%81%e7%ae%a1%e7%90%86%e7%bb%88%e6%9e%81%e6%96%b9%e6%a1%88getx%e7%ac%ac%e4%ba%8c%e7%af%87-%e7%8a%b6%e6%80%81%e7%ae%a1%e7%90%86/
本站资源仅供个人学习交流,请于下载后24小时内删除,不允许用于商业用途,否则法律问题自行承担。

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

大前端WP主题 更专业 更方便

联系我们联系我们