像Android相同开发Flutter项目
我正在参与「启航计划」
前语
省流:运用束缚布局【flutter_constraintlayout】百分之80还原Android布局办法。再运用 GetX 结构 快速完结仿 MVP 的架构。
本文偏新手向,内行假如熟悉这两点其实能够跳过,下面是正文部分。
为什么用Flutter?
没得选,突发事件要新开发一个公司内部项目,时刻紧任务重,20天的时刻从筹备到上线。假如要从加班996 和 Flutter 之间做一个挑选的话,那毫无疑问我选Flutter。
人员配置怎样?
iOS 与 Android 共同开发的,大致三个人左右,都不太会Flutter ,(所以才会有这一篇文章嘛),讲的便是一个怎样从零开始快速开发一个运用并上线。咱们都是边学边写,所以后边假如有讹夺的当地也期望咱们指出哦。
Flutter行不行?
由于事务并不涉及到很杂乱的事务,都是事务逻辑,第三方集成的东西比较少,现在 Flutter 的插件与一些生态也比较完善。咱们乃至都不要以 module 的办法集成 Flutter 模块,直接以 app 的办法悉数用 Flutter 代码完结,乃至都没有写通信桥 Channel 就能完结悉数项目。
究竟 Flutter 仍是一个 UI 结构,咱们展现页面而且调用接口写逻辑仍是很快速的,而涉及到渠道化的东西也有开源的一些插件能够运用,不是一些特殊需求的话 Flutter 完全没问题。
哪些难点?
除了 Dart 语言办法比较好了解,其他都挺难的,主要是 Flutter 的控件思路不习气,不熟练,事务代码与页面Widget混杂在一同看不惯,所以依据这两点痛点,咱们运用了 Flutter 的束缚布局完结页面,运用了 GetX 结构快速完结 MVP 逻辑与页面分离。
那么依据这两点下面就做一些介绍,而且记载一下实践开发中遇到的一些坑与解决方案。
一、Flutter束缚布局
想加一个 margin?套一层 Container 再说,想加 padding? 套一层 Container 再说!哎,不是的 Container 比较杂乱,杀鸡不必牛刀,用 Padding 这个组件就行了…
地狱嵌套没完没了了,反正我刚入门我是受不了的,而且控件太多了我记不住啊。
我太菜了!
说到这儿,许多刚入坑 Flutter 的新手或许或多或少的被安利过,霉事的,咱们都这么嵌套,仅仅比较丑陋一点,功用其实是相同的!
那你真是太单纯了,怎样或许。
在 Flutter 中,嵌套层级更多的布局往往会导致功用上的下降。这是由于每个 Widget 都需求核算其自身的方位和巨细,而且嵌套越深,核算量越大,因而增加了核算时刻。同时,假如涉及到多个布局嵌套,还或许会导致重复核算。
反之,嵌套层级更少的布局往往具有更好的功用。由于不需求进行屡次核算,也不会重复烘托,然后进步运用的呼应速度和流畅度。
所以能不能像 Android 相同搞个束缚布局,削减层级嵌套?然后提高功用呢?哎,你还甭说还真有大佬做出来了。【传送门】 我信任他之前必定是一个Android仔,太相似了。
接下来咱们以下规划图为例:
1.1 常用的几种束缚与Android的异同
首要作为一个 Android 开发者,束缚布局必定是小ks拉,就算特点字段和用的办法有点差异,可是思维仍是相同的,当咱们看完文档之后就能学个7788了。这也不是很难嘛,立马用起来。
集成插件,界说布局,加入束缚。哟?怎样整个页面都变红了?哪报错了?也没有过错信息啊!
一学就会,一用就废,这TMD是个坑吧!
no,no,no。假如有过错的话大概率是没有理清楚下面这几点,快对号入座。
一定要依靠一个纵向点,一个横向点, 例如:
MyTextView(
controller.calateSalary(),
textColor: ColorConstants.appBlue,
isFontBold: true,
fontSize: 16,
).applyConstraint(
id: jobSalary,
top: jobTitle.bottom,
left: parent.left,
)
假如你只依靠了一个方向,那么编译报错,你或许都找不出哪里报错,报什么错。
为了便利,作者乃至供给了快速依靠点:
MyTextView(
controller.jobData?.title ?? "-",
textColor: ColorConstants.black323843,
isFontMedium: true,
fontSize: 17,
).applyConstraint(
id: jobTitle,
topLeftTo: parent,
)
其他的一些快速依靠点如下:
能够左右束缚,能够上下束缚,可是不要相互束缚
MyImageView(
Assets.jobManagePostSuccessCouponIcon,
height: 70,
width: 164,
).applyConstraint(
id: imgBg,
top: parent.top,
horizontalBias: 0.14,
bottom: parent.bottom,
left: parent.left,
right: parent.right,
)
比方这样,上下依靠左右依靠之后它自身是固定宽高的,那么它便是居中展现的,这与Android的束缚布局没差异。
可是两个控件之间相互束缚,一同水平居中,就会报错,由于它不支撑链,所以不知道怎样一同水平居中(这是Row线性布局天然支撑的)
MyTextView(
"20000",
textColor: Colors.white,
fontSize: 24,
).applyConstraint(
id: one,
top: imgBg.top,
bottom: imgBg.bottom,
right: imgBg.right.margin(22),
left: two.right
),
MyTextView(
"20000 Coins",
textColor: ColorConstants.black33,
fontSize: 19,
isFontBold: true,
).applyConstraint(
id: two,
top: imgBg.top,
left: imgBg.right.margin(13),
right: one.left
),
例如咱们两个 TextView 相互束缚了,啊,你在我左面,我在你右边,咱们一同水平居中,错!
宽高束缚中matchConstraint,matchParent,wrapContent怎样用?
matchParent,wrapContent 这可太亲切了,字面意思,和Android相同的,充溢父布局,包裹内容。这都很好了解,可是 matchConstraint 是什么?哎呀。便是Android中的 0 。
在Android中咱们用束缚布局束缚份额,不也是要确定一个方向,让另一个方向设置为0,然后生成对应的宽高吗?
这儿也是相同的,仅仅它识别 0 这个特点,所以咱们需求手动的设置 matchConstraint ,其实意思便是我不知道这个控件的详细宽高,你帮我生成!
Container(
height: 0.5,
color: ColorConstants.dividerItem,
).applyConstraint(
width: matchConstraint,
left: parent.left.margin(15),
right: parent.right.margin(15),
top: tvYyCoins.bottom,
),
例如这样的布局,不是和 Android 相同的吗?假如宽度有值,左右都束缚了,那么便是居中了,假如宽度为 0 ,左右都束缚了,那便是充溢父布局嘛。
MyAssetImage(
Assets.homeMainTipBg,
).applyConstraint(
id: imgBG,
width: matchParent,
height: matchConstraint,
widthHeightRatio: 375 / 263,
top: parent.top,
left: parent.left,
)
比方我要宽度充溢父布局,高度和宽度的份额为375:263,那么我把高度设置为 matchConstraint (相当于Android的0),那么这样才干收效成功。
能够占位躲藏,不占位躲藏,不损坏树形结构
同样的,许多刚入坑 Flutter 的新手或许或多或少的被安利过这一点,Flutter你不需求的布局就能够不界说,运用if else 当需求的时分加进来,不要的是不加进来。这样树形图会少一层或少一点控件,是优化点。
这个嘛,对也不对,当静态布局比方下图所示的按钮组,假如对方现已接纳了,那么只显现按钮接纳的按钮,假如对方现已拒绝了显现拒绝的按钮,假如都没有则显现两个按钮。
对于这样的静态页面,咱们是能够用 if else 来操控挑选运用哪一种控件,可是假如换一种角色,假如你是用户,那么你这个页面便是动态的,你需求点击按钮来触发当时按钮组的显现状况。比方用户点击赞同,那么需求改写当时的按钮组。此时假如用不占位躲藏的控件会更好。由于视图树假如发生了结构性的改动会重构更损耗功用。
所以也引荐咱们按情况来操作,而Flutter的束缚布局就供给了完美的占位躲藏与不占位躲藏的功用,也保留的 Android 的躲藏 margin 功用呢。
MyTextView(
'YY Coins'.tr,
textColor: ColorConstants.white,
fontSize: 16,
isFontMedium: true,
).applyConstraint(
id: cointext,
top: parent.top.margin(16),
left: parent.left.margin(26),
visibility: CLVisibility.gone,
goneMargin: EdgeInsets.only(left: 26)
),
这样是不是更便利呢?避免了还要嵌套 Offstage / Visibility / Padding 等控件的嵌套。所以本质上束缚布局便是削减嵌套罢了。
1.2 开发中更引荐的布局办法
上图首页的页面布局,假如是用传统 Flutter 的 Widget 来做的话,Stack + Column + Row 内部再加上无数的 Padding ,假如要居中还要加 Center ,假如要装饰还需求 DecoratedBox 或 Container …
假如全体布局用 ConstraintLayout 来做那么能够削减许多层级,全体页面是Column ,子布局上部分是 ConstraintLayout 下部分用 Row 即可。
能够说除了简略的线性布局,其他的杂乱容器或 Stack 之类的布局都能够用 ConstraintLayout 来替代,同时束缚布局也特别合适一些份额布局百分比布局的办法。
为什么不把线性布局也换成束缚布局?
没必要,由于这个束缚布局并不支撑链的概念,而原生的线性布局天然支撑链和权重的概念,能够很便利的设置剩余控件巨细和排队对齐办法,这一点实践开发中非常重要。
而 ConstraintLayout 更重要的替代 Stack ,百分比布局,等份额布局,削减层级嵌套用的。
那么咱们需求掌握的一些常用的一些控件便是几个线性布局 Column ,Row 。一些列表ListView ,Sliver 。一些根本显现单元,例如文本与图片的展现之类的。
其实这样就现已满意能开发一款运用了,一些不太常见的控件,等用到去查文档就行了。
二、MVP架构GetX
只需是了解 Flutter 的应该没有人不知道 GetX 的大名吧,No.1的存在不需求我介绍了吧。
先等等,给先自己叠个甲。
我知道 GetX 结构其实在网上是有争议的,爱它的人xxx,恨它的人xxx。
萝卜青菜各有所爱,仅仅一个东西罢了,无所谓了。可是咱们想要快速开发的话还真得靠它,的确简化了许多开发场景,对小白比较友好,主要是状况办理与依靠注入太香了,反正咱们都还蛮喜爱的
根本的运用,引荐咱们能够自己看文档学习,现已很完善了。
这儿也并不涉及到原理和根本的一些运用,仅仅讲一下实践开发中需求留意的当地与坑点。
2.1 页面的生命周期办理与路由办理
咱们的 StatefulWidget 中 State 有 initState(){} 与 dispose(){ } 的生命周期,太少了,好就算够用,那 StatelessWidget 呢?
所以才引出 GetX 的 GetController ,有点相似 ViewModel 的意思,它和页面同生命周期,可是却没有 ViewModel 的耐久化特性,能够称为芳华版ViewModel。
Controller中常用的生命周期,页面创立,页面预备,页面毁掉。
@override
void onInit() {
super.onInit();
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
super.onClose();
}
那么当页面封闭的时分真的能正常毁掉 Controller 吗?真的只能正常调用 onClose() 吗?
这… 还用想吗?当然是能够的啦。这不废话吗?可是咱们需求留意的是,假如咱们运用 GetX 结构中路由界说运用到 binding 的时分:
GetPage(
name: RouterPath.AUTH_LOGIN,
page: () => LoginPage(),
binding: LoginBinding(),
),
class LoginBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => LoginController());
}
}
可是 LoginPage 页面又不是承继与 GetView 或许内部并没有运用 Get.put(),或 Get.find() 或内部视图运用 GetBuilder 等初始化办法,那么 Controller 是不会初始化也不会走 onclose() 办法的哦。
所以最便利的便是搭配 GetView 运用,或许不运用 binding 的办法懒加载生成 Controller ,而是自己手动写 Get.put()。
就算我都按要求做了,生命周期就能主动调用了吗?能走到 onclose 办法了吗? No,No,仍是有坑,当咱们跨页面毁掉页面的时分,例如Home->ConcatUs->Feedback。
当咱们Feedback点击封闭的按钮,直接回到Home页面。运用Get路由跳转如下:
Get.offNamedUntil(RouterPath.HOME, (route) => true);
没毛病,的确回到Home页面了,可是中心的ConcatUs页面的Controller能毁掉吗?能走到onClose办法吗?我试过了,不能!咱们有爱好试试。
假如我一向不能毁掉万一我再其中有资源的开支无法毁掉不是一向占用内存吗?那怎样办?
运用原生的导航封闭
Navigator.popUntil(context, ModalRoute.withName(RouterPath.HOME));
这样原生导航封闭多个页面,GetX结构能知道吗?能做出处理吗?
能的,仅仅需求做一下小小的监听:
main函数界说:
child: GetMaterialApp(
//顶部是否展现Debug图标
debugShowCheckedModeBanner: true,
//是否展现Log
enableLog: true,
//对原生导航的兼容
navigatorObservers: [
GetXRouterObserver(),
FlutterSmartDialog.observer
],
这样就能感知路由的封闭了。
/// 手动让getx感知原生路由
class GetXRouterObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
RouterReportManager.reportCurrentRoute(route);
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) async {
RouterReportManager.reportRouteDispose(route);
}
}
这样中心的页面才干正常的走生命周期了。当然只需封闭多个页面的时分才会遇到这样的问题,也不知道是不是我运用的办法不对,假如有更好的办法期望点拨。
今日又试了一次,运用GetX的办法封闭多个页面,不会触发onClose,也不会收回Controller:
同样的代码用原生路由封闭多个页面就会触发:
2.2 为什么不必MVP不必MVVM?
现在Android开发的主流都是 MVVM 了,你咋用 MVP 这种老办法?
由于 GetX 的 Controller 并不是 Android 中相似 ViewModel 的存在,它并不是与页面绑定的,也并不会保存相关的实例,是的,你想到了什么?
无法保存实例啊,当页面封闭之后重开,那么状况丢掉只能重新走生命周期,重建页面啊。网上给出的解决方案是把数据保存到 GetStorage 或 SP 中…太傻了。
比方:
class MyStatefulPage extends StatefulWidget {
@override
_MyStatefulPageState createState() => _MyStatefulPageState();
}
class _MyStatefulPageState extends State<MyStatefulPage> {
String _myText = '';
@override
void didChangeDependencies() {
// 在此处进行初始化操作
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('My Stateful Page')),
body: Center(child: Text(_myText)),
);
}
@override
void reassemble() {
// 在开发阶段进行快速重建UI
super.reassemble();
}
@override
void didUpdateWidget(MyStatefulPage oldWidget) {
// 在组件更新时保存需求耐久化的状况变量
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
// 在组件被毁掉时开释资源和撤销订阅等操作
super.dispose();
}
}
需求自己完结保存 _myText 字段到本地存储或其他耐久化中,这…
不知道有没有相似 onSaveInstanceState 和 onRestoreInstanceState ,或许一步到位能达到ViewModel作用的办法,也期望大佬点拨。
2.3 同一个页面的多开导致的无法收回问题
还有另一个坑,假如重复进同一个页面怎样办?页面对应的 Controller 究竟会不会收回?怎样确保 Controller 与多开的同一个页面逐个对应,而且确保对应的逐个收回?
不会收回,默许也不会开多个示例,需求咱们手动的Get.Put各自的 Controller ,而且自带各自的Tag,收回的时分也自行毁掉自己的 Controller,属实也是全手动了,不便利。
class FeedbackPage extends StatefulWidget {
@override
_FeedbackPageState createState() => _FeedbackPageState();
}
class _FeedbackPageState extends State<FeedbackPage> {
final FeedbackController controller = Get.put(FeedbackController());
@override
Widget build(BuildContext context) {
return Container();
}
@override
void dispose() {
Get.delete<FeedbackController>();
super.dispose();
}
}
是的,假如 Controller 不能手动毁掉,咱们在 StatefulWidget 中手动毁掉不就行了吗?可是咱们运用 GetX 结构不便是想尽量写 StatelessWidget 嘛,够简略。
所以咱们就需求封装为一个 Widget 来运用
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class GetBindWidget extends StatefulWidget {
const GetBindWidget({
Key? key,
this.bind,
this.tag,
this.binds,
this.tags,
required this.child,
}) : assert(
binds == null || tags == null || binds.length == tags.length,
'The binds and tags arrays length should be equal\n'
'and the elements in the two arrays correspond one-to-one',
),
super(key: key);
final GetxController? bind;
final String? tag;
final List<GetxController>? binds;
final List<String>? tags;
final Widget child;
@override
_GetBindWidgetState createState() => _GetBindWidgetState();
}
class _GetBindWidgetState extends State<GetBindWidget> {
@override
Widget build(BuildContext context) {
return widget.child;
}
@override
void dispose() {
_closeGetXController();
_closeGetXControllers();
super.dispose();
}
void _closeGetXController() {
if (widget.bind == null) {
return;
}
var key = widget.bind.runtimeType.toString() + (widget.tag ?? '');
GetInstance().delete(key: key);
}
void _closeGetXControllers() {
if (widget.binds == null) {
return;
}
for (var i = 0; i < widget.binds!.length; i++) {
var type = widget.binds![i].runtimeType.toString();
if (widget.tags == null) {
GetInstance().delete(key: type);
} else {
var key = type + (widget.tags?[i] ?? '');
GetInstance().delete(key: key);
}
}
}
}
运用的时分就能够指定bind的Controller目标,或许绑定的tags去毁掉:
/// 收回多个
class TestPage extends StatelessWidget {
final oneController = Get.put(OneController(), tag: 'one');
final twoController = Get.put(TwoController());
final threeController = Get.put(ThreeController(), tag: 'three');
@override
Widget build(BuildContext context) {
return GetBindWidget(
binds: [oneController, twoController, threeController],
tags: ['one', '', 'three'],
child: Container(),
);
}
}
跳转的时分咱们直接跳转,答应多个页面实例:
Get.toNamed('xxx', preventDuplicates: false);
2.4 呼应式布局改写的办法
GetX 的呼应式改写有两种办法,一种是Obx是配合Rx呼应式变量运用,另一种是 GetBuilder。
前者Obx主动改写,而后者需求运用 update 手动调用改写。
哪种更好?
GetBuilder 加上 update 办法半主动的办法更好,GetBuilder内部实践上是对 StatefulWidget 的封装,所以占用资源极小,而Obx的话随着变量越来越多,会生成生成大量的 GetStream ,对内存不友好。
那一个页面需求多个GetBuilder吗?
这个看情况,能够根视图包裹,也能够需求更新的控件包裹,假如您的运用程序中有多个 GetBuilder 与同一 Controller 相关联,则调用 update 办法将更新一切这些 GetBuilder 。这是由于每个 GetBuilder 都会在其所属的子树中坚持引证到相同的 Controller 实例,因而当您调用该操控器上的 update 办法时,它将通知一切订阅者并更新整个子树。
假如不想悉数改写,咱们能够给包裹的 GetBuilder 打上 tag 特点,咱们也能指定改写某一个 GetBuilder 包裹的子树。
GetBuilder(
tag: 'myTag',
init: controller,
builder: (_) => Text(controller.myValue),
)
controller.update(['myTag']);
一般来说GetBuilder的遍历开支一般不是一个问题,功用影响不大,所以大局运用一个仍是多个 GetBuilder 差异不是很大。
2.5 坚持单例的几种办法,为什么会失效?
Dart的单例界说与GetX的依靠注入单例
默许的单例写法
class EventBus {
//私有结构函数 //留意单例怎样写,统一的三板斧:私有结构+static变量+工厂结构函数
EventBus._internal();
//保存单例
static final EventBus _singleton = EventBus._internal();
//工厂结构函数
factory EventBus()=> _singleton;
}
//界说一个top-level(大局)变量,页面引入该文件后能够直接运用bus
var bus = EventBus();
另一种Dart的单例写法,也能够不必顶层函数,直接用静态的getInstance获取实例:
class StorageService extends GetxService {
//保存单例
static StorageService _instance = StorageService._();
//工厂结构函数
factory StorageService() => _instance;
// //私有结构函数
StorageService._() {
init();
}
// 运用静态的办法获取实例
static StorageService getInstance() {
_instance = StorageService._();
return _instance;
}
GetX的依靠注入单例
默许的GetX对页面的Controller的注入是以懒加载的办法注入的。
class ChargeRecordsBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => ChargeRecordsController());
}
}
为什么?由于当页面封闭的时分就会主动收回,假如想坚持单例怎样做?
class FeedbackPage extends StatelessWidget {
FeedbackPage({Key? key}) : super(key: key);
final controller = Get.put(FeedbackController(),permanent: true);
}
手动的 put 目标而且设置耐久化,便是单例,怎样开释呢?由于Controller是比较特殊的存在,一般都是跟从页面的生命周期,所以也有了上面说到的手动指定开释Controller的解决方案。
那么其他的目标单例办理呢?比方 ApiRepositoty 这样的网络恳求数据仓库?
class AppBinding extends Bindings {
@override
void dependencies() async {
Get.put<ApiProvider>(ApiProvider(), permanent: true);
Get.put(ApiHomeRepository(apiProvider: Get.find()), permanent: true);
}
}
这样行不行?大局坚持单例嘛,这样必定行!可是我不想运用初始化的时分悉数创立,究竟只需在进入到这个模块才会调用这个模块的数据仓库。能懒加载是最好的?
class AppBinding extends Bindings {
@override
void dependencies() async {
Get.put<ApiProvider>(ApiProvider(), permanent: true);
Get.lazyPut<ApiHomeRepository>(() => ApiHomeRepository(apiProvider: Get.find<ApiProvider>()));
Get.lazyPut<ApiCommonRepository>(() => ApiCommonRepository(apiProvider: Get.find<ApiProvider>()));
Get.lazyPut<ApiMessageRepository>(() => ApiMessageRepository(apiProvider: Get.find<ApiProvider>()));
Get.lazyPut<ApiJobRepository>(() => ApiJobRepository(apiProvider: Get.find<ApiProvider>()));
Get.lazyPut<ApiCompanyRepository>(() => ApiCompanyRepository(apiProvider: Get.find<ApiProvider>()));
...
}
}
那咱们改成这样?合适吗?悉数用懒加载的办法创立?不太行!
只需在第一次的时分收效,当咱们在 Controller 中运用网络恳求数据仓库的时分就只能用一次,再运用就会失效,为什么?
自身 运用Get.lazyPut注册一个目标时,这个目标只需在初次被运用时才会创立,而且之后会一向存在于内存中,直到运用程序封闭或该目标被手动删除。
可是 Controller 又有一点特殊,假如 Controller 被收回,它所依靠的一切lazy load 目标也将被收回,由于它们不再被引证。这一般发生在 Controller 地点的页面被封闭的时分。
那怎样办?
这儿又引出了 Getx 依靠注入的作用域的概念:
-
GetxController: 这个类是你自己界说的操控器类,用于在运用中办理某个页面或部件的状况。这个类能够经过依靠注入来创立和办理,以便在整个操控器的生命周期中重复运用。
-
GetxView: 这个类是一个特殊的操控器类,它结合了 GetxController 和视图的概念。在大多数情况下,你能够运用 GetxView 来替代 GetxController ,由于它供给了更好的笼统和封装。
-
GetxService: 这是一个大局单例服务,能够在整个运用程序中运用。只需运用程序运行,GetxService 就会存在,而且能够经过依靠注入来访问。
-
GetxBindings: 这是一个用于绑定依靠项的类。你能够运用 GetxBindings 将依靠项绑定到操控器或视图中,以便在它们被创立时主动初始化。
-
GetxInstance: 它是一个根本的依靠注入容器,用于办理和创立其他类的实例。你能够运用 GetxInstance 来手动创立和办理实例,但一般情况下不需求这样做,由于GetX供给了更高级的依靠注入功用。
咱们想要坚持大局的单例,咱们只需求让咱们的数据仓库承继自 GetxService 即可
class ApiHomeRepository extends GetxService {
ApiProvider apiProvider;
...
}
这样就能完结懒加载的大局单例了。
2.6 GetX内置网络恳求封装
说起 Flutter 的网络恳求,除了原生的 Http 之外,咱们最耳熟能详的便是 dio 了, dio 当然好用了,可是 GetX 内置的 GetConnect 也是蛮好用的,往常的网络恳求与拦截之类操作都能满意需求了。
像 Android 封装协程相同封装 GetConnect 恳求过程
思路也是和协程相似,底层是基类的网络恳求封装,中心是数据仓库(Repository),然后在Controller(ViewModel)中运用数据仓库来建议恳求。
同样的咱们能够自界说一个回来目标封装自界说的过错与成功信息,并记载是否成功与失利,而且做好数据的转化流程。
与Android封装不同的是,Android能够直接经过泛型指定目标,然后直接把Json转化为目标,而 Dart 不支撑咱们这么做,咱们需求在数据仓库(Repository)中手动的调用目标的 formJson 之类的序列化办法才干转为指定的 Entity 目标。
- 先封装好网络恳求的成果目标
class HttpResult<T> {
HttpResult(
{required this.isSuccess,
dynamic dataJson,
List<dynamic>? listJson,
this.errorCode,
this.errorMsg}) {
this._dataJson = dataJson;
this._listJson = listJson;
}
//是否成功
bool isSuccess = false;
//成功的数据(Json数据)
dynamic _dataJson;
List<dynamic>? _listJson;
//成功的数据(真实的数据)
T? data;
List<T>? list;
//失利的数据
int? errorCode;
String? errorMsg;
/// 以Json目标的办法获取
Map<String, dynamic>? getDataJson() {
if (_dataJson is Map<String, dynamic>) {
return _dataJson as Map<String, dynamic>;
}
return null;
}
/// 以原始目标的办法获取,能够获取到String,Int,bool等根本类型
dynamic getDataDynamic() {
return _dataJson;
}
/// 以数组的办法获取
List<dynamic>? getListJson() {
return _listJson;
}
/// 设置真实的数据目标
void setData(T data) {
this.data = data;
}
void setList(List<T> list) {
this.list = list;
}
/// 根本类型转化为指定的泛型类型
HttpResult<T> convert<T>({T? data, List<T>? list}) {
var result = HttpResult<T>(
isSuccess: this.isSuccess,
dataJson: this._dataJson,
listJson: this._listJson,
errorCode: this.errorCode,
errorMsg: this.errorMsg);
result.data = data;
result.list = list;
return result;
}
}
- 底层的全能网络恳求封装,以常用的 Post Get 恳求为例
typedef NetSuccessCallback<T> = Function(T data);
typedef NetSuccessListCallback<T> = Function(T data);
typedef NetErrorCallback = Function(int? code, String? msg);
enum HttpMethod { GET, POST }
/// 网络恳求相关封装
class ApiProvider extends GetConnect {
/// 默许简略的恳求封装,回调的办法
Future<void> requestNetEasy(
String url, {
HttpMethod? method,
Map<String, String>? headers,
Map<String, dynamic>? query,
Map<String, String>? paths,
NetSuccessCallback<Map<String, dynamic>>? onSuccess,
NetSuccessListCallback<List<dynamic>>? onSuccessList,
NetErrorCallback? onError,
}) async {
//依据参数封装恳求
var req = generateRequest(method, query, paths, null, url, headers);
//开始恳求
final startTime = DateTime.now().millisecond;
var result = await req;
final endTime = DateTime.now().millisecond;
if (!AppConstant.inProduction) {
final duration = endTime - startTime;
Log.d('网络恳求耗时 $duration 毫秒, 呼应内容 ${result.body}}');
}
if (result.statusCode == 200) {
//网络恳求正确之后获取正常的Json-Map
Map<String, dynamic> jsonMap = result.body;
//检查apiCode是否正确
int code = jsonMap['code'];
if (code == 200) {
if (jsonMap['data'] is List<dynamic>) {
//回来数组
List<dynamic> list = jsonMap['data'];
if (onSuccessList != null) {
onSuccessList(list);
}
} else {
//回来目标
if (onSuccess != null) {
onSuccess(jsonMap['data']);
}
}
} else {
//Api过错
if (onError != null) {
onError(jsonMap['code'], jsonMap['message']);
}
}
} else {
//网络恳求过错
if (onError != null) {
// result.bodyString 过错信息,这儿没必要打印,拦截器中有打印的
onError(result.statusCode, result.statusText);
}
//吐司网络恳求过错
SmartDialog.compatible.showToast("Request Network Error :${result.statusCode} ${result.statusText}");
}
}
/// 网络恳求异步的Result封装
Future<HttpResult> requestNetResult(
String url, {
HttpMethod? method,
Map<String, String>? headers,
Map<String, dynamic>? query,
Map<String, String>? paths, //文件Flie
Map<String, Uint8List>? pathStreams, //文件流
}) async {
//依据参数封装恳求
var req = generateRequest(method, query, paths, pathStreams, url, headers);
//开始恳求
Response result;
if (!AppConstant.inProduction) {
final startTime = DateTime.now().millisecond;
result = await req;
final endTime = DateTime.now().millisecond;
final duration = endTime - startTime;
Log.d('网络恳求耗时 $duration 毫秒, 呼应内容 ${result.body}}');
} else {
result = await req;
}
if (result.statusCode == 200) {
//网络恳求正确之后获取正常的Json-Map
Map<String, dynamic> jsonMap = result.body;
//判别成功与失利
int code = jsonMap['code'];
if (code == 200) {
if (jsonMap['data'] is List<dynamic>) {
//成功->回来数组
return HttpResult(
isSuccess: true,
listJson: jsonMap['data'],
);
} else {
//成功->回来目标
return HttpResult(isSuccess: true, dataJson: jsonMap['data']);
}
} else {
//失利->回来Api过错
return HttpResult(isSuccess: false, errorCode: jsonMap['code'], errorMsg: jsonMap['message']);
}
} else {
//失利->回来网络恳求过错
return HttpResult(isSuccess: false, errorCode: result.statusCode, errorMsg: result.statusText);
}
}
///生成恳求体
Future<Response> generateRequest(
HttpMethod? method,
Map<String, dynamic>? query,
Map<String, String>? paths, //文件
Map<String, Uint8List>? pathStreams, //文件流
String url,
Map<String, String>? headers) async {
Future<Response> req;
if (method != null && method == HttpMethod.POST) {
var map = <String, dynamic>{};
if (query != null || paths != null || pathStreams != null) {
//只需有一个不为空,就能够封装参数
//默许的参数
if (query != null) {
map.addAll(query);
}
//Flie文件
if (paths != null) {
paths.forEach((key, value) {
if (value != null && value.isNotEmpty) {
final file = File(value);
map[key] = MultipartFile(
file.readAsBytesSync(),
filename: "file",
);
}
});
}
//File文件流
if (pathStreams != null) {
pathStreams.forEach((key, value) {
if (value != null && value.isNotEmpty) {
map[key] = MultipartFile(
value,
filename: "file_stream",
);
}
});
}
}
var form = FormData(map);
Log.d("Post恳求FromData参数,fields:${form.fields.toString()} files:${form.files.toString()}");
//以Post-Body的办法上传
req = post(url, form, headers: headers);
} else {
//默许以Get-Params的办法上传
req = get(url, headers: headers, query: query);
}
return req;
}
@override
void onInit() {
httpClient.baseUrl = ApiConstants.baseUrl;
httpClient.timeout = const Duration(seconds: 30);
/// 统一添加身份验证恳求头
httpClient.addRequestModifier(authInterceptor);
/// 打印Log(生产形式去除)
if (!AppConstant.inProduction) {
httpClient.addRequestModifier(logReqInterceptor);
}
httpClient.addResponseModifier(responseInterceptor);
/// 打印Log(生产形式去除)
if (!AppConstant.inProduction) {
httpClient.addResponseModifier(logResInterceptor);
}
}
/// 撤销网络恳求偏重新设置
ApiProvider cancelAndResetHttpClient() {
httpClient.close();
Get.replace(ApiProvider());
return Get.find();
}
}
咱们界说两种不同的运用办法,运用相似OkHttp那样的回调办法,或许协程那样的异步都支撑。
- 对应模块的数据仓库的创立
底层的全能基类封装好了之后,咱们就能写对应模块的数据仓库了。
class ApiRepository extends GetxService{
ApiProvider apiProvider;
ApiRepository({required this.apiProvider});
//获取服务器时刻
Future<HttpResult<ServerTime?>> getServerTime() async {
Map<String, String> headers = {};
headers["Accept"] = "application/x.yyjobs-api.v1+json";
//网络恳求获取原始数据
final result = await apiProvider.requestNetResult(ApiConstants.apiServiceTime, headers: headers);
//依据回来的成果,封装原始数据为Bean/Entity目标
if (result.isSuccess) {
final json = result.getDataJson();
var data = ServerTime.fromJson(json!);
//重新赋值data或list
return result.convert<ServerTime?>(data: data);
}
return result.convert<ServerTime?>();
}
// 获取职业的List
Future<HttpResult<IndustryData?>> getIndustryList() async {
Map<String, String> headers = {};
headers["Accept"] = "application/x.yyjobs-api.v1+json";
final result = await apiProvider
.requestNetResult(ApiConstants.apiIndustryList, headers: headers);
if (result.isSuccess) {
final jsonList = result.getListJson();
//获取List数据 需求转化一次
var list = jsonList?.map((value) {
if (value is Map<String, dynamic>) {
return IndustryData.fromJson(value);
} else {
return null;
}
}).toList();
return result.convert<IndustryData?>(list: list);
}
return result.convert<IndustryData>();
}
...
//用户登陆(回调的办法运用)
void userLoginEasy(
{NetSuccessCallback<UserLogin>? success, NetErrorCallback? onError}) {
Map<String, String> headers = {};
headers["Accept"] = "application/x.yyjobs-api.v1+json";
Map<String, String> params = {};
params["nric_no"] = "+861123456789";
params["password"] = "12345678";
params["registration_id"] = "123456789";
apiProvider.requestNetEasy(ApiConstants.apiUserLogin,
method: HttpMethod.POST,
headers: headers,
query: params, onSuccess: (json) {
var userLogin = UserLogin.fromJson(json);
if (success != null) {
success(userLogin);
}
}, onError: onError);
}
}
数据仓库中的代码演示了回调的办法调用,以及异步办法的调用。
而数据仓库中重要的功用便是调用基类恳求,封装参数,获取成果,判别是否成功,json转化为目标或集合,赋值转化对应真实的泛型目标便于上层运用。对过错信息只进行封装赋值不进行处理,由于咱们不知道详细的事务逻辑,上层拿到过错信息需求怎样处理,是弹出吐司,弹窗?仍是展现过错的Widget?
- 最终对应页面的上层调用
最终就到了咱们页面对应的 Controller 上层调用了,举个栗子:
class DemoController extends GetxController with StateMixin<dynamic> {
DemoController({required this.apiRepository});
//先登录再获取信息
Future<void> getUserInfo() async {
change(null, status: RxStatus.loading());
//用户登陆
final result = await apiRepository.userLogin();
if (result.isSuccess) {
final token = result.data?.token;
if (token != null) {
// 获取信息
final profile = await apiRepository.getUserProfile(token);
if (profile.isSuccess == true) {
final nickName = profile.data?.nickName;
SmartDialog.compatible.showToast("当时登录的用户为:$nickName");
change(null, status: RxStatus.success());
}
}
} else {
final errorMsg = result.errorMsg;
change(null, status: RxStatus.error(errorMsg));
}
}
咱们也是更引荐运用异步的办法来进行,更优雅不说,也便利支撑 Future 的并发。
Future<void> complicatedFetch() async {
// 等候一切的Future目标都完结
List<dynamic> results = await Future.wait([apiRepository.getServerTime2(), apiRepository.getIndustryList2()]);
int? timestamps;
List<IndustryData?>? industries;
for (var future in results) {
if (future is HttpResult<ServerTime?>) {
final serverTime = future;
timestamps = serverTime.data?.timestamps;
} else if (future is HttpResult<IndustryData?>) {
final industryList = future;
industries = industryList.list;
}
}
SmartDialog.showToast("并发完结的数据 职业数量:${industries?.length} 当时时刻:$timestamps");
}
总的来说 GetX 的网络恳求并没有什么大的槽点,用起来还算不错。
2.7 国际化与主题切换
其实 GetX 自身对这一块支撑的还蛮好的。
比方界说国际化文档
const Map<String, String> zh_CN = {
'loading': '加载中...',
'load_error_try_again': '加载失利,请点击重试',
'load_no_data': '暂无数据',
'Click Again Exit App': '再次点击退出运用',
'Select Video': '挑选视频',
'Select Image': '挑选图片',
'Camera': '相机',
'Album': '相册',
'Cancel': '撤销',
'Pull to refresh': '下拉改写',
'Release ready': '开释改写',
'Refreshing...': '改写中...',
'Succeeded': '成功',
'No more': '没有更多数据',
'Failed': '失利',
'Last updated at %T': '最近更新于 %T',
'Pull to load': '上拉加载更多',
'Loading...': '加载中...',
'Post Job': '发布工作',
'Message Conversation': '消息对话',
'Pending Interview': '待面试',
};
界说国际化服务类
class TranslationService extends Translations {
static Locale? get locale => Get.deviceLocale;
static const fallbackLocale = Locale('en', 'US');
@override
Map<String, Map<String, String>> get keys => {
'en_US': en_US,
'zh_CN': zh_CN,
};
}
main函数界说
child: GetMaterialApp(
//顶部是否展现Debug图标
debugShowCheckedModeBanner: true,
//是否展现Log
enableLog: true,
//本地化相关
locale: TranslationService.locale,
fallbackLocale: TranslationService.fallbackLocale,
translations: TranslationService(),
运用的时分:
直接在字符串后边加.tr 。 假如能找到对应的国际化文本就会履行逻辑,找不到就仍是显现当时的字符串,很有iOS的感觉,我感觉这一点比 Android 原生的国际化要便利一些。
切换主题
主题的办法与国际化相似,界说自己的主题样式对应的ThemeData目标
static ThemeData get lightTheme => createTheme(
brightness: Brightness.light,
background: ColorConstants.lightScaffoldBackgroundColor,
cardBackground: ColorConstants.secondaryAppColor,
primaryText: Colors.black,
secondaryText: Colors.black,
accentColor: ColorConstants.secondaryAppColor,
divider: ColorConstants.secondaryAppColor,
buttonBackground: Colors.black38,
buttonText: ColorConstants.secondaryAppColor,
disabled: ColorConstants.secondaryAppColor,
error: Colors.red,
);
static ThemeData get darkTheme => createTheme(
brightness: Brightness.dark,
background: ColorConstants.darkScaffoldBackgroundColor,
cardBackground: ColorConstants.secondaryDarkAppColor,
primaryText: Colors.white,
secondaryText: Colors.white,
accentColor: ColorConstants.secondaryDarkAppColor,
divider: Colors.black45,
buttonBackground: Colors.white,
buttonText: ColorConstants.secondaryDarkAppColor,
disabled: ColorConstants.secondaryDarkAppColor,
error: Colors.red,
);
main函数界说
child: GetMaterialApp(
//顶部是否展现Debug图标
debugShowCheckedModeBanner: true,
//是否展现Log
enableLog: true,
//默许路由与路由表的加载
//样式相关
theme: ThemeConfig.lightTheme,
darkTheme: ThemeConfig.darkTheme,
themeMode: ThemeMode.system,
当咱们手动的切换形式,比方漆黑形式与亮色形式的时分,就会主动加载对应的ThemeData样式了,
Get.changeTheme(Get.isDarkMode ? ThemeConfig.lightTheme : ThemeConfig.darkTheme);
也不需求像 Android 原生那么杂乱,去遍历视图树啊或许需求重启运用啊什么的。相对而言也是比原生 Android 便利一点。
假如当咱们想修改为指定的色彩的时分,比方暗黑形式下我不想为黑色,我想变为赤色,咱们能够经过东西类复写这作用:
//设置色彩兼容黑色形式
class DarkThemeUtil {
/// 默许漆黑形式下的色彩为[ColorConstants.darkScaffoldBackgroundColor].
/// 假如想自界说漆黑形式下的色彩
static Color multiColors(Color lightColor, {Color? darkColor}) {
Color color;
if (Get.isDarkMode) {
color = darkColor ?? ThemeConfig.darkTheme.cardTheme.color ?? ColorConstants.darkScaffoldBackgroundColor;
} else {
color = lightColor;
}
return color;
}
/// 默许漆黑形式下的色彩不变.
/// 假如想自界说漆黑形式下的图标色彩填充色彩就行
static Widget multiImageColorFit(String imgPath, double width, double height, {Color? darkColor, BoxFit? fit}) {
return MyAssetImage(imgPath, width: width, height: height, color: Get.isDarkMode ? darkColor : null, fit: fit);
}
/// 默许漆黑形式下的图片资源不变
/// 假如想自界说漆黑形式下的图片资源,能够直接替换图片
static Widget multiImagePath(String imgPath, double width, double height, {String? darkImagePath}) {
return MyAssetImage(Get.isDarkMode && darkImagePath != null ? darkImagePath : imgPath,
width: width, height: height);
}
}
比方修改漆黑形式下图标的色彩:
DarkThemeUtil.multiImageColor("assets/images/splash_center_blue_logo.webp", 144, 112,darkColor: Colors.white),
或许改动Button漆黑形式下的色彩:
backgroundColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.disabled)) {
return DarkThemeUtil.multiColors(disabledBackgroundColor ?? Colors.white,darkColor: Colors.lightBlue);
}
return DarkThemeUtil.multiColors(backgroundColor ?? Colors.white,darkColor: ColorConstants.appBlue);
}),
这种办法的收效级别更低,只在当时设置的控件收效,便利更精准的操控,而大局的ThemeData便利大范围的操控,都有各自的用法。
跋文
其实本文并不算是什么教程之类的文章。我本人也是新手来着谈不上教学,我仅仅记载项目开发到上线过程中遇到的一些问题,与自己得到一些解决方案,或许并不是最优解,乃至是有些过错的用法,本着和jym一同学习共享的心态发布出来,也是期望能得到一些点拨,也能碰撞出一些更好的思路与创意。
或许存在的一些问题:
的确很Android,那iOS同学习气吗?
说实话还挺习气的,乃至比 Android 同学更开心,wrapContent可是把iOS同学馋哭了,太好用了,束缚布局也太便利了,削减嵌套也太舒服了。开发效率比原生 iOS 高出不少。
假如不想用束缚布局有没有其他办法削减嵌套?
假如一个布局中嵌套太多的确是欠好看,自己写的页面过两天自己都不认识了,假如不想用束缚布局,也能够运用一些扩展办法的办法进行装饰与布局,或许把布局进来的抽取出来成办法,抽取出来成类。也会削减视觉上的嵌套,是的,仅仅视觉上的,自身仍是嵌套的那么多,乃至用一些扩展办法公共类办法包装的话,反而嵌套层数更多了,功用更差。
Getx中止保护了怎样办?
自身结构的更新速度就放缓了,其实也没所谓,现在的功用现已够用了,没有什么显着的缝隙,也不需求加一些其他的功用导致臃肿,乃至我都觉得像网络恳求等其他的非核心功用都不需求加进来,什么都想要最终搞得像之前的 XUtils 相同最终还不是那啥了。
后续的项目还用Flutter吗?
假如有新项目大概率仍是要上Flutter,由于咱们是比较新手嘛,第一次做项目踩了不少坑,速度其实和原生差不多,现在熟练了之后后期假如再做新项目会愈加快一点。
有Demo的示例代码吗?
Demo还没来得及做,简略给咱们哼哼几句…
哎不是,咱们这项目刚上线,的确还没整理出来,再说了一些知识点自身也都是一些比较零碎的不成系统,东一块西一块的欠好整理,后期或许出一个相似脚手架的东西开源出来。
后期或许会共享一些 Flutter 的其他文字,开发的小技巧,第三方库集成的等相关内容,两三篇的姿态,Flutter 的内容不预备做太多,究竟 Android 才是主业,Flutter 仅仅加分项。(主要也是太菜)
好了,本期内容就到这儿,如讲的不到位或讹夺的当地,期望同学们能够指出沟通。
假如感觉本文对你有一点点点的启示,还望你能点赞
支撑一下,你的支撑是我最大的动力啦。
Ok,这一期就此完结。
最近懒癌发作,文章也是拖了一个多星期才开始写,忙着看浪姐与拉票了。
话说回来,咱们没有给美依礼芽投票的赶紧投起来啊,尽管第一名了,可是咱们表现的便是一个遥遥领先,出类拔萃,不说了,今日的票还没投,溜了溜了。