Flutter应用框架搭建(一)GetX集成及使用详解

36,222 阅读13分钟

在 Flutter 应用开发过程中,状态管理、路由管理在应用框架中扮演着重要角色。目前主流的解决方案有 Google 官方的 Provider,三方的 GetX、Bloc、 fish-redux 等。经过多方实践对比,GetX 脱颖而出。

GetX 是一个轻量且强大的解决方案,拥有高性能的状态管理、智能的依赖注入以及便捷的路由管理。

本文将从零开始手把手教你如何集成 GetX 搭建属于你的 Flutter 应用框架。

0.GetX 集成

添加依赖

pubspec.yaml 文件中添加 GetX 的依赖,如下:

dependencies:
  flutter:
    sdk: flutter
  get: ^4.5.1

初始化 GetX

要使用 GetX 需要对 GetX 进行初始化,将默认的 MaterialApp 替换为 GetMaterialApp 即可,如下:

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomePage(),
    );
  }
}

1.状态管理

GetX 提供了两种响应式状态管理的方法:响应式变量方式和状态管理器方式。

响应式变量

定义

定义一个响应式变量,只需在变量的末尾加上一个 .obs 就可将变量定义为响应式变量:

var count = 0.obs;

响应式变量可以用在任何类型上:

final name = ''.obs;
final isLogged = false.obs;
final count = 0.obs;
final balance = 0.0.obs;
final number = 0.obs;
final items = <String>[].obs;
final myMap = <String, int>{}.obs;

// 自定义类 - 可以是任何类
final user = User().obs;
获取响应式变量的值

使用的时候调用 value 即可拿到变量的值。对于 ListMap 则不需要加 .value

String nameValue = name.value
bool isLoggedValue = isLogged.value
int countValue = count.value
double numberValue = number.value
String item = items[0] //不需要.value
int value = myMap['key'] //不需要.value
String name = user.value.name
更新数据:

对于基础数据类型,只需要对 value 重新赋值即可更新数据并通过 Obx 刷新界面:

name.value = "123"
isLogged.value = true
count.value = 1
number.value = 12.0

对于其他数据类型需要调用 update 或者变量方法更新,如下:

user.update((value) {
  value?.name = "123";
});

或者使用变量名方法重新赋值一个对象,比如变量名为 user 则可使用 user() 方法进行更新:

user(User(name: "abcd", age: 25));
刷新界面

在界面上使用响应式变量只需在使用变量的控件上包裹 Obx 即可实现响应式更新,即变量的值发生变化时自动刷新界面:

Obx(() => Text("${count.value}"))
数据变化监听

除了使用 Obx 实现界面数据自动刷新外,GetX 提供了多种手动方式对响应式变量进行数据变化监听,当数据发生变化时执行自定义的逻辑,比如数据变更后重新请求接口等。

  • ever 当数据发生改变时触发
  • everAll 和 "ever "很像,只是监听的是多个响应式变量的变化,当其中一个发生变化就会触发回调
  • once 只在变量第一次被改变时被调用
  • debounce 防抖,即延迟一定时间调用,且在规定时间内只有最后一次改变会触发回调。如设置时间为 1 秒,发生了3次数据变化,每次间隔500毫秒,则只有最后一次变化会触发回调。
  • interval 时间间隔内只有最后一次变化会触发回调。如设置时间间隔为1秒,则在1秒内无论点击多少次都只有最后一次会触发回调,然后进入下一次的时间间隔。

使用方式:

///每次`count`变化时调用。
ever(count, (newValue) => print("$newValue has been changed"));

///只有在变量count在第一次被改变时才会被调用。
once(count, (newValue) => print("$newValue was changed once"));

///防DDos - 每当用户停止输入1秒时调用,例如。
debounce(count, (newValue) => print("debouce$newValue"), time: Duration(seconds: 1));

///忽略1秒内的所有变化,只有最后一次会触发回调。
interval(count, (newValue) => print("interval $newValue"), time: Duration(seconds: 1));
示例

使用响应式变量实现计数器功能:

class CounterPage extends StatelessWidget {
  var count = 0.obs;
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Counter"),
      ),
      body: Center(
        child: Obx(() => Text("${count.value}", style: const TextStyle(fontSize: 50))),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () => count ++,
      ),
    );
  }
}

上述代码就实现了简单的计数器功能,仔细查看发现并没有使用 StatefulWidget 也能实现计数的自动更新。这就是响应式变量的强大之处。

状态管理器

GetX 还提供了使用 Controller 来管理状态,实现一个自定义 Controller 类继承自 GetxController ,Controller 中进行业务逻辑的处理,当需要改变状态数据时调用 update() 来通知数据改变。

