学习了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访问、状态管理、路由 & 依赖等,应该是中小型项目该有的东西应有尽有了。
GetX 是 Flutter 上的一个轻量且强大的解决方案:高性能的状态管理、智能的依赖注入和便捷的路由管理。
GetX 有3个基本原则:
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。
通过以下命令行创建初始化项目,用VS code打开项目,现在你的项目就是一个完整的Flutter官方的计数器项目了。
flutter create flutter_getx_boilerplate
复制代码
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请求主入口
复制代码
一般我们的项目中都会加一个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,导出文件。
在上面的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(),
),
];
}
复制代码
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模块,下面我们可以进入我们的处理业务模块了。
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}"));
复制代码
看到了吗,就是这么容易!
就像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等,还有路由、业务功能、主题、国际化等等模块。最后上源码,欢迎大家提供意见和建议!