戰(zhàn):護(hù)理服務(wù)跨端適配與狀態(tài)管理)
1. 養(yǎng)老護(hù)理場(chǎng)景里的硬需求為什么這塊硬骨頭選了Flutter先說項(xiàng)目背景。我所在的團(tuán)隊(duì)接手了一個(gè)現(xiàn)代智慧養(yǎng)老平臺(tái)其中一塊核心業(yè)務(wù)是護(hù)理服務(wù)護(hù)理員需要在自己手邊的設(shè)備上查看當(dāng)日任務(wù)、完成上門簽到、采集老人健康數(shù)據(jù)、上報(bào)護(hù)理記錄遇到突發(fā)狀況還要能一鍵觸發(fā)SOS呼叫。設(shè)備形態(tài)很雜有老人家里的固定終端、護(hù)理員隨身攜帶的平板、管理端的大屏其中固定終端和平板預(yù)裝的是OpenHarmony系統(tǒng)。這就意味著我們沒法只寫一套Android App就收工必須直面OpenHarmony上能不能跑起來、跑得順不順的問題。當(dāng)時(shí)擺在面前的有三條路一是用ArkTS原生重寫一套二是讓Android APK直接兼容運(yùn)行三就是標(biāo)題里這條路線——Flutter for OpenHarmony用一套Flutter代碼同時(shí)覆蓋Android、iOS和OpenHarmony三個(gè)平臺(tái)。先解釋一下為什么ArkTS原生不是首選。項(xiàng)目里的業(yè)務(wù)邏輯不是一個(gè)頁面放兩個(gè)按鈕這么簡單護(hù)理任務(wù)流轉(zhuǎn)、排班、工單狀態(tài)機(jī)、健康數(shù)據(jù)圖表這些模塊加起來有幾十個(gè)頁面如果全用ArkTS重寫至少要多養(yǎng)一個(gè)前端團(tuán)隊(duì)而且后續(xù)Android端和鴻蒙端的業(yè)務(wù)要維護(hù)兩套邏輯迭代效率會(huì)非常難受。Android APK直接兼容運(yùn)行這個(gè)方案聽起來省事但實(shí)際在OpenHarmony設(shè)備上跑遇到高版本SDK的API差異、權(quán)限模型不一致、以及部分傳感器和藍(lán)牙服務(wù)拿不到數(shù)據(jù)排查成本反而比直接適配更高。于是我們認(rèn)真評(píng)估了Flutter for OpenHarmony這條路。Flutter本身是跨平臺(tái)UI框架Framework層和Engine層跟底層操作系統(tǒng)解耦只要社區(qū)和OpenHarmony官方把Embedder層的適配做扎實(shí)業(yè)務(wù)層的Dart代碼幾乎不用動(dòng)。事實(shí)也確實(shí)如此Flutter社區(qū)針對(duì)OpenHarmony的適配已經(jīng)能支撐日常業(yè)務(wù)開發(fā)我們最終確定用Flutter寫業(yè)務(wù)同時(shí)按需保留少量原生通道去調(diào)用系統(tǒng)能力比如撥打SOS電話、讀取定位、調(diào)用藍(lán)牙采集設(shè)備數(shù)據(jù)。這篇文章里我會(huì)把護(hù)理服務(wù)模塊從環(huán)境搭建、平臺(tái)適配到核心功能實(shí)現(xiàn)的完整鏈路講一遍重點(diǎn)放在EventChannel原生日志流、Cubit狀態(tài)管理方案、以及頁面切換狀態(tài)保留這幾個(gè)容易被坑的地方。如果你也在做OpenHarmony上的跨端應(yīng)用或者你所在團(tuán)隊(duì)正準(zhǔn)備把存量Flutter工程往鴻蒙生態(tài)上遷移這篇文章應(yīng)該能幫你少踩幾個(gè)真實(shí)的坑。2. Flutter for OpenHarmony的適配深度從環(huán)境搭建到工程結(jié)構(gòu)2.1 環(huán)境搭建里最容易被忽略的三個(gè)細(xì)節(jié)網(wǎng)上講Flutter安裝的教程一抓一大把但Flutter for OpenHarmony的環(huán)境搭建跟普通Flutter有幾個(gè)關(guān)鍵差異不是簡簡單單裝個(gè)SDK就能跑。第一OpenHarmony側(cè)需要安裝完整的SDK和配套工具鏈。它不是Android SDK那套需要用DevEco Studio來管理OpenHarmony SDK路徑和配置方式都不同。我們的實(shí)際做法是先把DevEco Studio裝好確認(rèn)里面的SDK能編譯出一個(gè)空工程再回過頭配置Flutter命令行。第二OpenHarmony SDK版本要與Flutter適配版本匹配。Flutter官方主分支對(duì)OpenHarmony的適配版本有對(duì)應(yīng)關(guān)系我們?cè)陧?xiàng)目里遇到過Flutter版本升級(jí)后工程能編譯但運(yùn)行時(shí)Engine層跟OpenHarmony系統(tǒng)庫不兼容的情況癥狀表現(xiàn)為頁面渲染偶發(fā)空白、點(diǎn)擊事件響應(yīng)延遲。后來把Flutter版本鎖在社區(qū)推薦的穩(wěn)定分支情況立刻好轉(zhuǎn)。這里我的建議是不要追最新要追最穩(wěn)等某個(gè)適配版本在社區(qū)跑了一段時(shí)間后再升。第三環(huán)境變量里必須顯式聲明OpenHarmony SDK路徑。這一步少了Flutter工具鏈就找不到設(shè)備命令行執(zhí)行flutter devices時(shí)只能看到模擬器里的Android而OpenHarmony設(shè)備永遠(yuǎn)處于offline狀態(tài)。當(dāng)時(shí)我在配置里加了類似這樣的設(shè)置export OHOS_SDK_HOME/path/to/ohos-sdk export DEVECO_SDK_HOME$OHOS_SDK_HOME配置完后執(zhí)行flutter doctor如果輸出里出現(xiàn)了OpenHarmony相關(guān)的工具鏈信息說明環(huán)境基本通了。別小看這幾行我在社區(qū)看到大量設(shè)備連不上flutter run找不到目標(biāo)設(shè)備的提問八成都是這一步?jīng)]做或者路徑寫錯(cuò)。2.2 工程結(jié)構(gòu)里那個(gè)叫ohos的目錄用Flutter創(chuàng)建跨端工程時(shí)默認(rèn)會(huì)生成android、ios、web這些平臺(tái)目錄。當(dāng)Flutter的OpenHarmony適配被激活后工程里會(huì)多出來一個(gè)ohos目錄里面是OpenHarmony原生工程的骨架由ArkTS和native代碼組成。這個(gè)目錄的角色就相當(dāng)于Android工程里的android目錄。開發(fā)時(shí)Dart業(yè)務(wù)代碼寫在lib目錄下這個(gè)跟普通Flutter完全一致。但有一個(gè)點(diǎn)要注意ohos目錄里的原生代碼很多組件默認(rèn)不是參與到構(gòu)建里的只有你顯式引用了對(duì)應(yīng)插件原生模塊才會(huì)被編譯進(jìn)去。這會(huì)帶來一個(gè)隱蔽問題你的Dart代碼在真機(jī)上跑到了一個(gè)功能但ohos工程里根本沒有對(duì)應(yīng)實(shí)現(xiàn)運(yùn)行時(shí)拋MissingPluginException排查起來非常容易懵。解決辦法是建立一份Flutter插件到ohos原生工程的映射清單。這個(gè)習(xí)慣我是后來踩了幾次坑才養(yǎng)成的尤其是當(dāng)團(tuán)隊(duì)里同時(shí)有人在改Android實(shí)現(xiàn)、有人在改ohos實(shí)現(xiàn)時(shí)沒有清單特別容易漏掉。2.3 平臺(tái)插件適配鴻蒙的完整流程以登錄SDK為例我們的App里需要集成一個(gè)第三方統(tǒng)一登錄SDK這個(gè)SDK原本只有Android和iOS版本鴻蒙端沒有現(xiàn)成的對(duì)接包。在H2標(biāo)題里提到的flutter平臺(tái)插件適配鴻蒙流程在這里得到了完整落地。我拆解成四步創(chuàng)建平臺(tái)接口、編寫鴻蒙端原生實(shí)現(xiàn)、用通道橋接、在Flutter側(cè)統(tǒng)一調(diào)用。第一步先定義Dart側(cè)的抽象接口。我們選用的是Federated Plugin的思路——Flutter社區(qū)里插件統(tǒng)一用這種模式一個(gè)app_side的接口包負(fù)責(zé)定義上層API具體實(shí)現(xiàn)由各個(gè)平臺(tái)去注冊(cè)。這樣Dart業(yè)務(wù)代碼不用關(guān)心底層是Android還是OpenHarmony只調(diào)用接口即可。第二步在ohos目錄里實(shí)現(xiàn)一個(gè)ArkTS類把登錄SDK的方法包一層。比如登錄方法底層要拉起SDK的登錄頁等待回調(diào)后把token通過回調(diào)返回。ArkTS的寫法跟TypeScript接近但對(duì)異步回調(diào)的約束更嚴(yán)一些所有原生回調(diào)都要通過通道轉(zhuǎn)發(fā)到Flutter側(cè)。第三步橋接。這里我用的是MethodChannelDart端發(fā)起登錄請(qǐng)求通過channel.invokeMethod觸發(fā)ArkTS側(cè)的login函數(shù)原生把登錄結(jié)果轉(zhuǎn)成JSON字符串再通過result.success返回。三端數(shù)據(jù)類型對(duì)齊是這一階段最常見的問題——Boolean在Dart里是bool在ArkTS里也可以是boolean但一旦某個(gè)方法返回的是數(shù)組套對(duì)象轉(zhuǎn)來轉(zhuǎn)去很容易丟字段。第四步在Flutter側(cè)寫一個(gè)統(tǒng)一入口讓業(yè)務(wù)只管調(diào)用LoginService.login()底層自動(dòng)路由到對(duì)應(yīng)平臺(tái)實(shí)現(xiàn)。跑通這個(gè)流程大概花了兩天時(shí)間其中一半時(shí)間耗在ArkTS側(cè)的異步回調(diào)線程切換上這個(gè)后面專門講。3. 護(hù)理服務(wù)的核心鏈路任務(wù)流轉(zhuǎn)、健康數(shù)據(jù)采集與SOS呼叫3.1 護(hù)理任務(wù)模塊的業(yè)務(wù)狀態(tài)機(jī)護(hù)理服務(wù)里第一個(gè)要落地的核心模塊是護(hù)理任務(wù)。這個(gè)模塊表面上看是一個(gè)帶列表和詳情頁的CRUD但實(shí)際背后是一個(gè)狀態(tài)機(jī)任務(wù)從待分配流轉(zhuǎn)到已接單護(hù)理員開始上門后變成服務(wù)中服務(wù)完成填寫記錄后變成已完成如果護(hù)理員臨時(shí)有事需要交接給別人還要有待轉(zhuǎn)單狀態(tài)。這幾個(gè)狀態(tài)之間不是隨便能跳的。比如待分配的任務(wù)不能被護(hù)理員直接改成已完成必須經(jīng)過接單動(dòng)作而服務(wù)中的任務(wù)如果老人臨時(shí)取消又得走已取消分支。設(shè)計(jì)這種狀態(tài)機(jī)的時(shí)候如果只是簡單地在頁面里if else判斷代碼會(huì)隨著狀態(tài)增加迅速腐化。我們?cè)贔lutter側(cè)用了枚舉加約束守衛(wèi)的方式enum CareTaskStatus { pending, // 待分配 assigned, // 已接單 inProgress, // 服務(wù)中 completed, // 已完成 cancelled, // 已取消 transferring // 待轉(zhuǎn)單 }每次狀態(tài)變更都走一個(gè)統(tǒng)一的transition方法在這個(gè)方法里用switch判斷當(dāng)前狀態(tài)和目標(biāo)狀態(tài)是否構(gòu)成合法遷移。一旦發(fā)現(xiàn)非法跳轉(zhuǎn)直接拋出異常并上報(bào)日志。這個(gè)設(shè)計(jì)在前期看起來有點(diǎn)用力過猛但等到接單、轉(zhuǎn)單、取消、完成這些業(yè)務(wù)流程全部串起來后受益非常明顯——每個(gè)頁面都不用重復(fù)校驗(yàn)狀態(tài)合法性只需要調(diào)transition方法。3.2 EventChannel把心率和血壓數(shù)據(jù)實(shí)時(shí)推到Flutter頁面護(hù)理服務(wù)里有一個(gè)關(guān)鍵場(chǎng)景護(hù)理員上門后需要用設(shè)備采集老人的心率、血壓、血氧數(shù)據(jù)。這些數(shù)據(jù)一部分來自藍(lán)牙穿戴設(shè)備一部分來自一體機(jī)。在OpenHarmony終端上藍(lán)牙和傳感器的能力都在原生層Flutter側(cè)拿不到必須通過通道橋接。數(shù)據(jù)采集場(chǎng)景適合用EventChannel而不是MethodChannel。MethodChannel是單向調(diào)用一次invoke對(duì)應(yīng)一次返回適合查一下狀態(tài)調(diào)一個(gè)接口這類場(chǎng)景而EventChannel建立的是持續(xù)的推送通道原生側(cè)可以持續(xù)向Flutter側(cè)發(fā)送事件比如每秒鐘上報(bào)一次心率讀數(shù)。這正好匹配健康數(shù)據(jù)采集的實(shí)時(shí)性要求。ArkTS原生側(cè)的思路是初始化一個(gè)EventSink然后把藍(lán)牙設(shè)備回調(diào)里的數(shù)據(jù)逐步寫入import { eventEmitter } from ohos.base; let heartRateEventSink: (data: string) void null; // 在SDK回調(diào)中持續(xù)推送數(shù)據(jù) function onHeartRateReceived(value: number) { if (heartRateEventSink) { heartRateEventSink(JSON.stringify({ bpm: value, timestamp: Date.now() })); } }Flutter側(cè)訂閱這段事件流的代碼相對(duì)簡單static const _eventChannel EventChannel( com.careapp/health_monitor ); StreamMapObject?, Object? _healthStream; void initHealthStream() { _healthStream _eventChannel.receiveBroadcastStream() .castMapObject?, Object?(); }拿到流數(shù)據(jù)后我在頁面里接了一個(gè)StreamBuilder把心率數(shù)值實(shí)時(shí)渲染成折線圖。這里有個(gè)實(shí)戰(zhàn)細(xì)節(jié)值得強(qiáng)調(diào)EventChannel的數(shù)據(jù)是異步到達(dá)的頁面如果切到后臺(tái)再回來流的訂閱關(guān)系可能會(huì)斷需要重新訂閱并做一次數(shù)據(jù)快照拉取。我是在頁面生命周期里處理恢復(fù)邏輯的具體方法后面講Navigator狀態(tài)時(shí)一起說。3.3 SOS緊急呼叫里最容易做錯(cuò)的一步護(hù)理服務(wù)中還有一個(gè)緊急場(chǎng)景——老人突發(fā)狀況護(hù)理員或老人本人按下SOS按鈕App需要立刻撥出預(yù)設(shè)的緊急聯(lián)系人電話同時(shí)把定位信息和簡單情況發(fā)送到管理后臺(tái)。這部分最初我們照搬了Android端的實(shí)現(xiàn)邏輯——點(diǎn)擊SOS后先通過網(wǎng)絡(luò)請(qǐng)求上報(bào)位置等請(qǐng)求返回成功后再調(diào)起撥號(hào)。實(shí)際在OpenHarmony真機(jī)上測(cè)試時(shí)發(fā)現(xiàn)一個(gè)問題網(wǎng)絡(luò)請(qǐng)求有時(shí)會(huì)卡住幾秒老人和護(hù)理員在緊張狀態(tài)下會(huì)反復(fù)點(diǎn)擊按鈕導(dǎo)致重復(fù)上報(bào)。后來改成撥號(hào)優(yōu)先異步上報(bào)點(diǎn)擊按鈕后先立即通過MethodChannel調(diào)起系統(tǒng)撥號(hào)同時(shí)后臺(tái)異步發(fā)送定位數(shù)據(jù)并且加了防重復(fù)點(diǎn)擊的節(jié)流器兩秒內(nèi)的重復(fù)點(diǎn)擊全部忽略。這個(gè)改動(dòng)邏輯很小但用戶的體感完全不一樣。撥號(hào)本身需要申請(qǐng)系統(tǒng)權(quán)限這里有一個(gè)跟Android的差異點(diǎn)OpenHarmony的權(quán)限模型更細(xì)撥號(hào)、定位、藍(lán)牙分別對(duì)應(yīng)不同的權(quán)限組而且部分權(quán)限需要用戶到系統(tǒng)設(shè)置里手動(dòng)授權(quán)App內(nèi)彈窗申請(qǐng)的能力有限。我們的做法是在首次啟動(dòng)時(shí)通過引導(dǎo)頁把權(quán)限一次性申請(qǐng)清楚避免緊急場(chǎng)景下彈窗打擾。4. 狀態(tài)管理與組件通信Cubit方案和頁面狀態(tài)保留的真相4.1 為什么CI方案我用Cubit而不是Bloc在Flutter社區(qū)里狀態(tài)管理一直是討論熱度最高的話題。我們的護(hù)理服務(wù)模塊最終采用了Cubit這是Bloc庫的精簡版。選擇它不是因?yàn)锽loc不好而是在這個(gè)項(xiàng)目的實(shí)際場(chǎng)景里Cubit的抽象層次更合適。Bloc的核心是把事件和狀態(tài)完全拆開通過事件驅(qū)動(dòng)狀態(tài)變化適合大型團(tuán)隊(duì)、復(fù)雜業(yè)務(wù)邏輯里需要嚴(yán)格流程管控的場(chǎng)景。但它的儀式感也帶來額外的代碼量每個(gè)交互都要定義Event類、寫mapEventToState方法、維護(hù)多個(gè)文件。而在護(hù)理服務(wù)模塊中很多邏輯是頁面觸發(fā)動(dòng)作數(shù)據(jù)變一下UI刷新用Bloc有點(diǎn)殺雞用牛刀。Cubit保留了Bloc的State流式管理能力但去掉了Event層直接通過方法調(diào)用來改變狀態(tài)。比如接單操作代碼如下class CareTaskCubit extends CubitCareTaskState { CareTaskCubit(this._repository) : super(CareTaskInitial()); final CareTaskRepository _repository; Futurevoid acceptTask(String taskId) async { emit(CareTaskLoading()); try { final task await _repository.acceptTask(taskId); emit(CareTaskLoaded(task)); } catch (e) { emit(CareTaskError(接單失敗請(qǐng)重試)); } } }這個(gè)寫法非常直觀業(yè)務(wù)人員看著代碼就能知道點(diǎn)接單后發(fā)生了什么。而如果用Bloc同樣的流程還要多一層SealedEvent類的定義。所以我的經(jīng)驗(yàn)是如果團(tuán)隊(duì)里Flutter水平參差不齊Cubit的接受成本更低如果你在做一個(gè)流程嚴(yán)苛的交易系統(tǒng)再上Bloc不遲。4.2 Navigator切換頁面后會(huì)丟失狀態(tài)嗎——這個(gè)問題要分兩半看這個(gè)熱搜詞我在項(xiàng)目里真實(shí)遇到了。護(hù)理員正在填寫一條護(hù)理記錄填到一半有人打電話來接完電話回來發(fā)現(xiàn)剛才草稿全沒了氣得直冒火。技術(shù)上這是頁面被銷毀導(dǎo)致State丟失的問題要理解它必須搞清楚Flutter頁面棧的機(jī)制。Flutter里用Navigator.push跳轉(zhuǎn)新頁面時(shí)默認(rèn)情況下原頁面并沒有銷毀它只是被壓到路由棧里State對(duì)象還活著。真正導(dǎo)致狀態(tài)丟失的常見原因是頁面已經(jīng)被pop銷毀了。比如護(hù)理員從任務(wù)列表點(diǎn)進(jìn)詳情頁在詳情頁里填草稿然后誤觸返回詳情頁出棧銷毀草稿自然沒了。另一種情況更隱蔽頁面還在棧里但系統(tǒng)內(nèi)存緊張時(shí)Flutter會(huì)觸發(fā)重建如果State里的數(shù)據(jù)沒有持久化依然會(huì)丟失。我們項(xiàng)目里護(hù)理記錄草稿用的是普通內(nèi)存變量后來改成每次輸入變化都寫入本地?cái)?shù)據(jù)庫再在頁面重建時(shí)恢復(fù)。具體做法是使用SharedPreferences做自動(dòng)保存每次EditController的內(nèi)容變化后防抖保存到本地頁面initState時(shí)讀回來回填。/// 草稿自動(dòng)保存的核心邏輯 textController.addListener(() { _debounce(() { prefs.setString(draft_brief, textController.text); }, 400ms); });至于頁面確實(shí)還留在棧里但是UI沒刷新這種偽丟失本質(zhì)是狀態(tài)更新后沒有通知到當(dāng)前頁面這個(gè)屬于組件通信問題下面繼續(xù)講。4.3 組件通信的四種主流方案和養(yǎng)老場(chǎng)景下的取舍Flutter組件間通信我實(shí)際用的方案按頻次排列ValueNotifier、Cubit、EventBus、InheritedWidget。ValueNotifier適合單節(jié)點(diǎn)狀態(tài)比如一個(gè)開關(guān)、一個(gè)滑塊頁面內(nèi)部用沒問題Cubit適合跨頁面共享某個(gè)業(yè)務(wù)領(lǐng)域的狀態(tài)比如護(hù)理任務(wù)的當(dāng)前狀態(tài)EventBus適合完全解耦的事件廣播比如老人數(shù)據(jù)已更新、刷新地圖標(biāo)記這類不關(guān)心誰監(jiān)聽的消息InheritedWidget適合主題、語言包這類全局靜態(tài)配置。在老項(xiàng)目里人們經(jīng)常用一個(gè)全局靜態(tài)類保存所有狀態(tài)頁面之間互相讀寫寫著寫著就分不清是誰改了誰排查問題非常痛苦。我們這次從一開始就規(guī)定凡是兩個(gè)以上頁面都要讀寫的狀態(tài)一律丟進(jìn)Cubit頁面里通過context.read和context.watch去讀寫禁止直接操作全局變量。4.4 一次真實(shí)的詭異數(shù)據(jù)不同步排查過程項(xiàng)目聯(lián)調(diào)時(shí)出現(xiàn)一個(gè)怪問題護(hù)理員在A頁面完成了接單操作回到B頁面刷新后B頁面上的任務(wù)狀態(tài)還是待接單。模型上看B頁面監(jiān)聽了同一個(gè)Cubit實(shí)例理論上狀態(tài)變化會(huì)逐幀廣播。排查了半天發(fā)現(xiàn)問題不在狀態(tài)管理而在B頁面用的Cubit對(duì)象是從一個(gè)每次build都新建實(shí)例的代碼塊里拿的。如果每次刷新都new一個(gè)Cubit那后續(xù)頁面監(jiān)聽的是新實(shí)例而A頁面操作的是舊實(shí)例兩者根本沒連上。這一類問題極容易發(fā)生在組件內(nèi)嵌帶參數(shù)初始化的場(chǎng)景?,F(xiàn)在我要求所有跨頁面共享的Cubit實(shí)例必須從頂層依賴注入容器里統(tǒng)一獲取任何地方都不得直接new。如果你也遇到明明監(jiān)聽了狀態(tài)卻不更新的問題先去檢查是不是每個(gè)build里都新new了狀態(tài)對(duì)象比排查UI邏輯快得多。5. OpenHarmony真機(jī)聯(lián)調(diào)中的編譯問題與性能優(yōu)化5.1 could not determine the dependencies of task這個(gè)報(bào)錯(cuò)到底是誰的鍋Flutter for OpenHarmony在打包和編譯階段搜索引擎里高頻出現(xiàn)的報(bào)錯(cuò)是類似could not determine the dependencies of task :app:compileDebugJavaWithJavac這一類任務(wù)依賴無法解析的錯(cuò)誤。第一次遇到時(shí)我以為是Flutter工程的問題折騰半天發(fā)現(xiàn)根因在Gradle依賴?yán)∩?。OpenHarmony工程的構(gòu)建鏈Flutter層會(huì)生成一個(gè)Gradle工程同時(shí)基于ArkTS的Hvigor構(gòu)建系統(tǒng)。當(dāng)兩套構(gòu)建系統(tǒng)的版本聲明和依賴倉庫不一致時(shí)Gradle解析任務(wù)依賴就會(huì)失敗。我們踩過的具體原因是工程里的ohos目錄帶了一個(gè)獨(dú)立的Gradle配置它引用的插件倉庫在部分網(wǎng)絡(luò)環(huán)境下拉取超時(shí)Gradle就干脆報(bào)依賴無法解析。解決辦法不神秘分三步第一步檢查Flutter工程根目錄的gradle-wrapper.properties確認(rèn)Gradle版本和已安裝版本一致第二步檢查倉庫源配置把OpenHarmony工程需要的三個(gè)倉庫——mavenCentral、華為開源倉、以及Gradle插件倉——全部顯式聲明第三步把依賴緩存目錄清掉重新構(gòu)建。項(xiàng)目里我們最終是通過整理統(tǒng)一版本的Gradle配置文件解決社區(qū)里有人說升級(jí)Gradle也能解決但升級(jí)有連帶風(fēng)險(xiǎn)我的經(jīng)驗(yàn)是優(yōu)先排查倉庫源。5.2 Flutter SDK版本兼容警告和一例打包崩潰的處理隨著Flutter版本迭代命令行里會(huì)頻繁出現(xiàn)the current configured Flutter SDK is not known to be fully supported這類提示。這個(gè)警告的意思是當(dāng)前Flutter版本和工程里某些插件適配版本沒有經(jīng)過官方全量測(cè)試有可能行為不一致。我記得有一次打包時(shí)崩潰在java.lang.AssertionError報(bào)錯(cuò)信息指向一個(gè)閉包無法關(guān)閉。這個(gè)問題的真實(shí)原因是Flutter引擎和OpenHarmony版本的一個(gè)已知兼容沖突。當(dāng)時(shí)社區(qū)里還沒有現(xiàn)成教程我排查兩天后是把Flutter的引擎回退到了鴻蒙適配分支的上一個(gè)穩(wěn)定tag崩潰就消失了。從那以后我養(yǎng)成一個(gè)習(xí)慣OpenHarmony工程里做任何Flutter版本升級(jí)都要先在測(cè)試設(shè)備上跑一遍核心用例再推給其他同事不要以命令行不報(bào)錯(cuò)作為升級(jí)成功的標(biāo)準(zhǔn)。5.3 性能層面值得關(guān)注的點(diǎn)Impeller和頁面卡頓OpenHarmony的Flutter渲染初始化、頁面切換的過程如果沒有做性能優(yōu)化在低端設(shè)備上很容易觀察到卡頓。熱搜詞里有一個(gè)flutter impeller這是Flutter渲染引擎的一個(gè)實(shí)現(xiàn)。Impeller的核心優(yōu)勢(shì)是避免了傳統(tǒng)Skia渲染的每幀著色器編譯卡頓這在跨端場(chǎng)景里體驗(yàn)差異比較明顯。但OpenHarmony適配分支上Impeller的支持狀態(tài)要以實(shí)際測(cè)試為準(zhǔn)如果設(shè)備上開啟后出現(xiàn)異?;ㄆ辆突赝说絊kia渲染。護(hù)理服務(wù)里那個(gè)實(shí)時(shí)心率折線圖最初在低端平板上刷新時(shí)掉幀明顯。后來我把刷新頻率從每幀刷新降到每秒三幀再把圖表數(shù)據(jù)的點(diǎn)集采樣壓縮設(shè)備負(fù)載立刻降下來。性能優(yōu)化這種事個(gè)中體會(huì)就是跨端框架的渲染性能上限不低但如果你不做節(jié)流和控制再強(qiáng)的框架也頂不住無腦刷新。5.4 真機(jī)聯(lián)調(diào)時(shí)最常見的三類問題OpenHarmony真機(jī)聯(lián)調(diào)跟Android模擬器調(diào)試有幾點(diǎn)明顯的不同按遇到概率排序第一設(shè)備連接不穩(wěn)定。用命令行跑flutter run設(shè)備偶爾會(huì)斷連日志直接中斷。這個(gè)是OpenHarmony調(diào)試服務(wù)自動(dòng)休眠導(dǎo)致的可以通過在設(shè)備端持續(xù)保持屏幕常亮來緩解設(shè)置電源為不休眠。第二權(quán)限彈窗容易誤觸。開發(fā)階段多次安裝應(yīng)用權(quán)限彈窗會(huì)重復(fù)彈出一旦誤點(diǎn)拒絕后續(xù)即使重新安裝部分權(quán)限也不會(huì)再自動(dòng)向用戶申請(qǐng)需要在系統(tǒng)設(shè)置里手動(dòng)打開。涉及定位和藍(lán)牙的模塊我建議在開發(fā)階段就做一個(gè)權(quán)限自檢頁面打開直接顯示哪些權(quán)限已授權(quán)、哪些被拒絕、哪個(gè)入口去設(shè)置里打開節(jié)省的時(shí)間遠(yuǎn)超寫這個(gè)頁面的投入。第三日志過濾規(guī)則不同。Dart側(cè)的print輸出和ArkTS側(cè)的hilog日志在OpenHarmony上不全是同一個(gè)出口很多時(shí)候你看到Flutter側(cè)沒有報(bào)錯(cuò)但原生其實(shí)已經(jīng)崩了。我的習(xí)慣是出問題先在ArkTS側(cè)打關(guān)鍵日志再回Flutter側(cè)對(duì)比不要只盯著Dart控制臺(tái)。6. 模塊設(shè)計(jì)里那份原生橋接清單的價(jià)值前面斷斷續(xù)續(xù)提到了不少踩坑經(jīng)歷但這里我還是想再單獨(dú)強(qiáng)調(diào)一個(gè)工程習(xí)慣維護(hù)原生橋接清單。一份清單記錄三個(gè)平臺(tái)下達(dá)成的通道協(xié)議和參數(shù)格式。我們護(hù)理服務(wù)模塊最終涉及到的橋接通道大概有十來個(gè)健康數(shù)據(jù)EventChannel、撥號(hào)MethodChannel、定位調(diào)用、藍(lán)牙連接、文件上傳原生壓縮等等。每加一個(gè)新通道如果不在清單里及時(shí)更新參數(shù)格式和回調(diào)類型等另外兩個(gè)平臺(tái)的同學(xué)各寫各的聯(lián)調(diào)階段就會(huì)冒出來一模一樣的字段、類型卻對(duì)不上的問題。實(shí)際落地的清單長這樣通道名類型參數(shù)格式原生平臺(tái)狀態(tài)health_monitorEventChannelJSON字符串OpenHarmony已聯(lián)調(diào)sos_callMethodChannelcontactId, lat, lngOpenHarmony已聯(lián)調(diào)location_updateMethodChannelJSON字符串OpenHarmony待聯(lián)調(diào)bluetooth_scanEventChannelJSON字符串OpenHarmony開發(fā)中有了這張表團(tuán)隊(duì)溝通成本會(huì)低很多。另外提醒一點(diǎn)通道名稱和參數(shù)結(jié)構(gòu)在正式使用后輕易不要改要改也要先改清單再改代碼否則兩端的同學(xué)完全無法感知對(duì)方變了。7. 護(hù)理服務(wù)上線前后我的一些復(fù)盤這套Flutter for OpenHarmony的護(hù)理服務(wù)App從開發(fā)到完成真機(jī)驗(yàn)收整個(gè)流程走下來我對(duì)跨端適配鴻蒙有了新的認(rèn)識(shí)。第一個(gè)體會(huì)是Flutter的統(tǒng)一UI能力在OpenHarmony上確實(shí)是成立的。幾十個(gè)頁面、復(fù)雜的狀態(tài)流轉(zhuǎn)、和有原生通信的模塊用一套Dart代碼覆蓋三個(gè)平臺(tái)效率層面的收益很明顯。真正拉開差距的是你對(duì)平臺(tái)差異的理解深度。OpenHarmony不是Android它的權(quán)限模型、構(gòu)建系統(tǒng)、調(diào)試方式、ArkTS的語言約束都自成體系。盲目地把Android經(jīng)驗(yàn)照搬過來會(huì)在細(xì)節(jié)處反復(fù)碰壁。第二個(gè)體會(huì)是狀態(tài)的持久化設(shè)計(jì)要提前做不能等到上線前再補(bǔ)。像護(hù)理記錄草稿、任務(wù)列表的篩選條件、健康數(shù)據(jù)的緩存這些看起來不起眼的功能如果一開始就規(guī)劃好本地存儲(chǔ)方案后期會(huì)省掉大量返工時(shí)間。我們項(xiàng)目里中途補(bǔ)了個(gè)本地緩存層代價(jià)是改了十幾個(gè)頁面的數(shù)據(jù)讀取邏輯。第三個(gè)體會(huì)是關(guān)于團(tuán)隊(duì)協(xié)作的。跨端開發(fā)最忌諱的是我這邊能跑就行。同樣的Dart代碼Android上的行為和OpenHarmony上的行為未必一致?,F(xiàn)在我們的做法是每個(gè)功能點(diǎn)至少要在兩個(gè)真實(shí)設(shè)備上跑一遍才算完成不允許只在一個(gè)平臺(tái)驗(yàn)證后直接標(biāo)已通過。這當(dāng)然會(huì)拉長單個(gè)需求的測(cè)試時(shí)間但上線后的穩(wěn)定性提升非常明顯。如果你正在評(píng)估Flutter for OpenHarmony或者已經(jīng)被分配了類似的項(xiàng)目我建議你從我們這套方案里直接拿走的不是具體代碼而是那幾條基于真實(shí)場(chǎng)景得出來的經(jīng)驗(yàn)版本鎖定優(yōu)先于版本最新、橋接通道必須維護(hù)清單、狀態(tài)和導(dǎo)航的設(shè)計(jì)要提前想清楚數(shù)據(jù)生命周期。這些點(diǎn)沒有在官方文檔里高亮但它們決定了項(xiàng)目能不能平穩(wěn)落地。