实现方式:

class CounterController extends GetxController{
  int count = 0;
  void increment(){
    count ++ ;
    update();
  }
}

在界面中使用时需要使用 GetBuilder 进行包裹,这样使用 Controller 中的数据变化时,调用 update() 后就会刷新界面控件。

GetBuilder<CounterController>(
	init: CounterController(), //Controller 首次初始化
	builder: (controller) {
		return Text("${controller.count}", style: const TextStyle(fontSize: 50));
})

第一次使用某个 Controller 时需要进行初始化,后续再使用同一个 Controller 就不需要再进行初始化,即不需要配置 init。

初始化完成后,可以使用 Get.find() 找到对应的 Controller :

示例

使用 Controller 实现计数器:

class CounterController extends GetxController{
  int count = 0;

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


class CounterPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: const Text("Counter"),
      ),
      body: Center(
        child: Column(
            mainAxisSize:MainAxisSize.min,
          children: [
            
            GetBuilder<CounterController>(
              init: CounterController(), /// 初始化 Controller
              builder: (controller) {
                return Text("${controller.count}", style: const TextStyle(fontSize: 50));
              }),
            
            GetBuilder<CounterController>(  ///没有进行初始化
                builder: (controller) {
                  return Text("${controller.count}", style: const TextStyle(fontSize: 50));
                }),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
          ///使用 find 找到 Controller
          CounterController controller = Get.find();
          ///调用 Controller 方法
          controller.increment();
        },
      ),
    );
  }
}

这里实现了两个显示数字的控件,一个初始化了 CounterController ,另一个则是直接进行了使用。

2.依赖管理

其实上一节已经使用到了 GetX 的依赖管理,在 GetBuilder 里初始化 Controller 后,在其他地方就可以使用 Get.find() 找到对应的 Controller ,这就是 GetX 的依赖管理。GetX 依赖管理可以注入任意类型的实例,并提供了多种依赖插入/注册方式。

插入/注册依赖

Get.put

使用 put 将需要依赖的对象插入到 GetX 中:

Get.put<CounterController>(CounterController());
Get.put<CounterController>(CounterController(), permanent: true);
Get.put<CounterController>(CounterController, tag: "counter");

插入依赖时除了依赖类的实例以外还可以设置额外参数:

  • permanent:是否永久,默认 false 当实例不再使用时会进行销毁,true 则会一直保留
  • tag:标签,用于区分同一个类不同实例。
Get.lazyPut

延迟初始化,在需要用到的时候才会初始化实例对象,即第一次 find 某一个类的时候才会进行初始化。

///只有当第一次使用Get.find<CounterController>时,CounterController才会被调用。
Get.lazyPut<CounterController>(() => CounterController());

Get.lazyPut<CounterController>(
  () {
    // ... some logic if needed
    return CounterController();
  },
  tag: Math.random().toString(),
  fenix: true
)

lazyPut 同样有额外参数,跟 put 基本相同。

  • fenix:类似'永久',不同的是,当不使用时,实例会被丢弃,但当再次需要使用时,Get会重新创建实例

  • tag:标签,用于区分同一个类不同实例。

Get.putAsync

putAsync 可以异步注册一个实例。用于某些实例需要异步初始化时使用,比如 SharedPreferences:

Get.putAsync<SharedPreferences>(() async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setInt('counter', 12345);
  return prefs;
});

跟 put 一样,同样拥有 permanenttag 参数,且作用一样。

Get.create

createput 使用方式上基本类似,不同的是它的 permanent 默认为 true。

Get.create<CounterController>(() => CounterController());

使用

通过 find() 方法获取依赖的实例:

final controller = Get.find<CounterController>();
// 或者
CounterController controller = Get.find();

///通过 tag 获取
final controller = Get.find<CounterController>("counter");

也可以通过 delete() 方法来手动移除注入的依赖实例,大部分情况下不需要手动调用该方法,GetX 内部会自动处理,当不需要时自动移除

Get.delete<CounterController>();

3.路由管理

路由也是 Flutter 项目重要的一环,在 Flutter 中进行页面跳转就是通过路由实现,GetX 提供了 普通路由别名路由

普通路由

  • to:进入下一个界面
Get.to(CounterPage());

使用 arguments 进行参数传递:

Get.to(CounterPage(), arguments: count);

使用 arguments 方式可以传递任意类型的参数。

在下个页面获取参数:

dynamic args = Get.arguments;
  • off:进入下一个界面,且导航没有返回
