阅读 216

文科生也编程 - GetX的简单使用

GetX简介

GetX是Flutter的轻便而强大的解决方案,它结合了高性能状态管理,智能依赖注入和快速实用的路由管理。

更新时间:4.18

一、响应式控件

(一)Obx

关键词:StatelessWidget、.obs、Obx()

[ 例:一个加法按钮 —— 使用.obs变量和Obx对象构建响应式控件 ]

下面这个例子,是基于可观察的变量和控件Obx构建的一个简单视图,点击按钮可以改变页面中的数字; 该例子表明GetX框架可在页面未进行setState()刷新的情况下,实现由build方法构建的控件的状态更新。

import 'package:flutter/material.dart';
import 'package:get/get.dart';

// ignore: must_be_immutable
class GetXPage extends StatelessWidget {
  var count = 0.obs;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: Obx(() => Text("$count"))
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => count++,
        child: Icon(Icons.add),
      ),
    );
  }
}
复制代码

如果没有使用GetX框架(即变量count无后缀.obs 或Text没有包含在控件Obx中),则点击按钮+,屏幕中的数字不会增加;这是由于,方法build仅执行了一次,onPressed所引起的 count值的变化 并没有进入控件Text,因而屏幕中的数字显示不会变化。

[ 编写步骤 ]

  • 使用StatelessWidget创建Widget对象;
  • 声明一个"可观察变量"count,在其后缀.obs,此变量将使用于控件Obx中 ;
  • 使用 控件Obx() 包裹 包含变量count的控件Text() ,常用格式为Obx( () => 目标更新状态的控件 )

注:使用动态类型的可观察变量无法用于有类型要求的构造传参;

(二)GetX

关键词:.obs、GetX()

[ 例:使用别人家的按钮(一) —— 基于可观察变量的视图-逻辑分离 ]

使用GetX框架可以加载并使用非当前类的控制器来更新当前视图控件的状态,实现视图与逻辑的分离;

import 'package:flutter/material.dart';
import 'package:get/get.dart';

class Controller {
  var count = 0.obs;

  void increment() {
    count++;
  }
}

class TestGetX extends StatelessWidget {
  final controller = Controller();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.all(170),
            child: GetX(builder: (_) => Text('clicks: ${controller.count}')),
          ),
          FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: () => controller.increment(),
          ),
        ],
      ),
    );
  }
}
复制代码

通过导包本身即能够在他类构造实例以访问类中的变量和方法;但如果没有使用GetX框架,方法increment所引起的 count的值变化,将无法传递更新到控件,实现视图状态刷新;除了构造实例创建控制器外,该例子的实现还需要将相应变量声明为 "可观察的" 。

[ 编写步骤 ]

1、在控制器中定义 可观察变量

  • 定义业务逻辑类Controller;
  • 变量count将保存需要更新的数据,因而后缀".obs ";
  • 方法increment能够引起变量count的值变化;
  • 变量count的"可观察"特性,意味着当其值发生变化,变量能够自动响应并更新值变化;

2、将 包含可观察变量的视图控件作为 控件GetX的子控件;

  • 创建视图页面:使用StatelessWidget;
  • 构建响应控件:将目标更新状态或显示变化的控件 传入 控件GetX的属性builder中;

3、使用控制器更新变量的值,观察视图控件的 状态响应和变化;

  • 创建动作控件:在FloatingActionButton的onPressed属性中设置更新触发
  • 每次点击将执行一次controller的方法increment;
  • 当count发生变化时,由builder构建的显示变量count的控件状态将响应更新;

注:使用控件GetX的前提是,声明变量可观察,并非定义继承GetxController及使用Get.put进行注入;

(三)GetxController & GetBuilder

关键词:GetxController、update、Get.put()、GetBuilder()

[ 例:使用别人家的按钮(二) —— 使用GetBuilder构建视图-逻辑分离的响应式控件 ]

使用GetBuilder实现响应控件与使用GetX的差别在于:后者基于变量可观察,而前者则通过加载指定控制器来更新控件状态;

import 'package:get/get.dart';

class Controller extends GetxController {
  var count = 0;

