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状态管理_第1张图片Flutter - GetX状态管理_第2张图片

Flutter - GetX状态管理_第3张图片Flutter - GetX状态管理_第4张图片

Flutter - GetX状态管理_第5张图片Flutter - GetX状态管理_第6张图片

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的计数器,需要如下三步:

步骤一:在你的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 {  
    @override  void 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 {
  @override  Widget 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 {
  @override  void 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 {
  @override  void 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();

  @override  void 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 {
  @override  Widget 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

你可能感兴趣的:(前端,flutter,前端,dart,flutter,getx,状态管理)