Get.off(CounterPage());
  • offAll: 进入下一个界面并取消之前的所有路由
Get.offAll(CounterPage());
  • back:返回
Get.back();

返回传参:

Get.back(result: 'success');

获取返回参数:

var data = await Get.to(CounterPage());

别名路由

首先创建一个 RouteGet(名字自己定义) 的类,用于统一配置路由映射关系:

class RouteGet {
  /// page name
  static const String counter = "/counter";

  ///pages map
  static final List<GetPage> getPages = [
    GetPage(
        name: counter, 
        page: () => CounterPage(), 
    )
  ];
}

GetPage 定义别名与页面的映射关系。

然后在 GetMaterialApp 进行initialRoutegetPages 的配置,即初始页面和路由映射集合:

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'Flutter Demo',
      initialRoute: RouteGet.counter,
      getPages: RouteGet.getPages,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      )
    );
  }
}
跳转及参数的传递

使用方法与普通路由基本相同,只是方法上多了 Named

  • 路由跳转:
Get.toNamed(RouteGet.login);
  • 路由传参:
Get.toNamed(RouteGet.login, arguments: {"name":"aaaa"});

也可以直接在路由别名后面跟参数,类似于 Url get 传参的方式:

Get.toNamed("/NextScreen?device=phone&id=354&name=Enzo");
  • 接收参数:

    通过 arguments 进行传参,在下个页面接收参数直接使用 Get.arguments 获取到传递过来的参数:

dynamic args = Get.arguments;

使用别名后 Url 传递参数的方式,使用 Get.parameters 获取参数:

Get.parameters['device']

Bindings

Bindings 主要是配合路由进行使用,当通过 GetX 路由进入页面时,会自动调用 dependencies 方法, 可以在这里进行依赖关系的注册等。

class CounterBinding implements Bindings {
  @override
  void dependencies() {
    Get.lazyPut<CounterController>(() => CounterController());
    Get.put<Service>(()=> Api());
  }
}

普通路由使用:

Get.to(CounterPage(), binding: CounterBinding());

别名路由使用,在 GetPage 中设置路由对应的 Bindings

 ///pages map
  static final List<GetPage> getPages = [
    GetPage(
        name: counter, 
        page: () => CounterPage(), 
      	binding: CounterBinding() ///设置Binding
    )
  ];

然后使用别名路由的方式不变

更多路由相关操作请查看官方文档:route_management

至此,GetX 的集成和主要功能:状态管理、依赖管理、路由管理的使用都已经实现了,可以开始项目开发了。

4.GetX 插件的使用

为了在项目中方便使用 GetX ,可以选择安装 GetX 插件,使用 GetX 可以快速创建 GetX 的页面模板,并且可以通过快捷键快速使用 GetX 相关功能。插件如图:

image.png

安装后在目录右键 -> New 里面就会有 GetX 菜单,选择 GetX 在弹出界面即可快速创建页面模板,插件使用如图:

image-20211207191557982.png

点击 OK 以后,会在对应目录生成 bindingcontrollerstateview 四个文件,如下图:

image.png

文件的命名可以在插件设置里进行设置。

对应文件内容如下:

  • **binding:**用于懒加载对应的Controller
class CounterBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => CounterController());
  }
}
  • controller: 编写界面业务逻辑代码,包含生命周期回调函数
class CounterController extends GetxController {
  final CounterState state = CounterState();

  @override
  void onReady() {
    // TODO: implement onReady
    super.onReady();
  }

  @override
  void onClose() {
    // TODO: implement onClose
    super.onClose();
  }
}
  • state: 存放界面状态数据
class CounterState {
  CounterState() {
    ///Initialize variables
  }
}
  • view: 界面控件,主要进行界面开发
class CounterPage extends StatelessWidget {
  final controller = Get.find<CounterController>();
  final state = Get.find<CounterController>().state;

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

这里使用的 Bindings 的方式,会自动在 Binding 里注册 Controller , 并且在 Page 里生成 find Controller 的代码。

除此以外这里用到了 state ,是为了当页面状态数据过多的时候可以将所有状态数据单独抽取放在 state 里更方便维护,避免 Controller 过于臃肿。

更多关于 GetX 插件的使用详见插件作者的文章介绍:GetX代码生成IDEA插件,超详细功能讲解(透过现象看本质)

5. 国际化

GetX 提供了多语言国际化的处理,方便在项目中进行多语言的管理和切换。

  • 首先创建一个语言资源类继承自 GetX 的 Translations, 实现 get keys:
class StringRes extends Translations{

