在 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;
响应式变量可以用在任何类型上:
代码语言:javascript复制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
即可拿到变量的值。对于 List
、Map
则不需要加 .value
。
String nameValue = name.valuebool isLoggedValue = isLogged.valueint countValue = count.valuedouble numberValue = number.valueString item = items[0] //不需要.valueint value = myMap['key'] //不需要.valueString name = user.value.name
更新数据:
对于基础数据类型,只需要对 value 重新赋值即可更新数据并通过 Obx 刷新界面:
代码语言:javascript复制name.value = "123"isLogged.value = truecount.value = 1number.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秒内无论点击多少次都只有最后一次会触发回调,然后进入下一次的时间间隔。
使用方式:
代码语言:javascript复制///每次`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));
示例
使用响应式变量实现计数器功能:
代码语言:javascript复制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()
来通知数据改变。
实现方式:
代码语言:javascript复制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 实现计数器:
代码语言:javascript复制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 某一个类的时候才会进行初始化。
代码语言:javascript复制///只有当第一次使用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 一样,同样拥有 permanent
和 tag
参数,且作用一样。
Get.create
create
与 put
使用方式上基本类似,不同的是它的 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
方式可以传递任意类型的参数。
在下个页面获取参数:
代码语言:javascript复制dynamic args = Get.arguments;
off
:进入下一个界面,且导航没有返回
Get.off(CounterPage());
offAll
: 进入下一个界面并取消之前的所有路由
Get.offAll(CounterPage());
back
:返回
Get.back();
返回传参:
代码语言:javascript复制Get.back(result: 'success');
获取返回参数:
代码语言:javascript复制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
进行initialRoute
和 getPages
的配置,即初始页面和路由映射集合:
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 传参的方式:
代码语言:javascript复制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()); }}
普通路由使用:
代码语言:javascript复制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 相关功能。插件如图:
安装后在目录右键 -> New
里面就会有 GetX 菜单,选择 GetX
在弹出界面即可快速创建页面模板,插件使用如图:
点击 OK 以后,会在对应目录生成 binding
、 controller
、state
、 view
四个文件,如下图:
文件的命名可以在插件设置里进行设置。
对应文件内容如下:
- 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:
代码语言:javascript复制const zh_CN_res = { 'hello': '你好 世界',};
str_res_en:
代码语言:javascript复制const en_US_res = { 'hello': 'Hello World',};
然后 StringRes 修改如下:
代码语言:javascript复制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
代码语言:javascript复制class SR{ static const hello = 'hello';}
修改语言文字资源配置如下:
代码语言:javascript复制const zh_CN_res = { SR.hello: '你好 世界',};
const en_US_res = { SR.hello: 'Hello World',};
然后使用如下:
代码语言:javascript复制Text(SR.hello.tr);
这样项目就完成了多语言的配置,整体目录如图:
6.GetX 其他功能
snackbar
GetX 提供了方便快捷使用 snackbar
的方法, 使用如下:
Get.snackbar("title", "message");
默认是在上方弹出的,可以使用 snackPosition
修改弹出的位置,效果如图:
除了位置以外,还可以设置很多属性,比如文字颜色、背景颜色等,详细可设置属性如下:
代码语言:javascript复制 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 样式进行显示:
第一种:
代码语言:javascript复制Get.dialog(Widget)
第二种:
代码语言:javascript复制Get.defaultDialog(title: "title", middleText: "this is dialog message");
效果:
除了 title 、middleText 外,还可以设置确定、取消按钮以及对应的回调、圆角、背景颜色等等参数,详细如下:
代码语言:javascript复制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
使用如下:
代码语言:javascript复制Get.bottomSheet(Container( height: 200, color: Colors.white, child: const Center( child: Text("bottomSheet"), ),));
效果:
仔细查看发现无论是 snackbar
、dialog
还是 bottomSheet
都不需要用到 context, 这意味着可以在项目的任何地方进行调用。
如果要取消snackbar
、dialog
、 bottomSheet
可以使用 Get.back()
。
GetUtils
GetX 还提供了很多工具方法,可以使用 GetUtils
调用, 比如判断是否是邮箱,判断文件格式类型等,详细见下图:
除此之外 GetX 还提供了一些扩展方法:
代码语言:javascript复制//检查应用程序在哪个平台上运行。GetPlatform.isAndroidGetPlatform.isIOSGetPlatform.isMacOSGetPlatform.isWindowsGetPlatform.isLinuxGetPlatform.isFuchsia
//检查设备类型GetPlatform.isMobileGetPlatform.isDesktop//所有平台都是独立支持web的!//你可以知道你是否在浏览器内运行。//在Windows、iOS、OSX、Android等系统上。GetPlatform.isWeb
// 相当于.MediaQuery.of(context).size.height,//但不可改变。Get.heightGet.width
// 提供当前上下文。Get.context
// 在你的代码中的任何地方,在前台提供 snackbar/dialog/bottomsheet 的上下文。Get.contextOverlay
// 注意:以下方法是对上下文的扩展。// 因为在你的UI的任何地方都可以访问上下文,你可以在UI代码的任何地方使用它。
// 如果你需要一个可改变的高度/宽度(如桌面或浏览器窗口可以缩放),你将需要使用上下文。context.widthcontext.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:如果宽度大于1200context.responsiveValue<T>()