  void increment() {
    count++;
    update();
  }
}
复制代码
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'test_controller.dart';

class TestGetBuilder extends StatelessWidget {
  final controller = Get.put(Controller());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(170.0),
            child: GetBuilder<Controller>(
                builder: (_) => Text(
                      'clicks: ${controller.count}',
                    )),
          ),
          FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: () => controller.increment(),
          ),
        ],
      ),
    );
  }
}
复制代码

通过GetBuilder实现响应式控件,控制器必须继承自GetxController,所定义的目标状态变量之后无需后缀".obs ",但需要定义方法update;并且加载指定控制器,不仅需要使用Get.put进行注入,而且GetBuilder还需要通过指定泛型绑定目标注入的控制器。

[ 编写步骤 ]

  • 定义逻辑控制类Controller,继承自GetxController
    • 定义变量count,保存状态数据
    • 定义方法increment,改变状态数据
    • 在方法increment中,执行方法update,更新数据状态;
  • 使用StatelessWidget创建视图,将目标显示变化的控件作为GetBuilder的构建对象;
    • 通过 Get.put() 加载 Controlle实例,控制器将对当前Widget下的所有子路由可用;
    • 将响应变化的控件传入,泛型指定为Controller的GetBuilder的属性builder中;
    • 创建动作控件FloatingActionButton,在onPressed中设置每次触击将执行controller的方法increment;
    • 当count发生变化,由GetBuilder构建的控件将响应变化并更新状态;

(四)Get put & find

关键词:Get.put、Get.find

[ 例:最直观的跨组件状态通信 —— 在这个页面触发事件,在另一个页面状态变化 ]

我们在例子-使用别人家的按钮(二) 中,已了解到Get x框架中的Get.put可以用来注入非当前类的控制器;使用GetBuilder构建响应式控件,不仅需要通过Get.put进行控制器注入,还要求控制器必须作为GetxController实现类实例,且控件GetBuilder必须指定了泛型才能够真正获取到控制器,实现在当前视图使用另一个类中定义的业务逻辑对控件状态进行变化和更新;

但Get.find使这些步骤得到了魔术般的简化:使用Get.find能够发现并获取到已经通过Get.put注入的控制器,它不仅并不要求控制器来自GetxController的实现类,也无须使用Obx、GetX、GetBuilder等控件 —— 通过Get.find获取的控制器可以直接应用于当前Widget下的所有子路由,实现真正意义上的跨组件状态通信。

import 'package:flutter/material.dart';
import 'package:get/get.dart';

class Controller {
  var count = 0;
}

class FirstPage extends StatelessWidget {
  final controller = Get.put(Controller());

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      child: Text('Next Route'),
      onPressed: () {
        controller.count++;
      },
    );
  }
}

class SecondPage extends StatelessWidget {
  final Controller ctrl = Get.find();

  @override
  Widget build(context) {
    return Scaffold(body: Center(child: Text("${ctrl.count}")));
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(context) => Scaffold(
        body: SlidePage(),
      );
}

// ignore: must_be_immutable
class SlidePage extends StatelessWidget {
  List<Widget> pages = <Widget>[FirstPage(), SecondPage()];

  @override
  Widget build(BuildContext context) {
    return PageView.custom(
      scrollDirection: Axis.vertical,
      childrenDelegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
          return pages[index];
        },
        childCount: 2,
      ),
    );
  }
}
复制代码

使用Get.find的局限在于,它必须应用在两个或两个以上的widget中,若在同一widget中将无法实现视图的状态响应和刷新,这是因为,Get.find的使用前提是:获取已经通过Get.put在另一个widget完成了注入的控制器。

[ 编写步骤 ]

1、定义业务逻辑类Controller

  • 定义变量count,保存状态数据(可以不用".obs "或方法update)
  • Controller可以不必继承GetxController

2、使用 Get.put 注入控制器,通过控制改变count数据

  • 使用StatelessWidget创建界面

  • 创建控制器:在Get.put()构造Controller实例controller,使后者对当下所有子路由可用;