  @override
  Map<String, Map<String, String>> get keys => {
    'zh_CN': {
      'hello': '你好 世界'
    },
    'en_US':{
      'hello': 'Hello World'
    }
  };
}

在 keys 返回一个多语言的配置,key 为语言标识,格式为[国家]_[语言],value 是一个 Map,存放的就是我们实际的文字资源。

  • 然后在 GetMaterialApp 中进行配置:
GetMaterialApp(
      translations: StringRes(),
      locale: const Locale('zh', 'CN'),
  		fallbackLocale: Locale('en', 'US')
  		...
    );

translations 传入的就是我们定义好的继承自 Translations 的类的对象,locale 则是我们默认使用的语言,fallbackLocale 则是当我们默认语言资源没有时就会使用 fallbackLocale 配置的资源。

locale 也可以使用获取系统语言环境:

import 'dart:ui' as ui;

return GetMaterialApp(
    locale: ui.window.locale,
);
  • 使用

使用的时候直接使用对应资源的 key.str 即可,如下:

Text('hello'.tr);
  • 更改语言

使用 Get.updateLocale 更改语言:

Get.updateLocale(const Locale('en', 'US'));
优化

经过上面的配置,项目就实现了多语言,并且可以对其进行切换,但是发现如果把所有语言都写到一个文件里内容太多了也不好管理,所以可以将对应的语言资源进行拆分,每个语言一个文件:

str_res_zh.dart:

const zh_CN_res = {
  'hello': '你好 世界',
};

str_res_en:

const en_US_res = {
  'hello': 'Hello World',
};

然后 StringRes 修改如下:

class StringRes extends Translations{
  @override
  Map<String, Map<String, String>> get keys => {
    'zh_CN': zh_CN_res,
    'en_US':en_US_res
  };
}

这样就更方便管理。但是还有一个问题,使用的时候需要每次都使用 'hello'.tr ,这种手动的方式很不友好,没有提示且可能会写错,因此可以再进行优化一下,像 Android 中使用 String 资源那样,定义一个专门存放字符串 key 的类:

str_res_keys.dart

class SR{
  static const hello = 'hello';
}

修改语言文字资源配置如下:

const zh_CN_res = {
  SR.hello: '你好 世界',
};

const en_US_res = {
  SR.hello: 'Hello World',
};

然后使用如下:

Text(SR.hello.tr);

这样项目就完成了多语言的配置,整体目录如图:

image-20211208100341039.png

6.GetX 其他功能

snackbar

GetX 提供了方便快捷使用 snackbar 的方法, 使用如下:

Get.snackbar("title", "message");

默认是在上方弹出的,可以使用 snackPosition 修改弹出的位置,效果如图:

image-20211208100737268.png

除了位置以外,还可以设置很多属性,比如文字颜色、背景颜色等,详细可设置属性如下:


