前端|Flutter - GetX状态管理

学习了Flutter&Dart也有一段时间了,从开始以为的嵌套地狱,到现在觉得也还不错!似乎没有那么可怕,在我逐渐的熟悉了Flutter以后,学会了开始封装Widget,学会了开始抽象Function,学会了添加Service,慢慢的觉得并不是这么难学,而且还开始喜欢上了Flutter来构建app,因为他方便啊,一套代码Android、IOS、Web端全部搞定,没有不兼容,一切都很丝滑。。。
最近看了Flutter的状态管理框架,flutter_bloc、MobX、GetX,这3个框架用过第一个,MobX没有用过,但是看过ReadMe,会有一堆codegen的代码所以本人不太喜欢也就没有继续深入了,但是当我看到GetX之后,发现这个也太简单了吧,这么容易就搞定了状态管理,那么是不是这样呢,今天我们来做一个以GetX为状态管理的开始项目,截图如下,我们做了Splash页面,然后进入登录&注册,然后进入Home页面。虽然简单,但是涵盖了预定义的文件夹结构、样式主题、API访问、状态管理、路由 & 依赖等,应该是中小型项目该有的东西应有尽有了。
前端|Flutter - GetX状态管理
文章图片
前端|Flutter - GetX状态管理
文章图片

前端|Flutter - GetX状态管理
文章图片
前端|Flutter - GetX状态管理
文章图片

前端|Flutter - GetX状态管理
文章图片
前端|Flutter - GetX状态管理
文章图片

1. GetX是什么?怎么用?

  • GetX 是 Flutter 上的一个轻量且强大的解决方案:高性能的状态管理、智能的依赖注入和便捷的路由管理。
  • GetX 有3个基本原则:
    • 性能: GetX 专注于性能和最小资源消耗。GetX 打包后的apk占用大小和运行时的内存占用与其他状态管理插件不相上下。如果你感兴趣,这里有一个性能测试。
    • 效率: GetX 的语法非常简捷,并保持了极高的性能,能极大缩短你的开发时长。
    • 结构: GetX 可以将界面、逻辑、依赖和路由完全解耦,用起来更清爽,逻辑更清晰,代码更容易维护。
  • GetX 并不臃肿,却很轻量。如果你只使用状态管理,只有状态管理模块会被编译,其他没用到的东西都不会被编译到你的代码中。它拥有众多的功能,但这些功能都在独立的容器中,只有在使用后才会启动。
  • Getx有一个庞大的生态系统,能够在Android、iOS、Web、Mac、Linux、Windows和你的服务器上用同样的代码运行。 通过Get Server 可以在你的后端完全重用你在前端写的代码。
从上面的描述我们知道了什么是GetX,那么GetX这么用呢?也非常简单,我们以Flutter官方的计数器为例子,写一个GetX的计数器,需要如下三步:
【前端|Flutter - GetX状态管理】步骤一:在你的MaterialApp前添加 "Get",将其变成GetMaterialApp。
void main() => runApp(GetMaterialApp(home: Home())); 复制代码

步骤二:创建你的业务逻辑类,并将所有的变量,方法和控制器放在里面。 你可以使用一个简单的".obs "使任何变量成为可观察的。
class Controller extends GetxController{ var count = 0.obs; increment() => count++; } 复制代码

