Flutter状态GetX详解,快速上手

2022年9月26日 1249点热度 0人点赞 0条评论

说状态管理到底在说些什么
一个应用的状态就是当这个应用运行时存在于内存中的所有内容。当然许多状态,例如纹理、动画状态等,框架本身会替开发者管理,所以对于状态更合适的定义是“当你需要重建用户界面时所需要的数据”,我们需要自己管理的状态可以分为两种概念类型:短时 (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,我们使用计数器示例来演示一下基本使用:

<code>class SimpleController extends GetxController {
  int _counter = 0;
  int get counter =&gt; _counter;

  void increment() {
    _counter++;
    update();
  }}
这是一个控制器,有 UI 需要的数据counter和用户点击一次加1的方法。
在 UI 层一个展示的文本和一个按钮:

class SimplePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print(&#039;SimplePage--build&#039;);
    return GetBuilder&lt;SimpleController&gt;(
        init: SimpleController(),
        builder: (controller) {
          return Scaffold(
            appBar: AppBar(title: Text(&#039;Simple&#039;)),
            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(&#039;SimplePage--build&#039;);
    return Scaffold(
      appBar: AppBar(title: Text(&#039;Simple&#039;)),
      body: Center(
        child: GetBuilder&lt;SimpleController&gt;(
            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&lt;SimpleController&gt;().increment();
          // controller..increment();
        },
GetxController也有生命周期的:

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

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

  @override
  void onInit() {
    super.onInit();
    print(&#039;SimpleController--onInit&#039;);
  }

  @override
  void onReady() {
    super.onReady();
    print(&#039;SimpleController--onReady&#039;);
  }

  @override
  void onClose() {
    super.onClose();
    print(&#039;SimpleController--onClose&#039;);
  }
}
之前在这里打印了一句:

class SimplePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print(&#039;SimplePage--build&#039;);
    return Scaffold(
    。。。
再次打开这个页面,控制台输出:

flutter: SimplePage--build
flutter: SimpleController--onInit[GETX] &quot;SimpleController&quot; has been initialized
flutter: SimpleController--onReady

SimplePage-build-&gt;SimpleController-onInit-&gt;SimpleController-onReady
推出当前页面返回:

[GETX] CLOSE TO ROUTE /SimplePage
flutter: SimpleController--onClose
[GETX] &quot;SimpleController&quot; onClose() called
[GETX] &quot;SimpleController&quot; deleted from memory
[GETX] Instance &quot;SimpleController&quot; already removed.
可以看到SimpleController已经被删除。
局部更新
多种状态可以分别更新,不需要为每个状态创建一个类。
再添加一个变量:

  int _counter = 0;
  int get counter =&gt; _counter;

  String _name = &quot;Lili&quot;;
  String get firstName =&gt; _name;

    void increment() {
    _counter++;
    _name = WordPair.random().asPascalCase;
    update([&#039;counter&#039;]);
  }

  void changeName() {
    _counter++;
    _name = WordPair.random().asPascalCase;
    update([&#039;name&#039;]);
  }
两个方法分别改变两个变量,但是注意update([&#039;counter&#039;]里添加了 id 数组,这样就只更新这个 id 对应的GetBuilder:

    GetBuilder&lt;SimpleAdvancedController&gt;(
            id: &#039;counter&#039;,
            builder: (ctl) =&gt; Text(ctl.counter.toString()),
          ),
          SizedBox(
            height: 50,
          ),
          GetBuilder&lt;SimpleAdvancedController&gt;(
            id: &#039;name&#039;,
            builder: (ctl) =&gt; Text(ctl.firstName),
          ),
响应式刷新
我们都用过 StreamControllers ,然后以流的方式发送数据。在 GetX 可以实现同样的功能,并且实现起来只有几个单词,不需要为每个观察的对象创建一个 StreamController ,也不需要创建 StreamBuilder。

var name = &#039;新垣结衣&#039;;
下面简单的一个后缀就可以把一个变量变得可观察,变量每次改变的时候,使用它的小部件就会被更新:

var name = &#039;新垣结衣&#039;.obs;
就这么简单,这个变量已经是响应式的了。然后通过 Obx 或者 GetX 包裹并使用响应式变量的控件,在变量改变的时候就会被更新:

Obx (() =&gt; Text (controller.name));
下面写个计算器的例子:

 final count1 = 0.obs;
 final count2 = 0.obs;
.obs就实现了一个被观察者,他们不再是 int 类型,而是 RxInt 类型。对应的小部件也不再是GetBuilder了,而是下面两种:

           GetX&lt;SumController&gt;(
                  builder: (_) {
                    print(&quot;count1 rebuild&quot;);
                    return Text(
                      &#039;${_.count1}&#039;,
                      style: TextStyle(fontWeight: FontWeight.bold),
                    );
                  },
                ),
               Obx(() =&gt; Text(
                      &#039;${Get.find&lt;SumController&gt;().count2}&#039;,
                      style: TextStyle(fontWeight: FontWeight.bold),
                    )),
因为是响应式,不再需要update,每次更改值,都自动刷新。但是更神奇的是,他们的运算和也是响应式的:
int get sum =&gt; count1.value + count2.value;
只要更新count1或者count2使用sum的小部件也会更改:

    Obx(() =&gt; Text(
                      &#039;${Get.find&lt;SumController&gt;().sum}&#039;,
                      style: TextStyle(fontWeight: FontWeight.bold),
                    )),
非常简单的使用方式,不是吗?除了使用.obs还有2种方法把变量变成可观察的:
第一种是使用 Rx{Type}。

// 建议使用初始值,但不是强制性的final name = RxString(&#039;&#039;);final isLogged = RxBool(false);final count = RxInt(0);final balance = RxDouble(0.0);final items = RxList&lt;String&gt;([]);final myMap = RxMap&lt;String, int&gt;({});
第二种是使用 Rx,规定泛型 Rx&lt;Type&gt;。

final name = Rx&lt;String&gt;(&#039;&#039;);final isLogged = Rx&lt;Bool&gt;(false);final count = Rx&lt;Int&gt;(0);final balance = Rx&lt;Double&gt;(0.0);final number = Rx&lt;Num&gt;(0)final items = Rx&lt;List&lt;String&gt;&gt;([]);final myMap = Rx&lt;Map&lt;String, int&gt;&gt;({});// 自定义类 - 可以是任何类final user = Rx&lt;User&gt;();
将一个对象转变成可观察的,也有2种方法:
可以将我们的类值转换为 obs

class RxUser {
  final name = &quot;Camila&quot;.obs;
  final age = 18.obs;}
或者可以将整个类转换为一个可观察的类。

class User {
  User({String name, int age});
  var name;
  var age;}//实例化时。final user = User(name: &quot;Camila&quot;, age: 18).obs;
注意,转化为可观察的变量后,它的类型不再是原生类型,所以取值不能用变量本身,而是.value
当然 GetX 也提供了 api 简化对 int、List 的操作。此外,Get还提供了精细的状态控制。我们可以根据特定的条件对一个事件进行条件控制(比如将一个对象添加到List中):

// 第一个参数:条件,必须返回true或false。
// 第二个参数:如果条件为真,则为新的值。
list.addIf(item &lt; limit, item);
响应式编程虽好,可不要贪杯。因为响应式对 RAM 的消耗比较大,因为他们的实现都是流,如果创建一个有80个对象的 List ,每个对象都有几个流,打开dart inspect,查看一个 StreamBuilder 的消耗量,我们就会明白这不是一个好的方法。而 GetBuilder 在 RAM 中是非常高效的,几乎没有比他更高效的方法。所以这些使用方式在使用过程中要斟酌。
Workers
响应式不只这些好处,还有一个 Workers ,将协助我们在事件发生时触发特定的回调,也就是 RxJava 的一些操作符;

  @override
  onInit() {
    super.onInit();

    /// 每次更改都会回调
    ever(count1, (_) =&gt; print(&quot;$_ has been changed&quot;));

    /// 第一次更改回调
    once(count1, (_) =&gt; print(&quot;$_ was changed once&quot;));

    /// 更改后3秒回调
    debounce(count1, (_) =&gt; print(&quot;debouce$_&quot;), time: Duration(seconds: 3));

    ///3秒内更新回调一次
    interval(count1, (_) =&gt; print(&quot;interval $_&quot;), 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(&#039;打印CrossOneController的age&#039;, () {
            print(Get.find&lt;CrossOneController&gt;().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(&#039;CrossTwoPage&#039;)),
      body: Container(
          child: Column(
        children: [
          CheetahButton(&#039;打印CrossTwoController&#039;, () {
            print(Get.find&lt;CrossTwoController&gt;());
          }),
          CheetahButton(&#039;CrossTwoSecondController&#039;, () {
            print(Get.find&lt;CrossTwoSecondController&gt;());
          }),
          CheetahButton(&#039;打印CrossOneController的age&#039;, () {
            print(Get.find&lt;CrossOneController&gt;().age);
          }),
        ],
      )),
    );
  }}
CrossTwoSecondController是成员变量,CrossTwoController是在build的时候 put 进去的,现在打印2个控制器,都能打印出来:

[GETX] &quot;CrossTwoSecondController&quot; has been initialized[GETX] GOING TO ROUTE /CrossTwoPage[GETX] &quot;CrossTwoController&quot; has been initialized
I/flutter (16952): Instance of &#039;CrossTwoController&#039;I/flutter (16952): Instance of &#039;CrossTwoSecondController&#039;
现在返回第一个页面,GetX 已经给我们打印了:

GETX] CLOSE TO ROUTE /CrossTwoPage[GETX] &quot;CrossTwoController&quot; onClose() called[GETX] &quot;CrossTwoController&quot; deleted from memory
然后我们在第一个页面点击按钮,分别打印页面CrossTwoPage的2个控制器:

════════ Exception caught by gesture ═══════════════════════════════════════════&quot;CrossTwoController&quot; not found. You need to call &quot;Get.put(CrossTwoController())&quot; or &quot;Get.lazyPut(()=&gt;CrossTwoController())&quot;════════════════════════════════════════════════════════════════════════════════
I/flutter (16952): Instance of &#039;CrossTwoSecondController&#039;
在build里 put 的控制器已经销毁为 null 了,另一个依然存在,那是不是这种不会销毁呢?因为第一个页面的路由依然持有第二个页面,第二个页面的实例还在内存中,所以控制器作为成员变量依然存在,退出第一个页面,自然就销毁了:

[GETX] CLOSE TO ROUTE /CrossOnePage[GETX] &quot;CrossOneController&quot; onClose() called[GETX] &quot;CrossOneController&quot; deleted from memory[GETX] &quot;CrossTwoSecondController&quot; onClose() called[GETX] &quot;CrossTwoSecondController&quot; deleted from memory
不使用 GetX 路由的状态管理
GetX虽然各个功能均可单独引用使用,但是状态管理和路由是搭配的,如果没有使用 route_manager 组件,那么状态管理的生命周期就会失效。put的Controller在不使用的时候不会再被删除,而变成了应用状态常驻内存里。
如果项目的路由暂时不能使用 GetX 替换,那么怎么使用状态管理呢,很简单,封装一个自动删除Controller的控件即可,因为习惯使用GetBinding,待可以替换为 GetX 路由的时候直接带上GetBinding,所以封装了一个GetBinding的控件和一个不使用GetBinding的控件:

abstract class GetBindingView&lt;T extends GetxController&gt;
    extends StatefulWidget {
  final String? tag = null;

  T get controller =&gt; GetInstance().find&lt;T&gt;(tag: tag);

  @protected
  Widget build(BuildContext context);

  @protected
  Bindings? binding();

  @override
  _AutoDisposeState createState() =&gt; _AutoDisposeState&lt;T&gt;();}class _AutoDisposeState&lt;S extends GetxController&gt;
    extends State&lt;GetBindingView&gt; {
  _AutoDisposeState();

  @override
  Widget build(BuildContext context) {
    return widget.build(context);
  }

  @override
  void initState() {
    super.initState();
    widget.binding()?.dependencies();
  }

  @override
  void dispose() {
    Get.delete&lt;S&gt;();
    super.dispose();
  }}

使用很简单:创建对应的GetBinding、GetxController和 Page,对应的 Page 修改为继承GetDisposeView,
实现binding()方法并返回第一步创建的GetBinding。

class BingPagePage extends GetBindingView&lt;BingPageController&gt; {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(&#039;BingPage Page&#039;)),
      body: Container(
        child: Obx(()=&gt;Container(child: Text(controller.obj),)),
      ),
    );
  }

  @override
  Bindings? binding() =&gt;BingPageBinding();
  }
接下来就可以像使用 GetView 一样使用了,如果以后替换了 GetX 路由,只需要把 GetDisposeView替换为GetView 。
下面是一个不使用GetBinding的控件,比上面的使用更简单,不需要创建GetBinding:

abstract class GetDisposeView&lt;T extends GetxController&gt; extends StatefulWidget {
  final String? tag = null;

  T get controller =&gt; GetInstance().find&lt;T&gt;(tag: tag);

  @protected
  Widget build(BuildContext context);

  @protected
  void setController();

  @override
  _AutoDisposeState createState() =&gt; _AutoDisposeState&lt;T&gt;();}class _AutoDisposeState&lt;S extends GetxController&gt;
    extends State&lt;GetDisposeView&gt; {
  _AutoDisposeState();

  @override
  Widget build(BuildContext context) {
    return widget.build(context);
  }

  @override
  void initState() {
    super.initState();
    widget.setController();
  }

  @override
  void dispose() {
    Get.delete&lt;S&gt;();
    super.dispose();
  }}

使用:创建对应的GetxController和 Page ,对应的 Page 修改为继承GetDisposeView,实现setController()方法并返回第一步创建的put第一步创建的GetxController对象。

class AutoDisposePage extends GetDisposeView&lt;BingPageController&gt; {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(&#039;Auto Dispose Page&#039;)),
      body: Container(
        child: Obx(()=&gt;Container(child: Text(controller.obj),)),
      ),
    );
  }

  @override
  void setController() {
    Get.put(BingPageController());
  }}</code>

小小调酒师

此刻打盹,你将做梦; 此刻学习,你将圆梦。 个人邮箱:shellways@foxmail.com

文章评论