背景
公司新項(xiàng)目端三,公司想要更高效的人力開發(fā)方式锯茄,之前的app采用MVVM+Kotlin的android和Swift開發(fā)IOS原生單獨(dú)開發(fā)搔驼,優(yōu)勢(shì)和劣勢(shì)都很明顯谈火,開發(fā)效率低,但是apk的體驗(yàn)和大小都比較極致匙奴,起碼我負(fù)責(zé)的android是同行業(yè)競品里包體積最小的堆巧,甚至比平均水平小50%,這次B端項(xiàng)目,這種極致的體驗(yàn)要求不高谍肤,反而是效率更重要啦租。
通過對(duì)比RN,Uniapp荒揣,F(xiàn)lutter方式的優(yōu)劣勢(shì)篷角,以及團(tuán)隊(duì)人員的組成以移動(dòng)端為主情況,最后我的決策是使用Flutter進(jìn)行開發(fā)系任。
使用flutter的好處主要是恳蹲,主要是移動(dòng)端上手更快,本身是一個(gè)偏UI引擎的開發(fā)語言俩滥,而且本身有正規(guī)軍google維護(hù)嘉蕾,所以對(duì)生態(tài)還是比較放心。
開始
- 開發(fā)工具支持:VScode霜旧,Android studio
- 環(huán)境要求:Xcode , android SDK, Flutter SDK
一開始也不清楚错忱,直接使用的Flutter doctor去檢測的環(huán)境,實(shí)際上Xcode和android SDK 應(yīng)該是可以2選一的挂据,我因?yàn)槭茿ndroid開發(fā)以清,后面因?yàn)榇疟P空間不夠,選擇把Xcode卸載了崎逃,實(shí)際開發(fā)中基本沒用過掷倔,因?yàn)槲覀僆OS打包有IOS開發(fā)負(fù)責(zé),如果你需要打包IOS个绍,Xcode還是需要的勒葱。
1、框架
實(shí)際這個(gè)過程也是從0到1障贸,沒有找參考項(xiàng)目错森。
但是框架搭建都大差不差,都是從一些基礎(chǔ)服務(wù)和基礎(chǔ)能力開始篮洁。
基礎(chǔ)服務(wù)提供埋點(diǎn)涩维,推送,網(wǎng)絡(luò)請(qǐng)求袁波,圖片加載瓦阐,數(shù)據(jù)庫,文件存儲(chǔ)篷牌,sp存儲(chǔ)等等睡蟋,
基礎(chǔ)UI能力下拉刷新,頁面的loading和重試框架枷颊,基類戳杀,通用彈窗等该面。
跨端信息獲取,統(tǒng)一的theme信卡,頁面跳轉(zhuǎn)路由等隔缀。
因?yàn)槭嵌嗳藚f(xié)作邊開發(fā)邊學(xué)習(xí),首先約定好文件和變量命名規(guī)則傍菇,然后項(xiàng)目搭建前面1-2天由我開始把最迫切的基礎(chǔ)能力網(wǎng)絡(luò)請(qǐng)求猾瘸,圖片加載,文件,sp存儲(chǔ)搭建好丢习,并且編寫入口啟動(dòng)頁和登錄頁牵触。
然后把各類基礎(chǔ)能力采用組合的方式把能力封裝出去,base_stateful和base_state_less咐低。
對(duì)于Flutter這種所有的頁面和view全部集成自Widget的然后在分成2個(gè)stateful和stateless控件的設(shè)計(jì)方式揽思,屬實(shí)適應(yīng)了一會(huì)兒,這讓我想起了android的Compose和單項(xiàng)數(shù)據(jù)流架構(gòu)的MVI见擦。
實(shí)際在使用Flutter之前我也沒用過Compose绰更,我們用Flutter寫這個(gè)項(xiàng)目的時(shí)候?qū)嶋HKMM還沒有成熟穩(wěn)定版本,現(xiàn)在已經(jīng)有了穩(wěn)定版本锡宋,接下來我可能會(huì)接觸下KMM。
對(duì)于基礎(chǔ)能力各家架構(gòu)各不相同特恬,我采用了組合的方式把主要的能力插拔式的采用組合的方式集成在基類中执俩。
abstract class BaseState<T extends BaseStatefulWidget> extends State<T> {
StreamController<PageData> pageStatus = StreamController<PageData>();
//頁面加載失敗顯示
Widget? error;
//頁面加載
Widget? loading;
//空頁面
Widget? empty;
//內(nèi)容
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;
///在頁面上方顯示一個(gè) loading widget
///共有兩種方法,showProgressDialog是其中一種
///具體參見 : loading_progress_widget.dart
///如果需要在 [dismissProgressDialog] 方法后跳轉(zhuǎn)到其他頁面或者執(zhí)行什么
///使用 參數(shù) [afterDismiss] 和 [dismissProgressDialog.afterPopTask] 不建議同時(shí)用使用癌刽。
///[loadingTimeOut] 超時(shí)時(shí)間役首,單位秒。
/// * 抵達(dá)這個(gè)時(shí)間后显拜,將自動(dòng)關(guān)閉loading widget,默認(rèn)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),
///使用默認(rèn)值效果可能不好
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] 會(huì)在loading隱藏后被調(diào)用衡奥,但是如果用戶主動(dòng)取消的話,將不會(huì)被調(diào)用
/// * 用戶主動(dòng)取消的將會(huì)調(diào)用[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();
}
//把任務(wù)拋到gpu繪制的下一幀執(zhí)行
void postAtNextFrame(Function task) {
WidgetsBinding.instance.addPostFrameCallback((_) {
task.call();
});
}
}
其中page_controller中實(shí)現(xiàn)了頁面的基礎(chǔ)狀態(tài)和UI邏輯远荠,并且實(shí)現(xiàn)基類的頁面可以自定義空頁面和loading等矮固。
而pageStatus表示頁面的狀態(tài)流,page_controller會(huì)負(fù)責(zé)監(jiān)聽這個(gè)狀態(tài)流進(jìn)行頁面狀態(tài)UI的實(shí)時(shí)更新譬淳。
網(wǎng)絡(luò)請(qǐng)求采用了通用的Dio庫并進(jìn)行了基礎(chǔ)封裝档址。
extension NetFunc on BaseState {
///get請(qǐng)求
///[showLoadingAuto] 是否自動(dòng)顯示和隱藏loading ,推薦在頁面加載完成之后的其他網(wǎng)絡(luò)請(qǐng)求中使用
///[toast] 是否自動(dòng)彈出失敗請(qǐng)求toast
///
///[antiContentFlow]只會(huì)走數(shù)據(jù)加載的狀態(tài)流
///主要應(yīng)用場景邻梆,在于頁面已經(jīng)加載出來之后守伸,如果繼續(xù)走flow會(huì)走完整的
///[loading -> content | error | empty 流]
///某些場景不需要loading
///
///[enableFlow]走完整的數(shù)據(jù)流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();
//模擬弱網(wǎng)
//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請(qǐng)求
///[showLoadingAuto] 是否自動(dòng)顯示和隱藏loading
///[toast] 是否自動(dòng)彈出失敗請(qǐng)求toast
///
///[antiContentFlow]只會(huì)走數(shù)據(jù)加載的狀態(tài)流
///主要應(yīng)用場景浦妄,在于頁面已經(jīng)加載出來之后尼摹,如果繼續(xù)走flow會(huì)走完整的
///[loading -> content | error | empty 流]
///某些場景不需要loading
///
///[enableFlow]走完整的數(shù)據(jù)流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();
//模擬弱網(wǎng)
// 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);
}
}
實(shí)際這個(gè)網(wǎng)絡(luò)擴(kuò)展類擴(kuò)展了base基類的能力,它主要的作用是根據(jù)網(wǎng)絡(luò)請(qǐng)求對(duì)UI流進(jìn)行分發(fā)蠢涝。底層的請(qǐng)求代碼就不貼了玄呛,非核心。
實(shí)際上頁面的狀態(tài)比這里定義的頁面狀態(tài)惠赫,更復(fù)雜把鉴。
比如:
- 頁面頁面中多請(qǐng)求,誰該主導(dǎo)loading狀態(tài)還是儿咱,說要等所有請(qǐng)求完再顯示Content庭砍。
- 頁面加載完之后,頁面條件變化混埠,需要重新請(qǐng)求部分內(nèi)容怠缸,這個(gè)時(shí)候覆蓋頁面的loading和蒙層的loading誰該顯示。
- 還有Flutter的單項(xiàng)數(shù)據(jù)流钳宪,在setState中執(zhí)行的邏輯不能繼續(xù)觸發(fā)setState等揭北。
- 還有復(fù)雜的路由跳轉(zhuǎn)維護(hù),電商業(yè)務(wù)的創(chuàng)建訂單結(jié)束復(fù)雜的頁面跳轉(zhuǎn)分發(fā)邏輯需要強(qiáng)大的路由基類支持等吏颖。
篇幅太長下章繼續(xù) To be continue....