步骤三:创建你的界面,使用StatelessWidget节省一些内存,使用Get你可能不再需要使用StatefulWidget。
class Home extends StatelessWidget {@override Widget build(context) {// 使用Get.put()实例化你的类,使其对当下的所有子路由可用。 final Controller c = Get.put(Controller()); return Scaffold( // 使用Obx(()=>每当改变计数时,就更新Text()。 appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),// 用一个简单的Get.to()即可代替Navigator.push那8行,无需上下文! body: Center(child: ElevatedButton( child: Text("Go to Other"), onPressed: () => Get.to(Other()))), floatingActionButton: FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment)); } }class Other extends StatelessWidget { // 你可以让Get找到一个正在被其他页面使用的Controller,并将它返回给你。 final Controller c = Get.find(); @override Widget build(context){ // 访问更新后的计数变量 return Scaffold(body: Center(child: Text("${c.count}"))); } } 复制代码

看到了吗?非常简洁的实现了官方的计数器项目。GetX介绍完了,我们来进入正题,构建我们的GetX开始项目flutter_getx_boilerplate。
2. 初始化Flutter项目。 通过以下命令行创建初始化项目,用VS code打开项目,现在你的项目就是一个完整的Flutter官方的计数器项目了。
flutter create flutter_getx_boilerplate 复制代码

3.构建项目文件夹结构如下,请仔细阅读每个文件夹及文件的解释。
lib/ |- api - 全局Restful api请求,包括请求拦截器等 |- interceptors - 拦截器,包括auth、request、response拦截 |- api.dart - Restful api导出文件 |- lang - 国际化,包含翻译文件,翻译服务文件等 |- lang.dart - 语言导出文件 |- models - 各种结构化实体类,分为request和response两种类型的实体 |- models.dart - 实体类导出文件 |- modules - 业务模块文件夹 |- auth - 登录&注册模块 |- home - 首页模块 |- splash - splash模块 |- modules.dart - 模块导出文件 |- routes - 路由模块 |- app_pages.dart - 路由页面配置 |- app_routes.dart - 路由名称 |- routes.dart - 路由导出文件 |- Shared - 全局共享文件夹,包括静态变量、全局services、utils、全局Widget等 |- shared.dart - 全局共享导出文件 |- theme - 主题文件夹 |- app_bindings.dart - 在app运行之前启动的服务等,如Restful api |- di.dart - 全局依赖注入对象,如SharedPreferences等 |- main.dart - 导出类,用作外面调用api请求主入口 复制代码

4. 新增Splash模块。 一般我们的项目中都会加一个Splash页面,这个页面的作用类似于欢迎页,在此项目中这个页面的作用是判断当前用户是否登录,如果没有登录则进入登录&注册选择页面,否则直接进入Home页面(Tips,当然我们也可以不用自己写Splash页面,pub上面有一个原生的欢迎页包可以使用,flutter_native_splash)。
Splash模块包含下面4个文件,后面我们的每个模块都会至少包含这几个文件,这个是参考了GetX的示例做了一些自己的习惯改动而成。
|- Splash - Splash模块文件夹 |- splash_binding.dart - Splash依赖绑定文件,也就是这个模块依赖的Controller,Service都可以在这里注入进去。 |- splash_controller.dart - Controller文件主要处理当前模块的业务逻辑,应该把所有的业务逻辑写在这里面,保证UI与业务完全分离。 |- splash_screen.dart - 当前模块的页面UI文件。 |- splash.dart - Splash模块的导出文件,导出这个模块下面的所有文件,方便引用。 复制代码

a. splash_binding.dart,splash模块我们只要依赖Controller,所以利用Get.put加进去即可,这样后面可以通过Get.find()来引入这个Controller。
import 'package:get/get.dart'; import 'splash_controller.dart'; class SplashBinding extends Bindings { @overridevoid dependencies() { Get.put(SplashController()); } } 复制代码

b. splash_controller.dart,Controller通过判断token是否存在来判断是否登录。注意这里的跳转我们用到了Get.toNamed()方法,有没有发现这里不需要context了,是的,GetX并不需要!另外,这里我们额外用了一个delay来模拟一些耗时操作,比如你需要请求后台api拿一些基础数据等。
import 'package:flutter_getx_boilerplate/routes/routes.dart'; import 'package:flutter_getx_boilerplate/shared/shared.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; class SplashController extends GetxController { @override void onReady() async { super.onReady(); await Future.delayed(Duration(milliseconds: 2000)); var storage = Get.find(); try { if (storage.getString(StorageConstants.token) != null) { Get.toNamed(Routes.HOME); } else { Get.toNamed(Routes.AUTH); } } catch (e) { Get.toNamed(Routes.AUTH); } } } 复制代码

c. splash_screen.dart,splash页面我们就用了一个简单的loading。
import 'package:flutter/material.dart'; import 'package:flutter_getx_boilerplate/shared/shared.dart'; class SplashScreen extends StatelessWidget { @overrideWidget build(BuildContext context) { SizeConfig().init(context); return Container( color: Colors.white, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.hourglass_bottom, color: ColorConstants.darkGray, size: 30.0, ), Text( 'loading...', style: TextStyle(fontSize: 30.0), ), ], ), ); } } 复制代码

d. splash.dart,导出当前模块所有文件。
export 'splash_binding.dart'; export 'splash_controller.dart'; export 'splash_screen.dart'; 复制代码

