小册试读3 - GetX框架的详述

442 阅读5分钟

状态管理是数据驱动视图设计理念中非常重要的一个概念,是每一个Flutter开发者都需要掌握的。
状态管理的本质是管理数据,如何获取,如何接收数据,并通过数据的变化驱动视图UI的改变。

2019 Google I/O 大会,Google就推出Provider,成为官方推荐的状态管理方式之一。Provider包基于InheritedWidget思想实现的一套跨组件状态共享解决方案,绑定InheritedWidget与依赖它的子孙组件的依赖关系,并且当InheritedWidget数据发生变化时,可以自动更新依赖的子孙组件!利用这个特性,我们可以将需要跨组件共享的状态保存在InheritedWidget中,然后在子组件中引用InheritedWidget即可。

笔者在使用Provider的过程中,发现了许多痛点,最大的痛点在于Provider只能在父子组件保存状态,同层级模块状态管理需要全局处理,也就是说非父子组件的状态管理问题,需要借助其他手段(全局,单例,eventbus)实现。也许后续的版本已经解决,笔者没有继续追踪。

也许正是存在这样的痛点,GetX应运而生,GetX可以随时添加、删除控制器Controller,使用控制器Controller便能轻松的进行父子、同级组件间的状态管理!

本小册便使用了GetXGetXFlutter状态管理的一个轻量且强大的解决方案,github地址:github.com/jonataslaw/… ,GetX的核心思想与实现:

  • 便捷的路由管理
  • 高性能的状态管理
  • 智能的依赖注入

1,便捷的路由管理

1.1,普通路由

首先看看原生Flutter路由中,是如何跳转页面的:

    Navigator.push(context, MaterialPageRoute<void>(
      builder: (BuildContext context) {
        return NextPage();
      },
    ));

GetX中:

Get.to(NextPage());

就是这么便捷简单。

1.2,命名路由

对应于原生Flutter中的

Navigator.pushNamed(context, "nextPage")

GetX中:

Get.toNamed(Routes.NextPage);

可以看到GetX并不需要上下文参数context便能实现路由跳转。

注册路由表,命名路由和页面一一对应

abstract class AppPages {
  static final pages = [
    GetPage(
      name: Routes.Initial,
      page: () => HomePage(),
    ),
    GetPage(
      name: Routes.NextPage,
      page: () => NextPage(),
    ),
  ];
}

abstract class Routes {
  static const Initial = '/';
  static const NextPage = '/NextPage';
}

main()函数中,将MaterialApp替换为GetMaterialApp

void main() {
  runApp(GetMaterialApp(
    debugShowCheckedModeBanner: false,
    initialRoute: Routes.Initial,   //App的初始页面(第一个页面)
    theme: appThemeData,
    defaultTransition: Transition.fade,
    getPages: AppPages.pages,       //注册所有命名路由
  ));
}

参数传递

Get.toNamed(Routes.NextPage, arguments: '你好');

NextPage()中接收参数

String text = Get.arguments;

从上可以看出,GetX大大简化了路由管理,其他未述用法细节如Snackbar, Dialog, BottomSheet的跳转可以参照GetX官方文档。

2,高性能的状态管理

首先我们来看一个典型的使用GetX框架的页面结构:

image.png

controller是控制器,继承GetxController,可以重写onInit(),onReady()等生命周期函数,通常在controller中定义页面page需要的数据变量。controller的作用便是获取数据,处理数据,数据的变化从而驱动视图page的改变!
page便是页面的具体的布局文件。
binding则是将controller绑定到page上,也就是说由这个controller来控制这个page,形成一对一的对应关系,这个具体在下一小节讲解。

我们来具体看下面这个例子:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetBuilder<HomeController>(
        init: HomeController(),
        builder: (controller) {
          return Scaffold(
            appBar: AppBar(title: Text('HomePage')),
            body: Center(
              child: Text(controller.counter.toString()), //显示controller中变量counter的值
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: () {
                controller.increment(); //点击按钮调用controller的方法,更改变量counter的值
              },
              child: Icon(Icons.add),
            ),
          );
        });
  }
}

整个build()方法使用GetBuilder<HomeController>包裹其中,并在init参数中初始化了这个page的控制器HomeController,下面是controller代码块:

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

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

点击page上按钮,调用controller中的increment()方法,改变了_counter的值,同时调用GetX方法update(),这样page中使用GetBuilder()包裹的child都会刷新。这便实现了数据驱动视图的改变。

上面的例子可以看出,点击按钮后,整个GetBuilder()包裹的childtextbutton都会刷新,但是显然button本身是不需要刷新的。那么我们仅仅需要把GetBuilder()包裹在text的外层即可。这便实现了最小局部的刷新,性能提升了。
上面例子刷新页面时,手动调用了update()方法,有没有可能在变量值发生变化时,无需调用update()方法,就能自动实现page的刷新呢?答案是肯定的,GetX推出了响应性刷新,之前定义变量如下:

var textString = 'hello';

我们只需要加一个简单的后缀,就能将textString变量变成可观察变量,改变变量值时,使用它的widget便能被刷新。

var textString = 'hello'.obs;

使用该变量的widget也就不使用上面的GetBuilder包裹了,而是改成如下两种方式:

GetX<HomeController>(
   builder: (_) {
      return Text(
         '${_.count1}',
          style: TextStyle(fontWeight: FontWeight.bold),
       );
     },
    ),
    
Obx(() => Text(
    '${Get.find<HomeController>().count1}',
     style: TextStyle(fontWeight: FontWeight.bold),
)),

3,智能的依赖注入

在第1章节中,提到binding的作用是将controller注入到内存中,将controllerpage相关联,那么这个注入动作和关联动作是如何实现的呢?
关联动作:在路由定义的时候,加入binding属性

abstract class AppPages {
  static final pages = [
    //闪屏页
    GetPage(
        name: Routes.SPLASH,
        page: () => SplashPage(),
        binding: SplashBinding()),
    //主页
    GetPage(name: Routes.HOME, page: () => HomePage(), binding: HomeBinding()),
  ];
}

注入动作:在binding.dart中,注入controller

class SplashBinding extends Bindings{
  @override
  void dependencies() {
    Get.put<SplashController>(SplashController()); //注入代码
  }
}

Get.put<SplashController>(SplashController())注入的controller的生命周期,和其绑定的page(widget)的生命周期一致,page从内存中删除,则controller也会被销毁。那么如果在App入口函数main()put,则这个controller便像一个单例一样,一直存在。
GetX还有下面几种注入方式:

  • 延迟注入
  Get.lazyPut<SplashController>(() => SplashController());

这个时候SplashController并不会被创建,而是在使用它的时候才会被创建,

  Get.find<SplashController>();  //使用时创建
  • 异步注入 当注入的controller需要一定的时候初始化时,我们可以选择异步注入
  Get.putAsync<SharedPreferences>(() async {
    final sp = await SharedPreferences.getInstance();
    return sp;
  });

最后需要的注意的一点是:我们通常在App中有这样的跳转需求,商品详情页A -> 其他页面B -> 商品详情页C,路由中出现了两个相同的页面page,此时在为商品详情页注入controller的时候需要加上tag属性进行区分。

    Get.put<ItemDetailController>(ItemDetailController(), tag:"你设置的tag值");