發(fā)實(shí)踐:校園打印店打卡應(yīng)用落地復(fù)盤(pán))
這幾年校園場(chǎng)景里的應(yīng)用需求越來(lái)越多打印店打卡就是其中一個(gè)典型學(xué)生到店取文件要確認(rèn)訂單、商家要核銷打印任務(wù)、管理員還要統(tǒng)計(jì)使用量。我之前做過(guò)幾版純?cè)鷮?shí)現(xiàn)安卓一套、iOS一套、鴻蒙再一套維護(hù)成本直接失控。后來(lái)?yè)Q成 Flutter 做跨平臺(tái)統(tǒng)一開(kāi)發(fā)再通過(guò)適配分支跑到鴻蒙設(shè)備上整體工作量至少砍了一半。這篇就完整復(fù)盤(pán)一下這個(gè)校園打印店打卡應(yīng)用從選型到落地的全過(guò)程重點(diǎn)放在 Flutter 跨平臺(tái)能力在鴻蒙場(chǎng)景下的真實(shí)表現(xiàn)、工程改造的細(xì)節(jié)以及我在真機(jī)調(diào)試時(shí)踩過(guò)的那些坑。如果你正準(zhǔn)備用 Flutter 開(kāi)發(fā)鴻蒙應(yīng)用或者想了解 Flutter 技術(shù)棧在鴻蒙設(shè)備上的可行性這篇文章應(yīng)該能幫你少走不少?gòu)澛?。我?huì)把工程怎么建、狀態(tài)管理怎么選、組件之間怎么通信、掃碼模塊怎么調(diào)、真機(jī)怎么連、打包怎么配全部按實(shí)操順序?qū)懬宄⒔o出可以直接抄的代碼和配置。1. 為什么這次打卡應(yīng)用選了 Flutter 而不是 ArkTS先聊一個(gè)繞不開(kāi)的問(wèn)題也是團(tuán)隊(duì)里爭(zhēng)論最久的鴻蒙應(yīng)用開(kāi)發(fā)到底該用 ArkTS 還是 Flutter當(dāng)時(shí)的情況是打印店打卡應(yīng)用需要同時(shí)覆蓋三類終端學(xué)生用的手機(jī)安卓、鴻蒙、iOS 都有、打印店商家用的平板以鴻蒙和安卓為主、以及運(yùn)營(yíng)后臺(tái)供管理員查看的 Web 端。如果全走 ArkTS 原生意味著鴻蒙單獨(dú)維護(hù)一套代碼安卓和 iOS 又各一套三個(gè)人維護(hù)三套邏輯光是接口字段對(duì)齊就夠喝一壺的。Flutter 這邊的優(yōu)勢(shì)很直接一套 Dart 代碼編譯出多端產(chǎn)物UI 層完全自繪不依賴系統(tǒng)控件理論上只要渲染引擎能跑界面的表現(xiàn)就能保持一致。尤其對(duì)打卡這種業(yè)務(wù)界面邏輯并不復(fù)雜核心在掃碼、定位、訂單狀態(tài)流轉(zhuǎn)這些跨端做起來(lái)并沒(méi)有太大障礙。但 ArkTS 也不是沒(méi)有吸引力。鴻蒙原生應(yīng)用在調(diào)用系統(tǒng)能力比如藍(lán)牙、NFC、掃碼攝像頭時(shí)最順暢性能也最有保障。問(wèn)題在于鴻蒙原生生態(tài)目前對(duì)第三方庫(kù)的支持還不夠全面尤其是地圖、支付、推送這類服務(wù)偶爾需要自己去對(duì)接鴻蒙 SDK 的原始接口。而 Flutter 社區(qū)生態(tài)里現(xiàn)成的插件更多雖然有些插件在鴻蒙上需要重新編譯適配但至少有現(xiàn)成方案可以參考。最后團(tuán)隊(duì)拍板選 Flutter核心原因有三個(gè)團(tuán)隊(duì)現(xiàn)有技能棧以 Dart 和前端為主上 Flutter 的學(xué)習(xí)成本明顯低于全員轉(zhuǎn) ArkTS。打卡應(yīng)用的核心邏輯訂單狀態(tài)機(jī)、計(jì)數(shù)統(tǒng)計(jì)、時(shí)段計(jì)算可以完全復(fù)用跨端只換 UI 殼子。未來(lái)如果要做微信小程序之外的輕量端Flutter 也能編譯到 Web一套邏輯多個(gè)出口。這里補(bǔ)充一點(diǎn)個(gè)人看法如果你做的應(yīng)用強(qiáng)依賴?guó)櫭商赜械南到y(tǒng)能力比如元服務(wù)、分布式流轉(zhuǎn)、超級(jí)終端聯(lián)動(dòng)那還是老老實(shí)實(shí)走 ArkTS。像打卡這種以業(yè)務(wù)邏輯為核心的場(chǎng)景Flutter 的跨平臺(tái)優(yōu)勢(shì)才能發(fā)揮出來(lái)。順便回應(yīng)一下網(wǎng)上常爭(zhēng)論的ArkTS 和 Flutter 誰(shuí)更流行。我的判斷是短期看鴻蒙原生應(yīng)用商店里 ArkTS 應(yīng)用數(shù)量占優(yōu)但跨平臺(tái)開(kāi)發(fā)者群體中 Flutter 的基數(shù)和生態(tài)豐富度更高。兩個(gè)技術(shù)棧橫向?qū)Ρ榷ㄎ徊⒉皇腔ハ嗵娲腔パa(bǔ)。對(duì)這種中小型校園項(xiàng)目來(lái)說(shuō)哪邊能更快交付、更好維護(hù)哪邊就是正確答案。2. 開(kāi)發(fā)環(huán)境準(zhǔn)備讓 Flutter 工程順利跑進(jìn)鴻蒙設(shè)備環(huán)境和工程配置是整個(gè)過(guò)程中最容易卡殼的環(huán)節(jié)而且報(bào)錯(cuò)信息經(jīng)常不是直接說(shuō)你缺了哪個(gè)依賴而是編譯編到一半彈出一個(gè)看不懂的底層錯(cuò)誤。先說(shuō) Flutter SDK 的版本問(wèn)題。要跑鴻蒙設(shè)備理論上用官方穩(wěn)定的 Flutter 主分支配合 OpenHarmony 適配版本。這里有個(gè)容易踩的坑直接下載官網(wǎng)最新版 Flutter創(chuàng)建工程后連 Ubuntu 的安卓模擬器都沒(méi)問(wèn)題但一連接鴻蒙真機(jī)就提示無(wú)法識(shí)別設(shè)備或者干脆在執(zhí)行構(gòu)建命令時(shí)找不到鴻蒙工具鏈。我在實(shí)操中采用的是這種方式安裝 OpenHarmony 命令行工具并把hdc的路徑單獨(dú)配置到系統(tǒng)環(huán)境變量。在 Flutter 工程中引入鴻蒙平臺(tái)的適配 SDK 路徑讓 Flutter 在構(gòu)建時(shí)能找到對(duì)應(yīng)的編譯器。創(chuàng)建工程后先執(zhí)行一次flutter doctor確認(rèn) Flutter、Dart 和鴻蒙工具鏈的狀態(tài)。Exhibit一個(gè)常見(jiàn)的環(huán)境配置示例以 Windows 下為例# 配置鴻蒙工具鏈環(huán)境變量 export DEVECO_SDK_HOME/path/to/ohos-sdk export PATH$PATH:$DEVECO_SDK_HOME/toolchains # 查看 Flutter 環(huán)境狀態(tài) flutter doctor如果flutter doctor檢測(cè)不到鴻蒙工具鏈不要急著換版本先手動(dòng)檢查環(huán)境變量里的路徑是否存在、版本是否匹配。我之前遇到過(guò) SDK 路徑配了但沒(méi)生效的情況最后是重啟終端才被正確加載。工程創(chuàng)建這一步有個(gè)優(yōu)化小技巧不要用默認(rèn)的計(jì)數(shù)器模板而是直接建一個(gè)空工程再?gòu)牧慵尤氪蚩I(yè)務(wù)模塊。默認(rèn)模板會(huì)帶一堆演示代碼后面清理起來(lái)反而麻煩。我習(xí)慣先創(chuàng)建好目錄結(jié)構(gòu)再動(dòng)手寫(xiě)業(yè)務(wù)。再說(shuō)組件通信的問(wèn)題這也是打卡應(yīng)用里躲不開(kāi)的設(shè)計(jì)點(diǎn)。打印店打卡涉及三個(gè)角色的聯(lián)動(dòng)學(xué)生端需要顯示自己的打卡記錄商家端需要看到實(shí)時(shí)訂單管理端需要統(tǒng)計(jì)匯總。三個(gè)入口如果各自維護(hù)一份狀態(tài)很容易出現(xiàn)數(shù)據(jù)不一致。Flutter 本身的組件通信機(jī)制解決了父子組件之間的數(shù)據(jù)傳遞但是兄弟組件、跨頁(yè)面組件、甚至是跨模塊之間的狀態(tài)同步還是需要一個(gè)統(tǒng)一的狀態(tài)管理層。這個(gè)應(yīng)用我選了 Google 官方推薦的 Provider 方案。原因有兩個(gè)一是它的 API 簡(jiǎn)潔ChangeNotifier加Consumer就能解決絕大多數(shù)場(chǎng)景沒(méi)有引入額外概念上手很快二是它在鴻蒙環(huán)境下的兼容性經(jīng)過(guò)驗(yàn)證不需要特殊適配分支處理。下面具體展開(kāi)說(shuō)一下 Provider 在打卡場(chǎng)景里的實(shí)際寫(xiě)法。3. 打卡核心模塊的狀態(tài)管理與組件通信Provider 的工程落地先說(shuō)需求拆解。打印店打卡應(yīng)用的核心場(chǎng)景是學(xué)生到店后掃碼打卡打卡成功則生成記錄商家在后臺(tái)確認(rèn)訂單屬實(shí)管理員可查看今日打卡報(bào)表。整個(gè)流程中打卡狀態(tài)是全局共享的——學(xué)生端打卡后商家端要立刻看到狀態(tài)變化管理端也要同步更新統(tǒng)計(jì)數(shù)字。如果只在某個(gè)頁(yè)面內(nèi)部處理頁(yè)面一銷毀狀態(tài)就丟了。所以我把全局狀態(tài)統(tǒng)一放進(jìn) Provider 里數(shù)據(jù)模型單獨(dú)建一個(gè)類來(lái)維護(hù)。Exhibit 打卡狀態(tài)的數(shù)據(jù)模型class CheckInState extends ChangeNotifier { ListCheckInRecord _records []; // 今日打卡次數(shù) int get todayCount _records.where((r) r.time.isToday()).length; // 最近一條打卡記錄 CheckInRecord? get latestRecord _records.isEmpty ? null : _records.last; void addRecord(CheckInRecord record) { _records.add(record); notifyListeners(); } void checkout(String recordId) { final index _records.indexWhere((r) r.id recordId); if (index ! -1) { _records[index].status confirmed; notifyListeners(); } } }這里的notifyListeners()是關(guān)鍵它通知所有監(jiān)聽(tīng)該狀態(tài)的組件重新構(gòu)建。學(xué)生端打卡按鈕觸發(fā)addRecord商家端頁(yè)面通過(guò)Consumer監(jiān)聽(tīng)到列表變化自動(dòng)刷新訂單列表。這就實(shí)現(xiàn)了跨頁(yè)面的實(shí)時(shí)聯(lián)動(dòng)不需要手動(dòng)傳參。再聊聊組件通信的具體層級(jí)。Flutter 里父子組件通信最簡(jiǎn)單的方式就是構(gòu)造函數(shù)傳值和回調(diào)比如打卡頁(yè)的按鈕組件接收一個(gè)onCheckIn回調(diào)由父頁(yè)面決定點(diǎn)擊后的業(yè)務(wù)邏輯。但跨頁(yè)面的狀態(tài)同步必須走 Provider 或類似方案否則你只能通過(guò)路由參數(shù)層層傳遞改起來(lái)非常痛苦。我這里把組件通信分成三個(gè)層級(jí)來(lái)設(shè)計(jì)頁(yè)面內(nèi)組件通信用構(gòu)造參數(shù)和回調(diào)簡(jiǎn)單直接。同一模塊內(nèi)跨頁(yè)面通信用 Provider 的ChangeNotifierProvider包裹模塊根節(jié)點(diǎn)??缒K共享數(shù)據(jù)例如登錄信息、打印訂單用全局 Provider同時(shí)配合ProxyProvider做模塊間依賴。實(shí)際運(yùn)行下來(lái)這套分層在鴻蒙設(shè)備上的表現(xiàn)和安卓幾乎沒(méi)有差別熱重載、狀態(tài)恢復(fù)都正常。唯一需要注意的是個(gè)別鴻蒙版本對(duì)ChangeNotifier的銷毀時(shí)機(jī)處理不夠及時(shí)在退出頁(yè)面的時(shí)候偶爾會(huì)收到notifyListeners回調(diào)的警告。我在代碼里加了一個(gè)統(tǒng)一的安全判斷在回調(diào)前先確認(rèn)組件是否仍然掛在組件樹(shù)上。Exhibit 安全判斷的寫(xiě)法if (mounted) { notifyListeners(); }這個(gè)處理看起來(lái)微小但真機(jī)上避免了很多奇怪的崩潰和紅屏提示。4. 打印店打卡的業(yè)務(wù)建模與數(shù)據(jù)設(shè)計(jì)一個(gè)打卡應(yīng)用看似簡(jiǎn)單但如果數(shù)據(jù)模型設(shè)計(jì)得不好等做到報(bào)表統(tǒng)計(jì)那一層會(huì)非常痛苦。尤其是校園打印店這種場(chǎng)景一個(gè)學(xué)生一天可能多次到店一次打印任務(wù)可能涉及多頁(yè)文件、多個(gè)打印參數(shù)如果不提前預(yù)留字段后期擴(kuò)展就得動(dòng)表結(jié)構(gòu)。我在設(shè)計(jì)數(shù)據(jù)模型時(shí)把整個(gè)業(yè)務(wù)拆成了四個(gè)實(shí)體用戶學(xué)生/商家/管理員用角色字段區(qū)分。打卡記錄每次到店取件生成一條記錄包含時(shí)間、地點(diǎn)、訂單號(hào)。打印訂單每頁(yè)的打印參數(shù)、份數(shù)、顏色模式、是否雙面。統(tǒng)計(jì)報(bào)表按天、按周匯總的打卡次數(shù)和打印量。打卡記錄和打印訂單是關(guān)聯(lián)關(guān)系一條打卡記錄可以對(duì)應(yīng)多個(gè)打印訂單因?yàn)閷W(xué)生可能一次取好幾份文件。反過(guò)來(lái)一個(gè)訂單只能對(duì)應(yīng)一條打卡記錄這樣在商家確認(rèn)環(huán)節(jié)可以直接通過(guò)打卡記錄反查訂單詳情。Exhibit 打卡記錄的核心字段設(shè)計(jì)字段類型說(shuō)明idString唯一ID使用時(shí)間戳加隨機(jī)數(shù)生成userIdString打卡用戶學(xué)生IDstoreIdString打印店IDorderIdsListString關(guān)聯(lián)訂單ID列表timeDateTime打卡時(shí)間locationString打卡地點(diǎn)通過(guò)定位或掃碼獲取statusint狀態(tài)0待確認(rèn)1已確認(rèn)2已取消訂單表還會(huì)記錄打印頁(yè)數(shù)、單價(jià)、合計(jì)金額等信息。這里有個(gè)實(shí)際經(jīng)驗(yàn)打印店的計(jì)費(fèi)規(guī)則很靈活有的按黑白頁(yè)收費(fèi)有的按彩色頁(yè)收費(fèi)還有的包含裝訂費(fèi)。在設(shè)計(jì)金額字段時(shí)最好把費(fèi)用明細(xì)做成一個(gè) JSON 字符串存起來(lái)而不是拆成多個(gè)列。因?yàn)橛?jì)費(fèi)規(guī)則經(jīng)常變把明細(xì)打包成 JSON改計(jì)費(fèi)邏輯時(shí)只改前端計(jì)算邏輯數(shù)據(jù)表結(jié)構(gòu)不用動(dòng)。這種設(shè)計(jì)在 Flutter 側(cè)就用 JsonSerializable 來(lái)序列化寫(xiě)模型類的時(shí)候加注解自動(dòng)生成 fromJson 和 toJson省掉很多手寫(xiě)模板。當(dāng)時(shí)我還對(duì)比過(guò)手寫(xiě)和 codegen 兩種方式實(shí)測(cè)直接手寫(xiě)字段映射在模型少的時(shí)候更快模型超過(guò)五個(gè)字段之后還是 codegen 更穩(wěn)尤其是字段重命名時(shí)不容易漏改。打印店的業(yè)務(wù)有個(gè)特殊性高峰期集中在中午下課前后同一時(shí)間可能幾十個(gè)學(xué)生同時(shí)到店取件。數(shù)據(jù)模型設(shè)計(jì)好了以后后端接口還需要處理并發(fā)打卡的冪等問(wèn)題。我的做法是打卡記錄的主鍵用userId 年月日 自增序號(hào)的復(fù)合格式保證同一個(gè)學(xué)生同一分鐘內(nèi)的多次打卡不會(huì)因?yàn)椴l(fā)請(qǐng)求造成重復(fù)數(shù)據(jù)。5. 打印店打卡的核心界面實(shí)現(xiàn)從掃碼到狀態(tài)同步界面這一塊我按用戶角色拆成三個(gè)端來(lái)做但共享同一套組件庫(kù)和主題。為什么強(qiáng)調(diào)共享因?yàn)?Flutter 的優(yōu)勢(shì)就在于一套組件可以在不同入口復(fù)用如果每個(gè)端都單獨(dú)寫(xiě)一套頁(yè)面那就白用 Flutter 了。學(xué)生端的核心頁(yè)面是掃碼打卡頁(yè)。這個(gè)頁(yè)面的邏輯其實(shí)很簡(jiǎn)單一個(gè)掃碼窗口一個(gè)顯示結(jié)果的狀態(tài)區(qū)域一個(gè)手動(dòng)補(bǔ)卡的入口。我把掃碼能力封裝成了一個(gè)獨(dú)立的 Widget底層調(diào)用攝像頭權(quán)限并識(shí)別二維碼識(shí)別結(jié)果回調(diào)到上層。Exhibit 掃碼頁(yè)面的骨架class ScanCheckInPage extends StatelessWidget { Futurevoid _handleScanResult(String result) async { // 解析二維碼中的打印店ID和訂單號(hào) final parsed await _parseQrCode(result); // 調(diào)用打卡接口向 Provider 寫(xiě)入新記錄 _checkInProvider.addRecord(parsed); // 跳轉(zhuǎn)到打卡結(jié)果頁(yè) Navigator.push(...); } override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(掃碼打卡)), body: Column( children: [ ScannerView(onScanned: _handleScanResult), ConsumerCheckInState( builder: (context, state, child) { return Text(今日已打卡 ${state.todayCount} 次); }, ), ], ), ); } }掃碼頁(yè)需要注意一個(gè)攝像頭權(quán)限的適配問(wèn)題鴻蒙的權(quán)限彈窗和安卓的權(quán)限請(qǐng)求不是同一套機(jī)制直接用現(xiàn)成插件很可能在鴻蒙上拿不到返回結(jié)果。我在鴻蒙真機(jī)上遇到的情況是插件可以打開(kāi)攝像頭畫(huà)面但識(shí)別到二維碼后回調(diào)遲遲不觸發(fā)。排查下來(lái)發(fā)現(xiàn)是插件內(nèi)部對(duì)鴻蒙授權(quán)結(jié)果的處理還沒(méi)有對(duì)齊后面通過(guò)修改插件的原生側(cè)代碼才解決。商家端則是訂單確認(rèn)頁(yè)。商家登錄后可以看到待確認(rèn)的打卡列表每一條都關(guān)聯(lián)具體的打印訂單點(diǎn)擊確認(rèn)后狀態(tài)從待確認(rèn)變成已確認(rèn)統(tǒng)計(jì)報(bào)表同步刷新。這個(gè)頁(yè)面用到了 Provider 的Consumer來(lái)監(jiān)聽(tīng)整個(gè)打卡列表的狀態(tài)變化當(dāng)學(xué)生端新增打卡記錄時(shí)商家端的列表會(huì)自動(dòng)插入新行不需要手動(dòng)去刷新接口。如果兩個(gè)角色所在的設(shè)備不是同一臺(tái)就需要后端接口配合長(zhǎng)連接或輪詢來(lái)同步。我的第一版實(shí)現(xiàn)是純前端狀態(tài)管理后來(lái)加了 WebSocket 推送才真正做到學(xué)生掃碼后商家端秒級(jí)顯示。這里如果你用 Provider 只做前端狀態(tài)很容易忽略后端同步的問(wèn)題我建議在設(shè)計(jì)階段就把接口推送納入打卡鏈路不要只圖前端省事。管理員端是報(bào)表頁(yè)本來(lái)打算用圖表庫(kù)畫(huà)柱狀圖后來(lái)發(fā)現(xiàn)打印店的實(shí)際需求就是看一張數(shù)字匯總表哪個(gè)時(shí)段人多、哪個(gè)套餐打印量高就夠了。圖表反而花哨且不實(shí)用最后我就用簡(jiǎn)單的列表加數(shù)字卡片實(shí)現(xiàn)了。6. flutter 真機(jī)調(diào)試記錄連接、構(gòu)建與熱重載的避坑清單真機(jī)調(diào)試是這次開(kāi)發(fā)里讓我最有收獲的部分也是報(bào)錯(cuò)最多的部分。這里記錄的每一條都是我實(shí)際踩到過(guò)的不是從文檔里抄來(lái)的理論。先說(shuō)說(shuō)設(shè)備連接。鴻蒙真機(jī)連接電腦調(diào)試和安卓一樣需要開(kāi)啟開(kāi)發(fā)者模式但有個(gè)細(xì)節(jié)鴻蒙系統(tǒng)默認(rèn)把USB 調(diào)試選項(xiàng)藏在更深的層級(jí)里你需要連點(diǎn)版本號(hào)進(jìn)入開(kāi)發(fā)者選項(xiàng)再額外打開(kāi)USB 調(diào)試下面的僅充電模式下允許 ADB 調(diào)試開(kāi)關(guān)。如果不開(kāi)這個(gè)電腦端檢測(cè)到的設(shè)備狀態(tài)永遠(yuǎn)是 offlineflutter devices里看不到設(shè)備更別提跑項(xiàng)目了。Exhibit 連接檢查的常用命令# 查看已連接的設(shè)備列表 flutter devices # 查看鴻蒙設(shè)備的 hdc 連接狀態(tài) hdc list targets # 如果離線可以嘗試重啟 hdc 服務(wù) hdc kill hdc start連接成功后跑項(xiàng)目大概率會(huì)遇到第一個(gè)報(bào)錯(cuò)Gradle 或構(gòu)建鏈配置問(wèn)題。這個(gè)報(bào)錯(cuò)在熱搜詞里也出現(xiàn)過(guò)you are applying flutters main gradle plugin imperatively using the apply(s,e/flutter (31173))大致意思是 Flutter 的 Gradle 插件和工程中已有的插件配置方式?jīng)_突。這個(gè)問(wèn)題的解決方式很簡(jiǎn)單在android/settings.gradle和android/build.gradle中把 Flutter 插件從apply腳本式改成現(xiàn)代插件聲明式。具體來(lái)說(shuō)在settings.gradle里加上pluginManagement的倉(cāng)庫(kù)配置然后在各個(gè)模塊的build.gradle里通過(guò)plugins { id com.android.application }方式聲明不再使用apply plugin:。Exhibit 關(guān)鍵配置片段pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } }改完配置文件后記得在命令行執(zhí)行清理構(gòu)建flutter clean flutter pub get flutter run如果只改配置不執(zhí)行flutter clean舊構(gòu)建緩存仍然生效報(bào)警也不會(huì)消失。熱重載這塊也值得一提。flutter run在鴻蒙真機(jī)上的熱重載體驗(yàn)總體還行但有個(gè)小問(wèn)題改了原生側(cè)代碼比如修改了某個(gè)鴻蒙原生模塊的 Kotlin 代碼之后普通的熱重載不會(huì)生效必須重新編譯運(yùn)行。我后來(lái)養(yǎng)成習(xí)慣純 Dart 邏輯的改動(dòng)直接熱重載涉及原生能力的改動(dòng)一律執(zhí)行完整重啟別省那點(diǎn)時(shí)間。更隱蔽的是定位權(quán)限。打印店打卡場(chǎng)景里打卡時(shí)需要記錄位置學(xué)生每次掃碼前要保證定位權(quán)限已開(kāi)啟。鴻蒙系統(tǒng)在定位權(quán)限上比安卓更嚴(yán)格定位服務(wù)需要同時(shí)在應(yīng)用層和系統(tǒng)層同時(shí)授權(quán)。我在真機(jī)上遇到的坑是第一次授權(quán)彈窗點(diǎn)擊允許后應(yīng)用確實(shí)拿到了權(quán)限但一旦切后臺(tái)再回前臺(tái)權(quán)限狀態(tài)會(huì)被重置。最后繞過(guò)的辦法是在頁(yè)面onResume里周期性地重新檢測(cè)權(quán)限狀態(tài)如果沒(méi)有權(quán)限就彈窗引導(dǎo)用戶去設(shè)置頁(yè)手動(dòng)開(kāi)啟。7. 性能優(yōu)化Impeller 渲染引擎在鴻蒙設(shè)備上的表現(xiàn)Flutter 的渲染引擎這幾年的變化很大從 Skia 逐步切換到 Impeller。Impeller 是為高性能圖形渲染設(shè)計(jì)的核心優(yōu)勢(shì)是預(yù)編譯著色器從根本上解決了 Skia 在部分設(shè)備上首次渲染掉幀的問(wèn)題。打印店打卡應(yīng)用界面不算復(fù)雜動(dòng)畫(huà)也不多但掃碼頁(yè)面的相機(jī)預(yù)覽和結(jié)果頁(yè)的列表滾動(dòng)都依賴穩(wěn)定的渲染幀率Impeller 在這類場(chǎng)景下的表現(xiàn)明顯比 Skia 順滑。鴻蒙真機(jī)的 GPU 型號(hào)五花八門(mén)老款平板的 GPU 驅(qū)動(dòng)對(duì) Skia 的兼容性尤其差滾動(dòng)列表時(shí)偶爾出現(xiàn)白屏閃爍。切到 Impeller 之后這類問(wèn)題幾乎消失。切換方式是在main.dart里配置渲染引擎參數(shù)void main() { // 啟用 Impeller 渲染引擎 const String.fromEnvironment(FLUTTER_ENGINE, defaultValue: impeller); runApp(const CheckInApp()); }實(shí)際構(gòu)建時(shí)也可以在運(yùn)行命令里指定flutter run --enable-impeller需要說(shuō)明的是Impeller 對(duì)鴻蒙的支持是一個(gè)漸進(jìn)的過(guò)程。如果遇到某個(gè)鴻蒙版本的驅(qū)動(dòng)不兼容 Impeller回退到 Skia 也只需要改一個(gè)環(huán)境變量不需要?jiǎng)訕I(yè)務(wù)代碼。這算是 Flutter 框架在設(shè)計(jì)上給自己留的后路對(duì)開(kāi)發(fā)者來(lái)說(shuō)很友好。另一個(gè)性能優(yōu)化點(diǎn)跟列表渲染有關(guān)。打卡記錄會(huì)隨著學(xué)生使用次數(shù)不斷累積如果直接用ListView.builder渲染全部數(shù)據(jù)長(zhǎng)列表一樣會(huì)卡。Flutter 的ListView.builder雖然做懶加載但如果不加緩存策略快速滑動(dòng)時(shí)還是會(huì)出現(xiàn)空白項(xiàng)。我給打卡列表加了一個(gè)cacheExtent參數(shù)并控制每個(gè)列表項(xiàng)的高度讓滾動(dòng)體驗(yàn)保持在流暢狀態(tài)。Exhibit 列表優(yōu)化代碼ListView.builder( cacheExtent: 500, itemCount: records.length, itemBuilder: (_, index) CheckInListItem(record: records[index]), )這套優(yōu)化在打印店高峰期特別有用。學(xué)生掃碼后商家端列表同時(shí)插入大量新記錄如果沒(méi)有緩存策略滑動(dòng)到標(biāo)記位置時(shí)頁(yè)面會(huì)短暫抖動(dòng)影響操作效率。8. 打包發(fā)布鴻蒙應(yīng)用簽名的細(xì)節(jié)與配置檢查打包發(fā)布是開(kāi)發(fā)流程的終點(diǎn)但也是問(wèn)題最多的地方。鴻蒙應(yīng)用打包有兩種模式調(diào)試包和發(fā)布包。調(diào)試包可以直接運(yùn)行在真機(jī)上但應(yīng)用圖標(biāo)、名稱、權(quán)限聲明都受限。發(fā)布包需要簽名簽名配置不對(duì)的話應(yīng)用在部分設(shè)備上會(huì)閃退。我按官方文檔配置簽名時(shí)遇到了一個(gè)細(xì)節(jié)問(wèn)題簽名文件路徑中的反斜杠在 Windows 環(huán)境下被錯(cuò)誤解析導(dǎo)致簽名失敗。解決辦法是把簽名配置寫(xiě)到build-profile.json5的signingConfigs節(jié)點(diǎn)里使用相對(duì)路徑并在構(gòu)建時(shí)打印出的日志中確認(rèn)生成的哈希值與配置的哈希值一致。發(fā)布前還需要檢查權(quán)限聲明。打卡應(yīng)用至少需要相機(jī)掃碼、位置記錄地點(diǎn)、網(wǎng)絡(luò)同步數(shù)據(jù)這三類權(quán)限。在鴻蒙的配置文件中權(quán)限聲明使用module.json5里的requestPermissions節(jié)點(diǎn)如果忘記聲明某條權(quán)限應(yīng)用運(yùn)行時(shí)調(diào)用對(duì)應(yīng)功能會(huì)直接閃退而且報(bào)錯(cuò)信息未必能指向權(quán)限問(wèn)題。Exhibit 權(quán)限聲明示例{ module: { requestPermissions: [ { name: ohos.permission.CAMERA }, { name: ohos.permission.LOCATION }, { name: ohos.permission.INTERNET } ] } }打包完成后建議先在一臺(tái)舊設(shè)備、一臺(tái)新設(shè)備上分別安裝測(cè)試。鴻蒙系統(tǒng)版本跨度比較大老機(jī)型對(duì) Impeller 和部分原生插件的支持與新機(jī)型有差異提前覆蓋能避免上線后被用戶投訴。我還遇到一個(gè)跟 WebView 相關(guān)的問(wèn)題。打卡應(yīng)用的打印訂單預(yù)覽需要在應(yīng)用內(nèi)展示 PDF 文件我最初用的是 Flutter 自帶的多媒體組件但在鴻蒙上對(duì) PDF 的支持不完整。后來(lái)?yè)Q成一個(gè)簡(jiǎn)單的原生視圖來(lái)承載 PDF 預(yù)覽才徹底解決。如果你也有類似的內(nèi)嵌文檔展示需求建議提前確認(rèn)好鴻蒙平臺(tái)是否有對(duì)應(yīng)可用的原生模塊別等到打包階段再返工。發(fā)布到鴻蒙應(yīng)用商店之前還需要審核應(yīng)用名稱和圖標(biāo)是否符合平臺(tái)規(guī)范。打卡應(yīng)用的圖標(biāo)和名稱可以根據(jù)自己的品牌設(shè)計(jì)但要注意不能使用包含誤導(dǎo)性或夸大宣傳的文案審核不通過(guò)很常見(jiàn)的就是名稱里出現(xiàn)官方系統(tǒng)級(jí)這類字眼需要仔細(xì)檢查。9. 常見(jiàn)編譯異常與問(wèn)題定位思路最后專門(mén)整理一下開(kāi)發(fā)過(guò)程中遇到的高頻編譯報(bào)錯(cuò)和定位思路。這部分是純粹的踩坑記錄每一條都可能讓你在排查時(shí)多花幾個(gè)小時(shí)。第一類是 Gradle 倉(cāng)庫(kù)拉取失敗。報(bào)錯(cuò)信息通常是一長(zhǎng)串無(wú)法解析的依賴路徑。這種問(wèn)題大多是網(wǎng)絡(luò)環(huán)境導(dǎo)致的配置國(guó)內(nèi)鏡像倉(cāng)庫(kù)可以緩解但要注意 Flutter 的公共依賴不僅存在 Maven 倉(cāng)庫(kù)還有 Google 的倉(cāng)庫(kù)需要同時(shí)配置多個(gè)鏡像源。在國(guó)內(nèi)服務(wù)器上下載依賴尤其容易出現(xiàn)中斷反復(fù)重試不如一次性把鏡像配好。第二類是 Dart 插件與原生模塊的版本不匹配。Flutter 生態(tài)里很多插件在鴻蒙上沒(méi)有現(xiàn)成的原生實(shí)現(xiàn)需要自己編寫(xiě)鴻蒙側(cè)的 Module 移植代碼。這個(gè)過(guò)程中報(bào)錯(cuò)最多的是No implementation found for method之類的錯(cuò)誤意思是 Dart 側(cè)調(diào)用了一個(gè)原生方法但原生側(cè)壓根沒(méi)有注冊(cè)。排查思路很簡(jiǎn)單先檢查原生代碼里是否實(shí)現(xiàn)了插件注冊(cè)的對(duì)應(yīng)方法再檢查工程里是否遺漏了dependencies塊里的插件引用最后檢查插件名是否與pubspec.yaml中的一致。第三類是第三方 SDK 接入問(wèn)題時(shí)出現(xiàn)的符號(hào)沖突。打卡應(yīng)用的登錄功能接入了第三方認(rèn)證 SDK在鴻蒙真機(jī)上編譯時(shí)提示 duplicate class。這個(gè)問(wèn)題的根源是第三方 SDK 與項(xiàng)目里的某個(gè)模塊存在同名類按照?qǐng)?bào)錯(cuò)信息中給出的路徑把其中一個(gè)模塊的依賴排除掉即可。定位編譯問(wèn)題有個(gè)通用思路先看最底層的報(bào)錯(cuò)行不要被上面的信息干擾然后定位報(bào)錯(cuò)涉及的是 Dart 層、原生層還是構(gòu)建工具層最后逐層排查。大多數(shù)報(bào)錯(cuò)都是構(gòu)建工具配置引發(fā)的和業(yè)務(wù)代碼無(wú)關(guān)不要一上來(lái)就懷疑自己的業(yè)務(wù)邏輯寫(xiě)錯(cuò)了。10. 從這次實(shí)戰(zhàn)里提煉出的幾點(diǎn)心得Flutter 做鴻蒙開(kāi)發(fā)的項(xiàng)目實(shí)踐到這里基本完整了。最后聊幾個(gè)我個(gè)人感受最深的地方。第一選好狀態(tài)管理方案能省掉很多組件通信的麻煩。打印店打卡這個(gè)業(yè)務(wù)說(shuō)大不大但涉及三個(gè)角色共享數(shù)據(jù)只要狀態(tài)設(shè)計(jì)不好后期每加一個(gè)頁(yè)面都要重新捋一遍數(shù)據(jù)流。Provider 在這個(gè)規(guī)模下恰到好處再?gòu)?fù)雜一點(diǎn)的場(chǎng)景我會(huì)考慮 Riverpod但普通業(yè)務(wù)真的沒(méi)必要為了技術(shù)而技術(shù)。第二鴻蒙適配的坑大部分在原生側(cè)不在 Flutter 側(cè)。Flutter 框架本身的跨端能力在鴻蒙平臺(tái)上比我預(yù)期中可靠真正讓人頭疼的是各種系統(tǒng)權(quán)限、原生插件、簽名打包的問(wèn)題。在做技術(shù)選型時(shí)最好先把接入的第三方能力全部在鴻蒙真機(jī)驗(yàn)證一遍再?zèng)Q定最終方案。第三行動(dòng)起來(lái)比爭(zhēng)論誰(shuí)更流行更重要。當(dāng)時(shí)團(tuán)隊(duì)內(nèi)部因?yàn)榧夹g(shù)選型反復(fù)開(kāi)了好幾次會(huì)現(xiàn)在回頭看真正推動(dòng)項(xiàng)目落地的不是某個(gè)框架的壓倒性優(yōu)勢(shì)而是團(tuán)隊(duì)在執(zhí)行過(guò)程中持續(xù)解決具體問(wèn)題的能力。如果你手頭正好有類似的校園場(chǎng)景應(yīng)用要做不妨先建一個(gè)空 Flutter 工程把掃碼、定位、列表這些核心能力在鴻蒙真機(jī)上跑通再逐步疊加業(yè)務(wù)邏輯。前期驗(yàn)證階段多花點(diǎn)時(shí)間后面整體開(kāi)發(fā)會(huì)順很多。