到了这里,splash模块就全部完成,总结一下,我们每新增一个模块,我们需要创建至少4个文件,binding - 绑定依赖,controller - 处理业务逻辑,screen - 页面UI,导出文件。
5. 加入路由Routes模块。 在上面的splash模块controller里面,我们写入了一些跳转逻辑,跳转到登录&注册选择页面还是Home页面。下面我们来定义路由,GetX也包含了路由模块,而且使用起来非常方便,不需要上下文就可以跳转。
a. 导航到下一个页面
Get.toNamed("/NextScreen"); 复制代码

b. 浏览并删除前一个页面
Get.offNamed("/NextScreen"); 复制代码

c. 浏览并删除所有以前的页面
Get.offAllNamed("/NextScreen"); 复制代码

d. 导航并带入参数,然后在screen或者controller中接收参数
Get.toNamed("/NextScreen", arguments: 'Get is the best'); print(Get.arguments); //print out: Get is the best 复制代码

非常的简洁、自然,如果我们用Flutter原生的导航会是下面这样子。
Navigator.pushNamed(context, "/NextScreen", arguments: "Get is the best"); 复制代码

好了,简单的介绍了一下GetX的路由功能,我们定义我们自己的路由模块。
a. app_routes.dart,定义路由名称,我们有根页面(splash),登录&注册选择页面、登录页面、注册页面和home页面。
part of 'app_pages.dart'; abstract class Routes { static const SPLASH = '/'; static const AUTH = '/auth'; static const LOGIN = '/login'; static const REGISTER = '/register'; static const HOME = '/home'; } 复制代码

b. app_pages.dart,定义GetX的路由,我们注意到GetPage以及他所包含的参数,每一个GetPage都是一个路由定义,每一个路由定义包含了name名称、page页面和binding依赖,这样我们就把依赖绑定到指定的路由了,每个路由都会有指定的依赖,当然我们也可以加入global的initialBinding,这个依赖是全局的依赖,我们后面在main入口文件里面会讲到。在Routes.AUTH中,我们还用到了子路由,auth及其子路由我们可以像下面这样访问。
Get.toNamed(Routes.AUTH); // 跳转到登录&注册选择页面 // 注意到,我们这里的参数传入了controller到子路由,因为我们的Routes.Auth主路由和子路由使用了同一个controller。 Get.toNamed(Routes.AUTH + Routes.LOGIN, arguments: controller); // 进入登录页面 Get.toNamed(Routes.AUTH + Routes.REGISTER, arguments: controller); // 进入注册页面 复制代码

app_pages.dart
import 'package:flutter_getx_boilerplate/modules/auth/auth.dart'; import 'package:flutter_getx_boilerplate/modules/home/home.dart'; import 'package:flutter_getx_boilerplate/modules/modules.dart'; import 'package:get/get.dart'; part 'app_routes.dart'; class AppPages { static const INITIAL = Routes.SPLASH; static final routes = [ GetPage( name: Routes.SPLASH, page: () => SplashScreen(), binding: SplashBinding(), ), GetPage( name: Routes.AUTH, page: () => AuthScreen(), binding: AuthBinding(), children: [ GetPage(name: Routes.REGISTER, page: () => RegisterScreen()), GetPage(name: Routes.LOGIN, page: () => LoginScreen()), ], ), GetPage( name: Routes.HOME, page: () => HomeScreen(), binding: HomeBinding(), ), ]; } 复制代码

6. 项目中加入api模块。 Api模块我们使用了免费的Restful api REQ|RES来模拟我们的业务登录、注册和用户信息等。同时我们使用了GetX内置的http模块来构建Api模块,我们添加了provider、repository、inteceptors等,这里因为是GetX模板项目,我们没有按照模块区分api。
a. base_provider.dart,提供拦截器inteceptors的功能,provider可以继承base_provider.dart来初始化拦截器。
import 'package:get/get.dart'; import 'api.dart'; class BaseProvider extends GetConnect { @overridevoid onInit() { httpClient.baseUrl = ApiConstants.baseUrl; httpClient.addAuthenticator(authInterceptor); httpClient.addRequestModifier(requestInterceptor); httpClient.addResponseModifier(responseInterceptor); } } 复制代码