3、使用 Get.find 获取控制器,共享其中的数据状态

  • 调用Get.find从另一个Widget中即页面FirstPage中,发现已注入的控制器;

    Get作为GetInterface子类私有对象的引用,调用父类拓展中的find,等同于一个GetInstance调用find

  • 通过find方法初始化控制器,并将其保存为ctrl;然后通过ctrl共享变量count中的数据状态;

4、实现跨页面的状态联动:创建滑动页面,点击页面FirstPage中的Button,页面SecondPgae中展示的数值将发生变化;

(五)Get.to

[ 例:使用Get.to实现路由跳转 ]

import 'package:flutter/material.dart';
import 'package:get/get.dart';

void main() {
  runApp(GetMyApp());
}

// 使用Get.to来进行页面路由,需要在GetMaterialApp下构建控件树
class GetMyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      home: GetPage(),
      // SlidePage(),
    );
  }
}

// 创建Get式可路由页面
class GetPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 通过RaisedButton按钮onPressed事件触发跳转
    return RaisedButton(
      child: Text('Next Route'),
      onPressed: () {
        // 将目标调整页面作为参数对象传入Get.to;
        Get.to(TargetPage());
      },
    );
  }
}

// 创建目标跳转页
class TargetPage extends StatelessWidget {
  @override
  Widget build(context) {
    return Scaffold(body: Center(child: Text("TargetPage")));
  }
}
复制代码

[ 整合例:使用Get.to + Get.put + Get.find 实现页面跳转计数 ]

import 'package:flutter/material.dart';
import 'package:get/get.dart';

void main() {
  runApp(MyApp());

  // testPrint();
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      home: GetPageTwo(),
      // SlidePage(),
    );
  }
}

class Controller extends GetxController {
  var count = 0;
}

class GetPageTwo extends StatelessWidget {
  final controller = Get.put(Controller());

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      child: Text('Next Route'),
      onPressed: () {
        controller.count++;
        Get.to(Second());
      },
    );
  }
}

class Second extends StatelessWidget {
  final Controller ctrl = Get.find();

  @override
  Widget build(context) {
    return Scaffold(body: Center(child: Text("${ctrl.count}")));
  }
}
复制代码

二、可伸缩应用程序

(一)Bindings

Bindings是拥有可伸缩应用程序的第一步;

Bindings是管理依赖注入的类,将依赖注入抽离出widget树,使视图层更纯粹地用于布置widget控件,使代码更整洁、有条理;

注入在Bindings的控制器,允许任何没有上下文的地方被访问;

打开页面上的Bindings文件,将可以清楚地看将注入页面的内容;

import 'package:get/get.dart';

class HomeBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut<Controller>(() => Controller());
  }
}
复制代码

使用作为私有实例GetInstance的引用Get,能够调用其父类GetInterface拓展中的方法lazyPut

[ 编写步骤 ]

  • 创建一个类HomeBinding,继承Bindings,并重写方法dependencies(Bindings是抽象类,方法dependencies须重写);
  • 在HomeBinding使用Get.lazyPut(),惰性地注入一个Controller实例,注意类型一致;
  • Bindings类的实例可与路由绑定,绑定后所注入的控制器将对所有子路由可用;

(二)GetView

GetView,是具有“ controller”属性的StatelessWidget;

在可伸缩应用程序中使用GetView创建界面;

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'home_controller.dart';

class Home extends GetView<HomeController> {
  @override
  Widget build(context) => Scaffold(
      appBar: AppBar(title: Text("counter")),
      body: Center(
        child: Obx(() => Text("${controller.count}")),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: controller.increment,
      ));
}
复制代码

[ 编写步骤 ]

  • 创建页面类Home,继承自GetView,后者通过指定泛型来确定目标控制器来源;
  • 导入控制器类型文件,在GetView页面中不需要构造控制器,因而也不需要Get.put()和Get.find();
  • 将有数据状态更新的控件构造放到**obx()**中进行,当count发生变化时,obx将自动响应接收新的Text();
  • 设置操作控件FloatingActionButton,在onPressed设置每次触击将调用一次controller的方法increment;

(三)GetPage & GetMaterialApp

将GetMaterialApp设置为widget视图顶端;

控件GetPage可以用来构造一个能够设置具体依赖注入绑定的命名路由页面;

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../home_view.dart';
import '../home_bindings.dart';