    String title,
    String message, {
    Color? colorText,
    Duration? duration = const Duration(seconds: 3),

    /// with instantInit = false you can put snackbar on initState
    bool instantInit = true,
    SnackPosition? snackPosition,
    Widget? titleText,
    Widget? messageText,
    Widget? icon,
    bool? shouldIconPulse,
    double? maxWidth,
    EdgeInsets? margin,
    EdgeInsets? padding,
    double? borderRadius,
    Color? borderColor,
    double? borderWidth,
    Color? backgroundColor,
    Color? leftBarIndicatorColor,
    List<BoxShadow>? boxShadows,
    Gradient? backgroundGradient,
    TextButton? mainButton,
    OnTap? onTap,
    bool? isDismissible,
    bool? showProgressIndicator,
    DismissDirection? dismissDirection,
    AnimationController? progressIndicatorController,
    Color? progressIndicatorBackgroundColor,
    Animation<Color>? progressIndicatorValueColor,
    SnackStyle? snackStyle,
    Curve? forwardAnimationCurve,
    Curve? reverseAnimationCurve,
    Duration? animationDuration,
    double? barBlur,
    double? overlayBlur,
    SnackbarStatusCallback? snackbarStatus,
    Color? overlayColor,
    Form? userInputForm,
  }

可以根据自己的需求设置。

dialog

GetX 提供了 dialog 的快捷使用,提供了两种方式,第一种是传入 dialog 显示的 Widget 进行显示,第二种是使用 GetX 默认提供的 dialog 样式进行显示:

第一种:

Get.dialog(Widget)

第二种:

Get.defaultDialog(title: "title", middleText: "this is dialog message");

效果:

image-20211208101642863.png

除了 title 、middleText 外,还可以设置确定、取消按钮以及对应的回调、圆角、背景颜色等等参数,详细如下:

Future<T?> defaultDialog<T>({
    String title = "Alert",
    EdgeInsetsGeometry? titlePadding,
    TextStyle? titleStyle,
    Widget? content,
    EdgeInsetsGeometry? contentPadding,
    VoidCallback? onConfirm,
    VoidCallback? onCancel,
    VoidCallback? onCustom,
    Color? cancelTextColor,
    Color? confirmTextColor,
    String? textConfirm,
    String? textCancel,
    String? textCustom,
    Widget? confirm,
    Widget? cancel,
    Widget? custom,
    Color? backgroundColor,
    bool barrierDismissible = true,
    Color? buttonColor,
    String middleText = "Dialog made in 3 lines of code",
    TextStyle? middleTextStyle,
    double radius = 20.0,
    //   ThemeData themeData,
    List<Widget>? actions,

    // onWillPop Scope
    WillPopCallback? onWillPop,

    // the navigator used to push the dialog
    GlobalKey<NavigatorState>? navigatorKey,
  })

bottomSheet

使用如下:

Get.bottomSheet(Container(
  height: 200,
  color: Colors.white,
  child: const Center(
    child: Text("bottomSheet"),
  ),
));

效果:

image-20211208101930707.png

仔细查看发现无论是 snackbardialog 还是 bottomSheet 都不需要用到 context, 这意味着可以在项目的任何地方进行调用。

如果要取消snackbardialogbottomSheet 可以使用 Get.back()

GetUtils

GetX 还提供了很多工具方法,可以使用 GetUtils 调用, 比如判断是否是邮箱,判断文件格式类型等,详细见下图:

image-20211208103111869.png

除此之外 GetX 还提供了一些扩展方法:

//检查应用程序在哪个平台上运行。
GetPlatform.isAndroid
GetPlatform.isIOS
GetPlatform.isMacOS
GetPlatform.isWindows
GetPlatform.isLinux
GetPlatform.isFuchsia

//检查设备类型
GetPlatform.isMobile
GetPlatform.isDesktop
//所有平台都是独立支持web的!
//你可以知道你是否在浏览器内运行。
//在Windows、iOS、OSX、Android等系统上。
GetPlatform.isWeb


// 相当于.MediaQuery.of(context).size.height,
//但不可改变。
Get.height
Get.width

// 提供当前上下文。
Get.context

// 在你的代码中的任何地方,在前台提供 snackbar/dialog/bottomsheet 的上下文。
Get.contextOverlay

// 注意:以下方法是对上下文的扩展。
// 因为在你的UI的任何地方都可以访问上下文,你可以在UI代码的任何地方使用它。

// 如果你需要一个可改变的高度/宽度(如桌面或浏览器窗口可以缩放),你将需要使用上下文。
context.width
context.height

// 让您可以定义一半的页面、三分之一的页面等。
// 对响应式应用很有用。
// 参数: dividedBy (double) 可选 - 默认值:1
// 参数: reducedBy (double) 可选 - 默认值:0。
context.heightTransformer()
context.widthTransformer()

/// 类似于 MediaQuery.of(context).size。
context.mediaQuerySize()

/// 类似于 MediaQuery.of(context).padding。
context.mediaQueryPadding()

/// 类似于 MediaQuery.of(context).viewPadding。
context.mediaQueryViewPadding()

/// 类似于 MediaQuery.of(context).viewInsets。
context.mediaQueryViewInsets()

/// 类似于 MediaQuery.of(context).orientation;
context.orientation()

///检查设备是否处于横向模式
context.isLandscape()

///检查设备是否处于纵向模式。
context.isPortrait()

///类似于MediaQuery.of(context).devicePixelRatio。
context.devicePixelRatio()

///类似于MediaQuery.of(context).textScaleFactor。
context.textScaleFactor()

///查询设备最短边。
context.mediaQueryShortestSide()

///如果宽度大于800,则为真。
context.showNavbar()

///如果最短边小于600p,则为真。
context.isPhone()

///如果最短边大于600p,则为真。
context.isSmallTablet()

///如果最短边大于720p,则为真。
context.isLargeTablet()

///如果当前设备是平板电脑,则为真
context.isTablet()

///根据页面大小返回一个值<T>
///可以给值为:
///watch:如果最短边小于300
///mobile:如果最短边小于600
///tablet:如果最短边(shortestSide)小于1200
///desktop:如果宽度大于1200
context.responsiveValue<T>()

源码:flutter_app_core

更多内容请查阅官方文档:GetX

参考资料:

Flutter 应用框架搭建系列文章: