:口腔護理App刷牙記錄功能全解析)
最近做的一個項目——用Flutter在OpenHarmony設(shè)備上實現(xiàn)一款口腔護理App核心是把“刷牙記錄”這套完整流程真實跑起來。這個組合目前不算主流OpenHarmony的Flutter適配資料少很多問題得自己摸但做完以后回頭看整體的技術(shù)路線是走得通的。如果你正在評估鴻蒙生態(tài)里做跨端應(yīng)用或者想看看Flutter除了安卓和iOS還能適配哪些系統(tǒng)這篇實戰(zhàn)記錄應(yīng)該能幫你省不少試錯時間。項目本身不復(fù)雜功能上就是一個帶計時、分區(qū)提醒、記錄留存和統(tǒng)計展示的刷牙工具。真正的難點集中在三塊Flutter在OpenHarmony上的工程配置、傳感器和攝像頭這類硬件能力的橋接、以及記錄數(shù)據(jù)從產(chǎn)生到展示的完整鏈路設(shè)計。下面按我實際開發(fā)的順序把每個環(huán)節(jié)拆開講包括遇到的具體報錯和解決辦法。1. 項目背景與整體設(shè)計思路1.1 為什么是FlutterOpenHarmony選這個組合不是拍腦袋。先說場景需求口腔護理類App很少只做一個平臺開發(fā)者通常希望一套代碼同時覆蓋手機、平板甚至未來的智能家居帶屏設(shè)備。OpenHarmony作為開源生態(tài)設(shè)備覆蓋面正在擴大如果每個目標系統(tǒng)都寫一套原生維護成本翻倍。Flutter的跨端能力剛好可以承擔UI層和業(yè)務(wù)邏輯層的復(fù)用只在系統(tǒng)能力調(diào)用時做平臺適配。另一個原因是OpenHarmony官方和社區(qū)已經(jīng)維護了一套Flutter適配SDK本質(zhì)上是在OpenHarmony系統(tǒng)上實現(xiàn)了Flutter引擎和Dart運行時。這就意味著你平時熟悉的Widget、狀態(tài)管理、動畫、路由體系都能繼續(xù)用不需要把業(yè)務(wù)代碼推翻重寫。相比直接用原生ArkTS開發(fā)對Flutter團隊來說學習成本低很多團隊現(xiàn)有的Dart代碼資產(chǎn)也能平移過來。當然它和安卓平臺還是有差異的。最直觀的區(qū)別在于插件體系不通用第三方Flutter插件大多針對安卓和iOS做了原生實現(xiàn)OpenHarmony上要用得自己補平臺通道代碼或者找專門適配OpenHarmony的插件版本。所以在做技術(shù)選型時就要有心理準備通用UI問題不大凡是涉及原生能力的都得預(yù)留適配時間。這個項目里我沒有用一堆重型插件核心邏輯全部自己實現(xiàn)目的就是為了減少不可控的外部依賴。1.2 功能模塊與數(shù)據(jù)流怎么拆這款口腔護理App最終拆成了五個模塊首頁概覽展示今日刷牙狀態(tài)、連續(xù)打卡天數(shù)、最近一次刷牙得分。刷牙計時頁核心交互頁包含開始/暫停/結(jié)束、分區(qū)提示、實時計時。記錄列表按時間倒序排列的歷史刷牙記錄支持下拉刷新。統(tǒng)計圖表頁以周/月維度展示刷牙時長、頻次、分區(qū)覆蓋率。設(shè)置頁提醒開關(guān)、目標時長配置、數(shù)據(jù)導出。模塊劃分遵循一個原則數(shù)據(jù)流的走向是單向的。頁面只負責展示和觸發(fā)事件業(yè)務(wù)邏輯全部收斂到Service層狀態(tài)通過Provider暴露給Widget層。刷牙記錄從產(chǎn)生到展示的路徑是這樣的傳感器采集動作數(shù)據(jù) 計時器累計時長 → 狀態(tài)機流轉(zhuǎn)判定結(jié)束 → 組裝記錄對象 → Hive本地寫入 → 通過ChangeNotifier通知列表頁刷新。這條鏈路在分模塊開發(fā)時很清晰后期排查問題也能沿著數(shù)據(jù)流快速定位。為什么不用更重的狀態(tài)管理方案項目體量決定了沒有必要。頁面之間需要共享的數(shù)據(jù)主要是“當前刷牙記錄”、“歷史記錄集合”、“用戶設(shè)置”這三類用ChangeNotifier加Provider足夠撐住。如果一開始就上Bloc或者Riverpod反而會因為概念過多拖慢開發(fā)節(jié)奏。這個選擇沒有對錯只看項目規(guī)模和團隊熟練度。數(shù)據(jù)存儲上我選了Hive沒有用sqflite。原因有兩個一是Hive純Dart實現(xiàn)不依賴原生SQLite在OpenHarmony上的兼容風險更小二是刷牙記錄本身是結(jié)構(gòu)化但不高頻的數(shù)據(jù)KV存儲足夠。如果你后續(xù)要做復(fù)雜統(tǒng)計查詢再考慮在OpenHarmony側(cè)用原生數(shù)據(jù)庫加通道封裝也不遲。2. 環(huán)境搭建與工程創(chuàng)建2.1 用AS創(chuàng)建Flutter項目并接入OpenHarmony SDK這個環(huán)節(jié)是新手最容易卡住的地方。說來好笑很多人拿到OpenHarmony設(shè)備后第一反應(yīng)是打開DevEco Studio但如果你用Flutter開發(fā)日常編輯器依然是Android Studio更順手寫Dart代碼、跑靜態(tài)分析、調(diào)試UI都很方便。步驟上分兩條線并行用flutter create創(chuàng)建標準Flutter工程項目名我用的是tooth_clock。把Flutter SDK切換成OpenHarmony適配分支或者通過配置把ohos目錄接入工程。實操時我的做法是先用Android Studio新建一個Flutter項目確認Dart和Flutter插件正常工作再手動給工程補充OpenHarmony相關(guān)的構(gòu)建配置。用AS創(chuàng)建項目的核心優(yōu)勢是它能自動識別本機安裝的Flutter SDK和設(shè)備列表但這里有個坑Android Studio默認只識別安卓設(shè)備OpenHarmony設(shè)備需要單獨配置設(shè)備連接和簽名信息否則你連設(shè)備列表都看不到。具體配置點有這幾個Flutter SDK路徑要指向適配OpenHarmony的版本而不是純原生Flutter SDK。工程需要生成或復(fù)制一份ohos平臺目錄里面包含entry模塊和build-profile.json5配置。OpenHarmony設(shè)備連接后要配置hdc工具路徑并在IDE里注冊設(shè)備信息。我把這些完成后項目才能在設(shè)備和模擬器上正常識別。如果你用的是社區(qū)維護的一體化模板創(chuàng)建流程會更順但自己手動搭一遍的好處是出問題知道去哪里排查。建議第一次做的時候老老實實按官方文檔把環(huán)境變量和SDK配置都過一遍不要直接復(fù)制別人的配置因為版本不一致會導致各種奇怪的構(gòu)建錯誤。2.2 構(gòu)建配置與依賴引入實戰(zhàn)工程能創(chuàng)建只是第一步真正跑起來靠的是構(gòu)建配置。Flutter工程依賴OpenHarmony側(cè)的幾個關(guān)鍵組件flutter_ohos引擎包、Dart運行時庫以及OpenHarmony SDK里暴露給Flutter層的接口包。這些在構(gòu)建時會被打進HAP包中。我的pubspec.yaml里核心依賴是這樣組織的provider: ^6.1.0 hive: ^2.2.3 hive_flutter: ^1.1.0 fl_chart: ^0.66.0Hive的初始化在main()里要先執(zhí)行并且需要指定一個可寫的目錄。OpenHarmony上不能直接照搬安卓的路徑我通過path_provider在設(shè)備上獲取應(yīng)用沙盒目錄再傳給Hive.init()。這里如果路徑不對Hive會報無法創(chuàng)建目錄的錯誤排查時優(yōu)先看存儲權(quán)限。構(gòu)建時還要注意ohos模塊里的依賴聲明。在build-profile.json5中需要把Flutter引擎相關(guān)的SDK依賴引入否則編譯時會出現(xiàn)找不到FlutterPlugin之類的報錯。我在做版本匹配時踩過一次Flutter適配版和OpenHarmony SDK版本差異過大時編譯能過但運行直接崩潰原因是引擎層和系統(tǒng)層接口不匹配。解決方案是把兩邊版本對齊到官方驗證過的組合。有段時間構(gòu)建一直失敗日志里反復(fù)出現(xiàn)you are applying flutters main gradle plugin imperatively using the apply這個提示。這是Flutter新版Gradle插件對應(yīng)用方式做了調(diào)整舊式apply寫法觸發(fā)警告某些版本會直接當錯誤處理。我一開始沒當回事后來發(fā)現(xiàn)構(gòu)建確實中斷了。解決方法是把根目錄settings.gradle里的插件聲明改成pluginsDSL方式聲明并在模塊級build.gradle里同步調(diào)整。這個報錯在社區(qū)論壇里問的人非常多屬于Flutter構(gòu)建腳本演進的過渡期陣痛碰到不要慌照著新版模板改就行。3. 刷牙記錄核心功能實現(xiàn)3.1 基于狀態(tài)機的刷牙流程設(shè)計刷牙不是一個瞬間動作它有明確的階段準備開始、計時中、暫停/繼續(xù)、分區(qū)切換提示、結(jié)束。這些階段之間還有邊界條件比如計時中不允許重復(fù)開始、結(jié)束后不能再暫停。用普通的布爾變量管理這些狀態(tài)代碼很快就會變成一團亂麻所以我把整個刷牙過程設(shè)計成一個狀態(tài)機??偣捕x了四個狀態(tài)idle初始空閑態(tài)。running正在刷牙并計時。paused用戶主動暫停。finished計時結(jié)束或用戶手動結(jié)束。狀態(tài)切換規(guī)則寫在一個擴展方法里非法操作直接忽略并返回當前狀態(tài)enum BrushState { idle, running, paused, finished } BrushState nextState(BrushState current, BrushAction action) { switch (current) { case BrushState.idle: if (action BrushAction.start) return BrushState.running; return current; case BrushState.running: if (action BrushAction.pause) return BrushState.paused; if (action BrushAction.stop) return BrushState.finished; return current; case BrushState.paused: if (action BrushAction.resume) return BrushState.running; if (action BrushAction.stop) return BrushState.finished; return current; case BrushState.finished: return current; } }狀態(tài)機的價值在于把所有的“能不能做某件事”的判斷集中到一個函數(shù)里UI層不直接改狀態(tài)只發(fā)動作。比如暫停按鈕的點擊回調(diào)里只調(diào)用nextState拿到新狀態(tài)再根據(jù)結(jié)果決定是否更新頁面。這樣即便后續(xù)加了“超時自動結(jié)束”這種新規(guī)則也只需要在狀態(tài)機里加一個動作源不需要改一堆頁面邏輯。3.2 計時器、傳感器與平臺橋接刷牙記錄的核心指標是“刷了多久”計時用Timer.periodic實現(xiàn)每秒累加一次秒數(shù)。代碼很常規(guī)但有一個細節(jié)值得說Dart的計時器回調(diào)精度和主線程負載直接相關(guān)不要在回調(diào)里做耗時操作否則一秒一次的累加會漂移。實測下來把累加邏輯和UI刷新分開計時誤差可以控制在可接受范圍內(nèi)。Timer.periodic(const Duration(seconds: 1), (timer) { if (state BrushState.running) { _seconds; _notifyListeners(); } });除了時間我還接了OpenHarmony的加速度計傳感器用來判斷用戶是不是真的在刷牙。這個判斷很簡單刷牙時手部會持續(xù)產(chǎn)生一定幅度的周期性加速度變化靜止狀態(tài)則接近為零。通過平臺通道把加速度計數(shù)值讀到Dart層設(shè)定一個閾值來判斷“是否有刷牙動作”如果連續(xù)多秒低于閾值可以提醒用戶。傳感器調(diào)用的通道設(shè)計如下class SensorChannel { static const _channel MethodChannel(tooth_clock/sensor); static Futurevoid startListening() async { await _channel.invokeMethod(startAccelerometer); } }OpenHarmony側(cè)的代碼需要注冊MethodChannel并調(diào)用系統(tǒng)傳感器接口底層實際上是通過HDI接口和硬件驅(qū)動通信的。這里要注意權(quán)限問題加速度計本身不需要特殊權(quán)限但如果是攝像頭這類敏感設(shè)備必須在module.json5里聲明權(quán)限否則調(diào)用時會被系統(tǒng)攔截。3.3 記錄落庫與列表查詢一次刷牙結(jié)束后需要把記錄完整保存下來。我設(shè)計的記錄模型字段如下字段類型說明idString唯一ID使用時間戳隨機數(shù)生成startTimeDateTime開始刷牙的時間durationSecondsint本次刷牙時長zonesCoveredList覆蓋的牙區(qū)編號4個分區(qū)scoreint綜合評分0-100noteString?用戶備注Hive存取代碼很直接class BrushRecord { final String id; final DateTime startTime; final int durationSeconds; final Listint zonesCovered; final int score; MapString, dynamic toJson() { id: id, startTime: startTime.millisecondsSinceEpoch, durationSeconds: durationSeconds, zonesCovered: zonesCovered, score: score, }; factory BrushRecord.fromJson(MapString, dynamic json) BrushRecord( id: json[id], startTime: DateTime.fromMillisecondsSinceEpoch(json[startTime]), durationSeconds: json[durationSeconds], zonesCovered: (json[zonesCovered] as List).castint(), score: json[score], ); }寫庫時機選在狀態(tài)機進入finished的瞬間而不是用戶退出頁面時。這個設(shè)計避免了用戶中途殺掉App導致記錄丟失。注意finished狀態(tài)本身也意味著計時結(jié)束所以在狀態(tài)切換的回調(diào)里同步做三件事停止計時器、組裝記錄對象、寫入Hive。查詢列表時按startTime倒序排列取最近100條展示。數(shù)據(jù)量不大不需要分頁但如果你計劃長期積累建議生成記錄時就按天構(gòu)建索引避免每次全量掃描。3.4 統(tǒng)計圖表與下拉刷新記錄數(shù)據(jù)只有變成圖表才有實際意義。統(tǒng)計頁我用了fl_chart畫柱狀圖展示最近7天每天的平均刷牙時長再用折線圖展示連續(xù)達標天數(shù)。圖表數(shù)據(jù)源從Hive讀取后做聚合計算ListBrushRecord records _box.values.toList(); MapDateTime, ListBrushRecord grouped groupByDay(records);這個函數(shù)按天分組然后映射成BarChartGroupData。一個容易忽略的點是時區(qū)問題startTime存的是毫秒級時間戳轉(zhuǎn)成DateTime后默認是本地時區(qū)但如果在別的設(shè)備上讀數(shù)據(jù)時區(qū)不一致會導致分組錯位。我在存儲時就統(tǒng)一用UTC時間展示時再轉(zhuǎn)換成本地時間。記錄列表頁的下拉刷新用的是Flutter自帶組件RefreshIndicator( onRefresh: () async { await _refreshRecords(); }, child: ListView.builder(...), )在OpenHarmony上這個組件能正常工作但有一個小差異觸屏下拉的觸發(fā)閾值和安卓不太一樣可能需要把displacement參數(shù)調(diào)大一點否則總感覺刷新不夠靈敏。這是交互細節(jié)實測調(diào)成40.0比較舒服。4. 組件通信與異步機制深度實踐4.1 頁面間通信的選型與實現(xiàn)Flutter的組件通信方式很多熱詞里也反復(fù)問到這個問題?;卣{(diào)和路由傳參適合父子頁面之間的常規(guī)數(shù)據(jù)傳遞EventBus適合跨頁面廣播狀態(tài)管理則適合全局共享數(shù)據(jù)。實際項目里我用得最多的還是ChangeNotifier Provider因為它能解決的問題范圍最廣且對代碼侵入性小。以“首頁展示今日時長”和“計時頁刷新數(shù)據(jù)”為例。這兩個頁面沒有直接的層級關(guān)系但它們都依賴當前的刷牙狀態(tài)。我的做法是建一個BrushSessionModel讓它繼承ChangeNotifier然后在需要展示的地方ConsumerBrushSessionModel( builder: (context, model, child) { return Text(${model.totalSeconds} 秒); }, )計時頁更新狀態(tài)后調(diào)用notifyListeners()首頁的文本自動刷新不需要寫任何頁面間傳值代碼。這就是狀態(tài)管理在實戰(zhàn)中最大的價值你不需要去維護組件間的引用關(guān)系只關(guān)心數(shù)據(jù)源在哪個Model里頁面各自訂閱即可。如果是臨時性的頁面?zhèn)髦当热缭O(shè)置頁把“目標時長”傳給計時頁我就直接用路由參數(shù)Navigator.push( context, MaterialPageRoute( builder: (_) BrushPage(targetSeconds: 120), ), );這兩種方式不沖突原則是跨頁面共享的數(shù)據(jù)用狀態(tài)管理一次性傳遞的數(shù)據(jù)用路由參數(shù)。4.2 Future回調(diào)、微任務(wù)與計時器陷阱開發(fā)過程中有幾個和Dart異步機制相關(guān)的坑。熱詞里有人問“Future的then回調(diào)是放入微任務(wù)隊列嗎”答案是分情況的Future的then回調(diào)默認進入微任務(wù)隊列但如果Future還沒完成且用的默認scheduleMicrotask之外的方式行為會略有差異。在刷牙計時這個場景里這個問題直接影響代碼正確性。比如你在Future.delayed后想繼續(xù)累加計時器如果延遲周期短于微任務(wù)執(zhí)行時機累加可能被吞掉。更穩(wěn)妥的做法是永遠只依賴Timer.periodic作為唯一的時間來源不要在業(yè)務(wù)代碼里混用多個Future.delayed倒計時。另一個坑在平臺通道調(diào)用上。invokeMethod本身是異步的在OpenHarmony上調(diào)用傳感器讀取如果返回延遲UI線程不會卡死但如果你連續(xù)多次調(diào)用回調(diào)順序可能錯亂。我在傳感器數(shù)據(jù)解析時加了一個版本號字段每次請求遞增回調(diào)回來時只處理最新的一次結(jié)果丟棄舊版本避免界面跳動。異步異常處理也要注意。try/catch必須包裹整個異步鏈條而不是只包在await處。我遇到過一種情況平臺通道在設(shè)備休眠時返回空響應(yīng)導致后續(xù)whereType強轉(zhuǎn)直接拋類型轉(zhuǎn)換異常App閃退。所以代碼里對平臺返回值一律先判空再處理不給異常留機會。4.3 調(diào)用OpenHarmony攝像頭做口腔影像記錄刷牙記錄除了數(shù)據(jù)我還加了一個可選功能刷完牙后拍一張口腔照片存入記錄詳情。這涉及到調(diào)用OpenHarmony的攝像頭能力Flutter側(cè)需要一個原生視圖來承載相機預(yù)覽在Flutter里對應(yīng)的是PlatformView機制。OpenHarmony的Flutter適配對PlatformView的支持還不夠完善直接在頁面嵌入相機預(yù)覽容易黑屏或白屏。我的方案是繞開實時預(yù)覽直接調(diào)相機拍照接口拍照完成后把圖片路徑回傳給Flutter層。這樣雖然交互上少了一個實時取景框但穩(wěn)定性高了很多。final imagePath await _cameraChannel.invokeMethodString(takePhoto);OpenHarmony側(cè)用CameraKit封裝拍照邏輯拍完保存為JPEG文件返回沙盒路徑。這里有個權(quán)限細節(jié)拍照需要ohos.permission.CAMERA權(quán)限聲明并且在跳轉(zhuǎn)拍照前要動態(tài)請求授權(quán)否則調(diào)用會被系統(tǒng)拒絕。另外保存圖片的目錄必須在應(yīng)用沙盒范圍內(nèi)否則后續(xù)讀文件會報權(quán)限錯誤。Dart層拿到路徑后把路徑存入對應(yīng)的刷牙記錄里詳情頁用Image.file(File(path))加載。事實證明這個“跳開PlatformView嵌入式預(yù)覽、直接用原生拍照再回傳圖片”的思路在整個項目里幫我省了很多適配時間。如果你后續(xù)也要在OpenHarmony上做類似功能建議優(yōu)先考慮這種簡化方案。5. 構(gòu)建、調(diào)試與問題排查實錄5.1 Gradle插件應(yīng)用方式報錯怎么改構(gòu)建過程中最折磨人的就是這個Gradle報錯you are applying flutters main gradle plugin imperatively using the apply這是Flutter新版調(diào)整了Gradle插件接入方式之后出現(xiàn)的兼容性提示。舊式寫法是在build.gradle里用apply把Flutter插件加進去新式寫法要求在settings.gradle里用plugins {}聲明。OpenHarmony構(gòu)建鏈對這個變更更敏感必須改成新式寫法才能繼續(xù)。我修改后的settings.gradle關(guān)鍵片段plugins { id dev.flutter.flutter-plugin-loader version 1.0.0 id com.android.application version 8.3.0 apply false id org.jetbrains.kotlin.android version 1.9.22 apply false }模塊級build.gradle里用apply plugin: com.android.application也改成plugins { id com.android.application }這里還要注意一個依賴順序問題Flutter插件加載器必須最先執(zhí)行否則后續(xù)插件找不到Flutter引擎。我把這些調(diào)整完后改了三次依賴版本才穩(wěn)定下來核心原因是Gradle插件、AGP、Kotlin插件三者的版本必須匹配。這個組合極其敏感建議參考Flutter官方模板的版本號不要全用最新的。5.2 Impeller渲染與運行時異常排查新版Flutter把默認渲染器切換到了Impeller在OpenHarmony上并不是所有GPU驅(qū)動都適配良好。我遇到過兩種現(xiàn)象一是頁面部分區(qū)域出現(xiàn)渲染錯亂二是某些動畫掉幀嚴重。最開始看到控制臺刷出類似這樣的日志E/flutter (31173): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception每次看到這個dart_vm_initializer開頭的日志第一反應(yīng)不是去改業(yè)務(wù)邏輯而是要區(qū)分這是Dart層異常還是引擎層異常。我那次排查下來發(fā)現(xiàn)是某個頁面在Texture上傳階段觸發(fā)了Impeller的兼容bug不是業(yè)務(wù)代碼出錯。解決方案是給AndroidManifest.xml里對應(yīng)Activity加上Flutter配置開關(guān)把渲染回退到Skia模式meta-data android:nameio.flutter.embedding.android.EnableImpeller android:valuefalse /如果你在OpenHarmony上測試三維場景或者復(fù)雜漸變可以先評估Impeller是否穩(wěn)定不穩(wěn)定就回退不要硬撐。當然這不是說Impeller不行而是當前在OpenHarmony上的適配還在完善中穩(wěn)定壓倒一切。運行時還有一個高頻坑包名不一致。Flutter工程里的applicationId、OpenHarmony入口模塊的bundleName、以及HAP的簽名信息三者只要有一個不一致安裝時就會報INSTALL_PARSE_FAILED。檢查順序是先看build-profile.json5里的bundleName再回看Android側(cè)的applicationId確保它們語義一致。5.3 真機調(diào)試、日志分析與XTS認證準備OpenHarmony設(shè)備用hdc工具連接類似安卓的adb。我測試用的是開發(fā)板連接后通過hdc install安裝HAP包。日志查看用hdc hilog但Flutter側(cè)的Dart日志通常還是走flutter run輸出兩條日志通道要分開看。Dart層日志看Flutter控制臺系統(tǒng)底層異??磆ilog。應(yīng)用開發(fā)完準備預(yù)裝或者分發(fā)時要留意OpenHarmony的兼容性認證流程也就是常說的XTS認證。它不是開發(fā)階段的事但在應(yīng)用上架前必須跑一遍。XTS會測試應(yīng)用的安裝、啟動、權(quán)限申請、后臺運行等基礎(chǔ)行為任何一條不合格都會被拒。我建議開發(fā)階段就養(yǎng)成規(guī)范習慣權(quán)限按需申請、不要硬編碼系統(tǒng)目錄路徑、應(yīng)用退出時要正確釋放傳感器監(jiān)聽。這些習慣能讓你后續(xù)跑XTS時少改很多代碼。另外在性能調(diào)優(yōu)上OpenHarmony設(shè)備的GPU能力和主流安卓機有差距。我在列表滾動和動畫上做了兩件事一是列表項用到const優(yōu)化Widget重建二是把圖表動畫時長縮短到300毫秒內(nèi)。實測下來這兩處調(diào)整對滾動的流暢度提升最明顯有時比你優(yōu)化算法還管用。6. 經(jīng)驗總結(jié)與可擴展方向6.1 個人踩坑心得從工程創(chuàng)建到刷牙記錄功能完整跑通我前后用了大概三周。最耗時的不是功能實現(xiàn)而是OpenHarmony的適配和構(gòu)建鏈調(diào)整。如果讓我重來一遍會先做三件事先把OpenHarmony設(shè)備刷到與Flutter適配SDK匹配的系統(tǒng)版本避免因為系統(tǒng)API差異導致的一堆詭異問題。工程搭建完成后第一時間跑通一個最簡單的頁面不急著加功能。把權(quán)限、包名、簽名這些基礎(chǔ)配置提前核對清楚后面每次調(diào)試都能省時間。關(guān)于代碼層面我想再強調(diào)一次狀態(tài)機的價值。如果沒有狀態(tài)機刷牙流程里的暫停、繼續(xù)、超時這些邏輯會散落在各個按鈕的回調(diào)里排查問題時要同時看三四個頁面?,F(xiàn)在所有狀態(tài)切換都集中在一個函數(shù)里出問題只需要看這一個函數(shù)就夠了。這種設(shè)計思路同樣適用于其他有明確階段劃分的業(yè)務(wù)場景比如跑步計時、冥想引導、番茄時鐘。6.2 下一步可以擴展的功能這個項目做完后我一直在思考可以擴展的方向。最有價值的有三個刷牙質(zhì)量的AI評估結(jié)合攝像頭拍下的口腔照片用圖像識別算法判斷清潔盲區(qū)給出個性化建議。多設(shè)備數(shù)據(jù)同步通過云服務(wù)把刷牙記錄同步到手機端實現(xiàn)家庭成員的健康數(shù)據(jù)匯總。更多健康場景復(fù)用這套“狀態(tài)機計時傳感器記錄”的框架完全可以復(fù)用到其他健康行為管理上比如喝水提醒、眼保健操計時。從技術(shù)底層看這套框架的核心其實是“傳感器驅(qū)動 狀態(tài)管理 本地持久化”的組合。你在OpenHarmony上做了一個垂直場景后再做第二個第三個邊際成本會顯著降低。這也是Flutter跨端方案在特定場景下最有說服力的地方不是省一遍UI代碼而是把整個業(yè)務(wù)開發(fā)范式沉淀下來可持續(xù)復(fù)用。坦白講目前OpenHarmony上的Flutter生態(tài)還在成長期很多能力要自己趟路。但如果你愿意花時間把這條鏈路跑通后續(xù)的項目會越來越順。希望這篇實戰(zhàn)記錄能幫你少踩幾個我已經(jīng)踩過的坑。