b. inteceptors,我们添加了3种拦截器,auth、request和response拦截器,这里可以对http请求做一些预处理,例如token获取保存,处理异常,request添加headers jwt,请求loading添加等等。
auth_interceptor.dart
import 'dart:async'; import 'package:get/get_connect/http/src/request/request.dart'; FutureOr authInterceptor(request) async { // final token = StorageService.box.pull(StorageItems.accessToken); // request.headers['X-Requested-With'] = 'XMLHttpRequest'; // request.headers['Authorization'] = 'Bearer $token'; return request; } 复制代码

request_interceptor.dart
import 'dart:async'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get_connect/http/src/request/request.dart'; FutureOr requestInterceptor(request) async { // final token = StorageService.box.pull(StorageItems.accessToken); // request.headers['X-Requested-With'] = 'XMLHttpRequest'; // request.headers['Authorization'] = 'Bearer $token'; EasyLoading.show(status: 'loading...'); return request; } 复制代码

response_interceptor.dart
import 'dart:async'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_getx_boilerplate/models/models.dart'; import 'package:flutter_getx_boilerplate/shared/shared.dart'; import 'package:get/get.dart'; import 'package:get/get_connect/http/src/request/request.dart'; FutureOr responseInterceptor( Request request, Response response) async { EasyLoading.dismiss(); if (response.statusCode != 200) { handleErrorStatus(response); return; } return response; }void handleErrorStatus(Response response) { switch (response.statusCode) { case 400: final message = ErrorResponse.fromJson(response.body); CommonWidget.toast(message.error); break; default: } return; } 复制代码

c. api_provider.dart,这里只有Restful api,也可以添加db_provider.dart,cache_provider.dart等。我们这里继承了BaseProvider,这样在第一次调用后天接口之前,我们会添加上述的3个拦截器。
import 'package:flutter_getx_boilerplate/api/base_provider.dart'; import 'package:flutter_getx_boilerplate/models/models.dart'; import 'package:get/get.dart'; class ApiProvider extends BaseProvider { Future login(String path, LoginRequest data) { return post(path, data.toJson()); }Future register(String path, RegisterRequest data) { return post(path, data.toJson()); }Future getUsers(String path) { return get(path); } } 复制代码

d. api_repository.dart,处理数据,这个类中我们只处理成功的请求,失败的都交给了拦截器。
import 'dart:async'; import 'package:flutter_getx_boilerplate/models/models.dart'; import 'package:flutter_getx_boilerplate/models/response/users_response.dart'; import 'api.dart'; class ApiRepository { ApiRepository({required this.apiProvider}); final ApiProvider apiProvider; Future login(LoginRequest data) async { final res = await apiProvider.login('/api/login', data); if (res.statusCode == 200) { return LoginResponse.fromJson(res.body); } }Future register(RegisterRequest data) async { final res = await apiProvider.register('/api/register', data); if (res.statusCode == 200) { return RegisterResponse.fromJson(res.body); } }Future getUsers() async { final res = await apiProvider.getUsers('/api/users?page=1&per_page=12'); if (res.statusCode == 200) { return UsersResponse.fromJson(res.body); } } } 复制代码

好了,有了api模块,下面我们可以进入我们的处理业务模块了。
7. Auth业务模块。 Auth模块模拟用户登录&注册,因页面UI每个项目有所不同,这里就不讲了,如果需要可以自己看源码,这里着重说一下binding和controller,以及页面UI如何使用controller。
a. auth_binding.dart,auth模块依赖了controller,注意到这里controller有一个参数apiRepository,我们直接用了Get.find()方法获取,前面我们讲到需要Get.put()之后才能使用Get.find()拿到对应的实例,这里apiRepository我们是注册了全局的依赖,也就是在main.dart入口文件里面的initialBinding,所以我们才能拿得到apiRepository。
import 'package:get/get.dart'; import 'auth_controller.dart'; class AuthBinding implements Bindings { @overridevoid dependencies() { Get.lazyPut( () => AuthController(apiRepository: Get.find())); } } 复制代码

b. auth_controller.dart,auth模块的controller文件,主要处理校验、登录、注册等业务逻辑,我们把所有的TextEditingController到放到了controller里面,方便我们在controller里面处理业务。
import 'package:flutter/material.dart'; import 'package:flutter_getx_boilerplate/api/api.dart'; import 'package:flutter_getx_boilerplate/models/models.dart'; import 'package:flutter_getx_boilerplate/routes/app_pages.dart'; import 'package:flutter_getx_boilerplate/shared/shared.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; class AuthController extends GetxController { final ApiRepository apiRepository; AuthController({required this.apiRepository}); final registerFormKey = GlobalKey(); final registerEmailController = TextEditingController(); final registerPasswordController = TextEditingController(); final registerConfirmPasswordController = TextEditingController(); bool registerTermsChecked = false; final loginFormKey = GlobalKey(); final loginEmailController = TextEditingController(); final loginPasswordController = TextEditingController(); @overridevoid onReady() { super.onReady(); }void register(BuildContext context) async { AppFocus.unfocus(context); if (registerFormKey.currentState!.validate()) { if (!registerTermsChecked) { CommonWidget.toast('Please check the terms first.'); return; }final res = await apiRepository.register( RegisterRequest( email: registerEmailController.text, password: registerPasswordController.text, ), ); final prefs = Get.find(); if (res!.token.isNotEmpty) { prefs.setString(StorageConstants.token, res.token); print('Go to Home screen>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>'); } } }void login(BuildContext context) async { AppFocus.unfocus(context); if (loginFormKey.currentState!.validate()) { final res = await apiRepository.login( LoginRequest( email: loginEmailController.text, password: loginPasswordController.text, ), ); final prefs = Get.find(); if (res!.token.isNotEmpty) { prefs.setString(StorageConstants.token, res.token); Get.toNamed(Routes.HOME); } } } } 复制代码

c. 页面UI如何使用controller?GetView,Obx。使用GetX提供的这2个类即可实现页面UI与controller的无缝衔接。
GetView是一个无状态的Widget,所以我们可以把GetView当做StatelessWidget使用即可,但是GetView有一个泛型的getter,可以拿到对应的Controller,这样我们就可以在我们的页面UI类中使用controller了。借用GetX官方的一个例子。
class AwesomeController extends GetController { final String title = 'My Awesome View'; }// ALWAYS remember to pass the `Type` you used to register your controller! class AwesomeView extends GetView { @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(20), child: Text(controller.title), // just call `controller.something` ); } } 复制代码

Obx,用了它你的项目里面就不会有StatefulWidget了,因为只要被Obx包裹的内容都可以实时响应controller中的变化。这样你只需要包裹需要变化的内容,没有变化的该怎么写就怎么写。也是借用一下官网的例子。
// This is your count variable: var name = 'Jonatas Borges'; // To make it observable, you just need to add ".obs" to the end of it: var name = 'Jonatas Borges'.obs; // And in the UI, when you want to show that value and update the screen whenever the values changes, simply do this: Obx(() => Text("${controller.name}")); 复制代码

看到了吗,就是这么容易!
8. 主入口模块及其他。 就像GetX官网介绍的一样,我们只需要把MaterialApp替换成GetMaterialApp,大功告成!
前面我们说到了全局依赖注入initialBinding,这样我们就可以全局通过Get.find()来使用这个依赖了。这个全局的依赖就是放到GetMaterialApp的参数里面的,同时GetMaterialApp还提供了initialRoute、smartManagement、locale、translations等等,可以自己摸索。
import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_getx_boilerplate/shared/shared.dart'; import 'package:get/get.dart'; import 'app_binding.dart'; import 'di.dart'; import 'lang/lang.dart'; import 'routes/routes.dart'; import 'theme/theme.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await DenpendencyInjection.init(); runApp(App()); }class App extends StatelessWidget { @overrideWidget build(BuildContext context) { return GetMaterialApp( debugShowCheckedModeBanner: false, enableLog: true, initialRoute: Routes.SPLASH, defaultTransition: Transition.fade, getPages: AppPages.routes, initialBinding: AppBinding(), smartManagement: SmartManagement.keepFactory, title: 'Flutter GetX Boilerplate', theme: ThemeConfig.lightTheme, locale: TranslationService.locale, fallbackLocale: TranslationService.fallbackLocale, translations: TranslationService(), ); } } 复制代码

另外,和我们main主入口文件一起的还有一个di.dart文件,这个文件里面我注册了存储的service,全局本地存储,例如本地存储token、userInfo等信息。
import 'package:get/get.dart'; import 'shared/services/services.dart'; class DenpendencyInjection { static Future init() async { await Get.putAsync(() => StorageService().init()); } } 复制代码

好了,到此就基本介绍完了整个boilerplate项目!总结一下,我们的项目包含了Restful api模块来处理http请求,shared模块添加一些全局使用的utils、constants、services和widgets等,还有路由、业务功能、主题、国际化等等模块。最后上源码,欢迎大家提供意见和建议!
源码:flutter_getx_boilerplate

    推荐阅读