void main() {
  runApp(GetMaterialApp(
    initialRoute: '/home',
    getPages: [
      GetPage(name: '/home', page: () => Home(), binding: HomeBinding()),
    ],
  ));
}
复制代码

[ 编写步骤 ]

  • 使用GetMaterialApp代替MaterialApp(记住,尽可能地避免使用StatefulWidget);
  • 在属性getPages中构造路由控件GetPage,命名为'/home',它将跳转至页面Home;
  • 属性initialRoute,将对'/home'路由进行初始化并绑定它的依赖注入为HomeBinding;

(四)构建可伸缩应用程序的基本步骤

1、定义业务逻辑类Controller

import 'package:get/get.dart';

class HomeController extends GetxController {
  var count = 0.obs;

  void increment() => count++;
}
复制代码

2、定义Bindings类,注入依赖

import 'package:get/get.dart';
import 'home_controller.dart';

class HomeBinding implements Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => HomeController());
  }
}
复制代码

3、使用GetView创建界面

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'home_controller.dart';

class Home extends GetView<HomeController> {
  @override
  Widget build(context) => Scaffold(
      appBar: AppBar(title: Text("counter")),
      body: Center(
        child: Obx(() => Text("${controller.count}")),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: controller.increment,
      ));
}
复制代码

4、将GetMaterialApp设置为widget视图顶端;

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../home_view.dart';
import '../home_bindings.dart';

void main() {
  runApp(GetMaterialApp(
    initialRoute: '/home',
    getPages: [
      GetPage(name: '/home', page: () => Home(), binding: HomeBinding()),
    ],
  ));
}
复制代码

附:使用GetX制作的一个计数器

官方文档例

import 'package:flutter/material.dart';
import 'package:get/get.dart';

void main() {
  runApp(GetMaterialApp(
    // It is not mandatory to use named routes, but dynamic urls are interesting.
    initialRoute: '/home',
    defaultTransition: Transition.native,
    translations: MyTranslations(),
    locale: Locale('pt', 'BR'),
    getPages: [
      //Simple GetPage
      GetPage(name: '/home', page: () => First()),
      // GetPage with custom transitions and bindings
      GetPage(
        name: '/second',
        page: () => Second(),
        customTransition: SizeTransitions(),
        binding: SampleBind(),
      ),
      // GetPage with default transitions
      GetPage(
        name: '/third',
        transition: Transition.cupertino,
        page: () => Third(),
      ),
    ],
  ));
}

class MyTranslations extends Translations {
  @override
  Map<String, Map<String, String>> get keys => {
    'en': {
      'title': 'Hello World %s',
    },
    'en_US': {
      'title': 'Hello World from US',
    },
    'pt': {
      'title': 'Olá de Portugal',
    },
    'pt_BR': {
      'title': 'Olá do Brasil',
    },
  };
}

class Controller extends GetxController {
  int count = 0;
  void increment() {
    count++;
    // use update method to update all count variables
    update();
  }
}

class First extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
          icon: Icon(Icons.add),
          onPressed: () {
            Get.snackbar("Hi", "I'm modern snackbar");
          },
        ),
        title: Text("title".trArgs(['John'])),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            GetBuilder<Controller>(
                init: Controller(),
                // You can initialize your controller here the first time. Don't use init in your other GetBuilders of same controller
                builder: (_) => Text(
                  'clicks: ${_.count}',
                )),
            RaisedButton(
              child: Text('Next Route'),
              onPressed: () {
                Get.toNamed('/second');
              },
            ),
            RaisedButton(
              child: Text('Change locale to English'),
              onPressed: () {
                Get.updateLocale(Locale('en', 'UK'));
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
          child: Icon(Icons.add),
          onPressed: () {
            Get.find<Controller>().increment();
          }),
    );
  }
}

