背景
公司新项目,公司想要更高效的人力开发方式,之前的app采用MVVM+Kotlin的android和Swift开发IOS原生单独开发,优势和劣势都很明显,开发效率低,但是apk的体验和大小都比较极致,起码我负责的android是同行业竞品里包体积最小的,甚至比平均水平小50%,这次B端项目,这种极致的体验要求不高,反而是效率更重要。
通过对比RN,Uniapp,Flutter方式的优劣势,以及团队人员的组成以移动端为主情况,最后我的决策是使用Flutter进行开发。
使用flutter的好处主要是,主要是移动端上手更快,本身是一个偏UI引擎的开发语言,而且本身有正规军google维护,所以对生态还是比较放心。
开始
- 开发工具支持:VScode,Android studio
- 环境要求:Xcode , android SDK, Flutter SDK
一开始也不清楚,直接使用的Flutter doctor去检测的环境,实际上Xcode和android SDK 应该是可以2选一的,我因为是Android开发,后面因为磁盘空间不够,选择把Xcode卸载了,实际开发中基本没用过,因为我们IOS打包有IOS开发负责,如果你需要打包IOS,Xcode还是需要的。
1、框架
实际这个过程也是从0到1,没有找参考项目。
但是框架搭建都大差不差,都是从一些基础服务和基础能力开始。
基础服务提供埋点,推送,网络请求,图片加载,数据库,文件存储,sp存储等等,
基础UI能力下拉刷新,页面的loading和重试框架,基类,通用弹窗等。
跨端信息获取,统一的theme,页面跳转路由等。
因为是多人协作边开发边学习,首先约定好文件和变量命名规则,然后项目搭建前面1-2天由我开始把最迫切的基础能力网络请求,图片加载,文件,sp存储搭建好,并且编写入口启动页和登录页。
然后把各类基础能力采用组合的方式把能力封装出去,base_stateful和base_state_less。
对于Flutter这种所有的页面和view全部集成自Widget的然后在分成2个stateful和stateless控件的设计方式,属实适应了一会儿,这让我想起了android的Compose和单项数据流架构的MVI。
实际在使用Flutter之前我也没用过Compose,我们用Flutter写这个项目的时候实际KMM还没有成熟稳定版本,现在已经有了稳定版本,接下来我可能会接触下KMM。
对于基础能力各家架构各不相同,我采用了组合的方式把主要的能力插拔式的采用组合的方式集成在基类中。
abstract class BaseState<T extends BaseStatefulWidget> extends State<T> {
StreamController<PageData> pageStatus = StreamController<PageData>();
//页面加载失败显示
Widget? error;
//页面加载
Widget? loading;
//空页面
Widget? empty;
//内容
PagerController? pageController;
@override
void initState() {
super.initState();
}
@override
void setState(VoidCallback fn) {
if (mounted) {
super.setState(fn);
} else {
print("Widget unmount. skip setState");
}
}
DialogLoadingController? _dialogLoadingController;
///在页面上方显示一个 loading widget
///共有两种方法,showProgressDialog是其中一种
///具体参见 : loading_progress_widget.dart
///如果需要在 [dismissProgressDialog] 方法后跳转到其他页面或者执行什么
///使用 参数 [afterDismiss] 和 [dismissProgressDialog.afterPopTask] 不建议同时用使用。
///[loadingTimeOut] 超时时间,单位秒。
/// * 抵达这个时间后,将自动关闭loading widget,默认15秒
showProgressDialog(
{Widget? progress,
Color? bgColor,
int loadingTimeOut = 15,
AfterLoadingCallback? afterDismiss}) {
if (_dialogLoadingController == null) {
_dialogLoadingController = DialogLoadingController();
Navigator.of(context)
.push(PageRouteBuilder(
settings: const RouteSettings(name: loadingLayerRouteName),
///使用默认值效果可能不好
transitionDuration: const Duration(milliseconds: 0),
reverseTransitionDuration: const Duration(milliseconds: 0),
opaque: false,
pageBuilder: (ctx, animation, secondAnimation) {
return LoadingProgressState(
controller: _dialogLoadingController,
progress: progress,
bgColor: bgColor,
loadingTimeOut: loadingTimeOut)
.transformToWidget();
}))
.then((value) {
_dialogLoadingController?.invokeAfterPopTask(value);
_dialogLoadingController = null;
if (afterDismiss != null) {
afterDismiss(value);
}
});
}
}
///隐藏loading
/// * 注意: [afterPopTask] 会在loading隐藏后被调用,但是如果用户主动取消的话,将不会被调用
/// * 用户主动取消的将会调用[showLoading]的[afterDismiss]
void dismissProgressDialog({AfterLoadingCallback? afterPopTask}) {
if (afterPopTask != null) {
_dialogLoadingController?.holdAfterPopTask(task: afterPopTask);
}
_dialogLoadingController?.dismissDialog();
}
@override
void dispose() {
_dialogLoadingController = null;
pageController?.dispose();
pageController = null;
super.dispose();
}
//把任务抛到gpu绘制的下一帧执行
void postAtNextFrame(Function task) {
WidgetsBinding.instance.addPostFrameCallback((_) {
task.call();
});
}
}
其中page_controller中实现了页面的基础状态和UI逻辑,并且实现基类的页面可以自定义空页面和loading等。
而pageStatus表示页面的状态流,page_controller会负责监听这个状态流进行页面状态UI的实时更新。
网络请求采用了通用的Dio库并进行了基础封装。
extension NetFunc on BaseState {
///get请求
///[showLoadingAuto] 是否自动显示和隐藏loading ,推荐在页面加载完成之后的其他网络请求中使用
///[toast] 是否自动弹出失败请求toast
///
///[antiContentFlow]只会走数据加载的状态流
///主要应用场景,在于页面已经加载出来之后,如果继续走flow会走完整的
///[loading -> content | error | empty 流]
///某些场景不需要loading
///
///[enableFlow]走完整的数据流loading , content , empty failure
Future<dynamic> get(String path, Map<String, dynamic> params,
{Function? successCallBack,
Function? errorCallBack,
bool showLoadingAuto = false,
bool enableFlow = true,
bool antiContentFlow = false,
bool toast = true,
bool refresh = false,
CancelToken? cancelToken}) async {
if (showLoadingAuto) showProgressDialog();
//模拟弱网
//await Future<void>.delayed(Duration(seconds: 2));
cancelToken?.cancel();
if (enableFlow) pageStatus.add(PageData(PageStatus.LOADING));
int currentTime = DateTime.now().millisecondsSinceEpoch;
await NetManager.getInstance().get(path, params, (data) {
if (showLoadingAuto) {
dismissProgressDialog(afterPopTask: (e) {
try {
successCallBack?.call(data);
} on Exception catch (error) {
//做了一些日志处理
}
});
} else {
try {
successCallBack?.call(data);
} on Exception catch (error) {
//做了一些日志处理
}
}
if (enableFlow || antiContentFlow) {
if (pageController?.emptyPrediction?.call(data) == true) {
pageStatus.add(PageData(PageStatus.EMPTY));
} else {
// pageStatus.add(PageData(PageStatus.ERROR));
pageStatus.add(PageData(PageStatus.CONTENT, data: data));
}
}
}, (BaseBeanEntity error) {
if (enableFlow) {
pageStatus.add(PageData(error.code != ResultCode.NO_NETWORK
? PageStatus.ERROR
: PageStatus.ERROR_NO_NET));
}
if (toast) {
ToastUtils.showToast(
context: context, msg: "Net Error Code:${error.code},${error.msg}");
}
if (showLoadingAuto) dismissProgressDialog();
errorCallBack?.call(error);
},startRequestTime : currentTime);
}
///post请求
///[showLoadingAuto] 是否自动显示和隐藏loading
///[toast] 是否自动弹出失败请求toast
///
///[antiContentFlow]只会走数据加载的状态流
///主要应用场景,在于页面已经加载出来之后,如果继续走flow会走完整的
///[loading -> content | error | empty 流]
///某些场景不需要loading
///
///[enableFlow]走完整的数据流loading , content , empty failure
Future<dynamic> post(String path, Map<String, dynamic> params,
{Function? successCallBack,
Function? errorCallBack,
bool showLoadingAuto = false,
bool enableFlow = true,
bool antiContentFlow = false,
bool toast = true,
CancelToken? cancelToken}) async {
if (showLoadingAuto) showProgressDialog();
cancelToken?.cancel();
//模拟弱网
// await Future.delayed(const Duration(seconds: 3));
if (enableFlow) pageStatus.add(PageData(PageStatus.LOADING));
int currentTime = DateTime.now().millisecondsSinceEpoch;
NetManager.getInstance().post(path, params, (data) async {
if (showLoadingAuto) {
dismissProgressDialog(afterPopTask: (e) {
try {
successCallBack?.call(data);
} on Exception catch (error) {
//做了一些日志处理
}
});
} else {
try {
successCallBack?.call(data);
} on Exception catch (error) {
//做了一些日志处理
}
}
if (enableFlow || antiContentFlow) {
if (pageController?.emptyPrediction?.call(data) == true) {
pageStatus.add(PageData(PageStatus.EMPTY));
} else {
pageStatus.add(PageData(PageStatus.CONTENT, data: data));
}
}
}, (error) {
if (enableFlow) {
pageStatus.add(PageData(error.code != ResultCode.NO_NETWORK
? PageStatus.ERROR
: PageStatus.ERROR_NO_NET));
}
if (toast) {
ToastUtils.showToast(
context: context, msg: "Net Error Code:${error.code},${error.msg}");
}
if (showLoadingAuto) dismissProgressDialog();
errorCallBack?.call(error);
},startRequestTime : currentTime);
}
}
实际这个网络扩展类扩展了base基类的能力,它主要的作用是根据网络请求对UI流进行分发。底层的请求代码就不贴了,非核心。
实际上页面的状态比这里定义的页面状态,更复杂。
比如:
- 页面页面中多请求,谁该主导loading状态还是,说要等所有请求完再显示Content。
- 页面加载完之后,页面条件变化,需要重新请求部分内容,这个时候覆盖页面的loading和蒙层的loading谁该显示。
- 还有Flutter的单项数据流,在setState中执行的逻辑不能继续触发setState等。
- 还有复杂的路由跳转维护,电商业务的创建订单结束复杂的页面跳转分发逻辑需要强大的路由基类支持等。
篇幅太长下章继续 To be continue....