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