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