class Second extends GetView<ControllerX> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('second Route'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            GetX<ControllerX>(
              // Using bindings you don't need of init: method
              // Using Getx you can take controller instance of "builder: (_)"
              builder: (_) {
                print("count1 rebuild");
                return Text('${_.count1}');
              },
            ),
            GetX<ControllerX>(
              builder: (_) {
                print("count2 rebuild");
                return Text('${controller.count2}');
              },
            ),
            GetX<ControllerX>(builder: (_) {
              print("sum rebuild");
              return Text('${_.sum}');
            }),
            GetX<ControllerX>(
              builder: (_) => Text('Name: ${controller.user.value.name}'),
            ),
            GetX<ControllerX>(
              builder: (_) => Text('Age: ${_.user.value.age}'),
            ),
            RaisedButton(
              child: Text("Go to last page"),
              onPressed: () {
                Get.toNamed('/third', arguments: 'arguments of second');
              },
            ),
            RaisedButton(
              child: Text("Back page and open snackbar"),
              onPressed: () {
                Get.back();
                Get.snackbar(
                  'User 123',
                  'Successfully created',
                );
              },
            ),
            RaisedButton(
              child: Text("Increment"),
              onPressed: () {
                Get.find<ControllerX>().increment();
              },
            ),
            RaisedButton(
              child: Text("Increment"),
              onPressed: () {
                Get.find<ControllerX>().increment2();
              },
            ),
            RaisedButton(
              child: Text("Update name"),
              onPressed: () {
                Get.find<ControllerX>().updateUser();
              },
            ),
            RaisedButton(
              child: Text("Dispose worker"),
              onPressed: () {
                Get.find<ControllerX>().disposeWorker();
              },
            ),
          ],
        ),
      ),
    );
  }
}

class Third extends GetView<ControllerX> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(onPressed: () {
        controller.incrementList();
      }),
      appBar: AppBar(
        title: Text("Third ${Get.arguments}"),
      ),
      body: Center(
          child: Obx(() => ListView.builder(
              itemCount: controller.list.length,
              itemBuilder: (context, index) {
                return Text("${controller.list[index]}");
              }))),
    );
  }
}

class SampleBind extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut<ControllerX>(() => ControllerX());
  }
}

class User {
  User({this.name = 'Name', this.age = 0});
  String name;
  int age;
}

class ControllerX extends GetxController {
  final count1 = 0.obs;
  final count2 = 0.obs;
  final list = [56].obs;
  final user = User().obs;

  updateUser() {
    user.update((value) {
      value.name = 'Jose';
      value.age = 30;
    });
  }

  /// Once the controller has entered memory, onInit will be called.
  /// It is preferable to use onInit instead of class constructors or initState method.
  /// Use onInit to trigger initial events like API searches, listeners registration
  /// or Workers registration.
  /// Workers are event handlers, they do not modify the final result,
  /// but it allows you to listen to an event and trigger customized actions.
  /// Here is an outline of how you can use them:

  /// made this if you need cancel you worker
  Worker _ever;

  @override
  onInit() {
    /// Called every time the variable $_ is changed
    _ever = ever(count1, (_) => print("$_ has been changed (ever)"));

    everAll([count1, count2], (_) => print("$_ has been changed (everAll)"));

    /// Called first time the variable $_ is changed
    once(count1, (_) => print("$_ was changed once (once)"));

    /// Anti DDos - Called every time the user stops typing for 1 second, for example.
    debounce(count1, (_) => print("debouce$_ (debounce)"),
        time: Duration(seconds: 1));

    /// Ignore all changes within 1 second.
    interval(count1, (_) => print("interval $_ (interval)"),
        time: Duration(seconds: 1));
  }

  int get sum => count1.value + count2.value;

  increment() => count1.value++;

  increment2() => count2.value++;

  disposeWorker() {
    _ever.dispose();
    // or _ever();
  }

  incrementList() => list.add(75);
}

class SizeTransitions extends CustomTransition {
  @override
  Widget buildTransition(
      BuildContext context,
      Curve curve,
      Alignment alignment,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
      Widget child) {
    return Align(
      alignment: Alignment.center,
      child: SizeTransition(
        sizeFactor: CurvedAnimation(
          parent: animation,
          curve: curve,
        ),
        child: child,
      ),
    );
  }
}
复制代码

官方学习文档:pub.flutter-io.cn/packages/ge…

编程小白,如有谬误,欢迎指出,由衷感谢!

文章分类
前端
文章标签