戰(zhàn):從零打造日歷家庭相冊(cè) App)
年初家里聚會(huì)翻手機(jī)相冊(cè)找去年除夕的合影翻了幾百?gòu)堃矝](méi)翻到長(zhǎng)輩們都在旁邊等著。那一刻我就下了決心要給家里做一個(gè)“按日子看照片”的家庭相冊(cè) App。正好手頭有一臺(tái) OpenHarmony 設(shè)備又想驗(yàn)證一下 Flutter 在這個(gè)新系統(tǒng)上的落地情況于是就有了這個(gè) flutter_for_openharmony 家庭相冊(cè) App 實(shí)戰(zhàn)項(xiàng)目。這篇文章把整個(gè)實(shí)現(xiàn)過(guò)程拆開(kāi)講清楚重點(diǎn)放在日歷 Tab 的完整實(shí)現(xiàn)鏈路上從環(huán)境搭建、狀態(tài)管理、媒體庫(kù)讀取到真機(jī)調(diào)試適合正在做 Flutter 多端適配、或者在 OpenHarmony 上找 UI 技術(shù)選型方案的人參考。1. 項(xiàng)目背景與總體架構(gòu)設(shè)計(jì)1.1 為什么是 Flutter for OpenHarmony家里人的設(shè)備很雜有人用 OpenHarmony 的平板有人用安卓手機(jī)我自己主力是 iPhone。真要給全家做一個(gè)相冊(cè)最理想的形態(tài)是一套 UI 代碼跑在多個(gè)平臺(tái)上。Flutter 在這里的優(yōu)勢(shì)非常明顯UI 層完全自繪不依賴系統(tǒng)原生控件只要目標(biāo)設(shè)備的 Flutter 引擎適配到位頁(yè)面的表現(xiàn)就能保持一致。OpenHarmony 這邊經(jīng)過(guò)社區(qū)和廠商的持續(xù)適配已經(jīng)具備跑 Flutter 應(yīng)用的基礎(chǔ)能力所以我直接把項(xiàng)目定位成 Flutter for OpenHarmony而不是給 OpenHarmony 單獨(dú)用 ArkTS 重寫(xiě)一套。就“ArkTS 和 Flutter 誰(shuí)更流行”這類討論我的觀點(diǎn)一向是別單純比流行度要比你手里有什么。如果你團(tuán)隊(duì)成員全是 Flutter 背景公司里已經(jīng)積累了一堆 Flutter 組件和工程化經(jīng)驗(yàn)?zāi)窃?OpenHarmony 設(shè)備上繼續(xù)用 Flutter 明顯劃算。反過(guò)來(lái)如果項(xiàng)目只面向 OpenHarmony 一種設(shè)備團(tuán)隊(duì)又更熟悉 TS 生態(tài)那 ArkTS 當(dāng)然也是合理選擇。技術(shù)選型沒(méi)有標(biāo)準(zhǔn)答案只有適不適合當(dāng)前團(tuán)隊(duì)和場(chǎng)景。1.2 家庭相冊(cè)的功能邊界與主頁(yè)面劃分這個(gè) App 的定位很明確不做社交不做云同步先做好一件事讓照片按時(shí)間組織打開(kāi)日歷點(diǎn)哪一天就能看到那一天的所有照片。所以主界面我采用了底部三 Tab 的結(jié)構(gòu)相冊(cè) Tab按日期分組展示所有照片滑動(dòng)列表快速瀏覽日歷 Tab月視圖日歷有照片的日期顯示縮略圖角標(biāo)點(diǎn)擊某一天進(jìn)入當(dāng)天的照片網(wǎng)格相機(jī) Tab直接打開(kāi)相機(jī)拍照拍完自動(dòng)歸到當(dāng)天日期下。日歷 Tab 是核心入口因?yàn)椤盎貞洝边@個(gè)東西天然帶時(shí)間屬性。比起傳統(tǒng)相冊(cè)里按文件夾或全部時(shí)間線倒序排列以日歷為入口的瀏覽方式更有“翻舊賬”的儀式感也符合家人找照片的直覺(jué)先想到某年某月某日發(fā)生過(guò)什么再去找那天的照片。1.3 分層方案數(shù)據(jù)層、狀態(tài)層、UI 層這個(gè)項(xiàng)目我沒(méi)有用很重的架構(gòu)但分層還是做了不然寫(xiě)到一半必然亂。數(shù)據(jù)層services負(fù)責(zé)調(diào)用 OpenHarmony 媒體庫(kù)能力讀取照片原始數(shù)據(jù)和縮略圖把結(jié)果封裝成模型返回狀態(tài)層providers用 Provider 管理全局共享狀態(tài)比如當(dāng)前選中日期、照片分組數(shù)據(jù)、加載狀態(tài)UI 層pages/widgets只負(fù)責(zé)展示和交互事件上報(bào)不直接碰媒體庫(kù) API。這樣分層以后日歷 Tab 和相冊(cè) Tab 之間只需要共享同一個(gè) Provider 實(shí)例日歷改了日期相冊(cè)網(wǎng)格自動(dòng)刷新完全不用在頁(yè)面構(gòu)造函數(shù)里層層傳參。這也是 Flutter 組件通信最常見(jiàn)也最穩(wěn)妥的做法。2. 搭建 Flutter for OpenHarmony 開(kāi)發(fā)環(huán)境2.1 工具鏈與 SDK 準(zhǔn)備OpenHarmony 開(kāi)發(fā)環(huán)境比傳統(tǒng) Android 要多裝幾樣?xùn)|西我把關(guān)鍵點(diǎn)列一下DevEco StudioOpenHarmony 應(yīng)用開(kāi)發(fā)的主力 IDE版本建議用 4.0 以上它會(huì)幫你在安裝時(shí)配置 OpenHarmony SDK、HDC 工具和 ohpm 包管理器HDCOpenHarmony Device Connector相當(dāng)于 Android 的 adb負(fù)責(zé)連接真機(jī)、安裝 HAP 包、抓日志Java JDK要求 JDK 17版本不對(duì)會(huì)在編譯期報(bào)各種奇怪的錯(cuò)誤Flutter SDK必須使用適配 OpenHarmony 的 flutter_flutter 分支不能用官方原版 Flutter SDK這一點(diǎn)最容易踩坑。我當(dāng)時(shí)用的是 openharmony-sig 維護(hù)的 flutter_flutter 3.7 適配分支配 OpenHarmony 4.0 Release。如果你現(xiàn)在拿到的是更新的版本原理都一樣只是 API 號(hào)有變化。2.2 從零初始化一個(gè)可用于 OpenHarmony 的 Flutter 工程初始化工程這一步最簡(jiǎn)單的方式還是在 DevEco Studio 里直接新建工程再在工程目錄下把 Flutter 相關(guān)的目錄結(jié)構(gòu)補(bǔ)上。但我的操作習(xí)慣是用命令行創(chuàng)建更可控flutter create --org com.example --project-name family_album --platforms ohos .這里的--platforms ohos是關(guān)鍵它會(huì)額外生成ohos目錄里面是 OpenHarmony 殼工程。如果沒(méi)有加這個(gè)參數(shù)生成出來(lái)的只有 android、ios 這些平臺(tái)目錄OpenHarmony 設(shè)備無(wú)從跑起。生成完成后還需要在ohos工程里配置 SDK 路徑和簽名信息。對(duì)于社區(qū)調(diào)試可以先不配置正式簽名但需要給ohos模塊配置自動(dòng)簽名否則 HAP 裝不進(jìn)真機(jī)。2.3 首次跑真機(jī)最常見(jiàn)的啟動(dòng)失敗排查“flutter 新建項(xiàng)目后跑不起來(lái)”這個(gè)問(wèn)題我在這套環(huán)境里踩了個(gè)遍一個(gè)排查表來(lái)整理最有效現(xiàn)象常見(jiàn)原因處理方法運(yùn)行時(shí)報(bào)找不到 OpenHarmony SDKlocal.properties 里的 sdk.dir 沒(méi)配置檢查并寫(xiě)入 sdk 實(shí)際路徑Gradle 同步失敗、版本沖突DevEco 自帶 Gradle 與 Flutter 插件要求不一致統(tǒng)一 Gradle 版本關(guān)閉離線模式重新拉取真機(jī)連接后 HDC 顯示離線USB 調(diào)試模式未打開(kāi)或驅(qū)動(dòng)問(wèn)題打開(kāi)開(kāi)發(fā)者模式用hdc list targets確認(rèn)識(shí)別HAP 安裝失敗報(bào)簽名錯(cuò)誤未配置調(diào)試簽名在 DevEco 中配置自動(dòng)簽名后重新構(gòu)建編譯通過(guò)但啟動(dòng)白屏Flutter 引擎未正確打包進(jìn) HAP檢查 flutter.embedding 相關(guān)依賴是否完整如果你的 Flutter 引擎是源碼方式編譯的首次構(gòu)建會(huì)比較久后面增量構(gòu)建會(huì)快很多。我當(dāng)時(shí)遇到最狡猾的一個(gè)坑是環(huán)境變量里的 JAVA_HOME 指向了低版本 JDKDevEco 自己內(nèi)部用新版本沒(méi)問(wèn)題但命令行構(gòu)建時(shí)用的卻是舊 JDK報(bào)錯(cuò)信息很不直觀浪費(fèi)了大半天。3. 日歷 Tab 實(shí)現(xiàn)的完整鏈路從日期數(shù)據(jù)到照片分組3.1 日歷展示方案選型自研還是依賴組件日歷控件是我在整個(gè)項(xiàng)目里糾結(jié)最久的模塊。網(wǎng)上有現(xiàn)成的日歷組件包也看到有人參考 uview 里“日歷直接展示”的做法就是整月平鋪、點(diǎn)擊切換日期。uview 那個(gè)交互思路本身很直觀一個(gè)網(wǎng)格展示整月日期頂部左右箭頭切換月份點(diǎn)某一天觸發(fā)列表刷新。但問(wèn)題在于第三方日歷組件往往為了通用性做了很多抽象業(yè)務(wù)上需要“日期有照片就顯示角標(biāo)”這種需求反而要花不少功夫去看源碼改造升級(jí)依賴時(shí)還容易出問(wèn)題。我的選擇是自研一個(gè)輕量日歷網(wǎng)格原因很簡(jiǎn)單方案優(yōu)點(diǎn)缺點(diǎn)第三方日歷包功能齊全、好看定制角標(biāo)困難、維護(hù)風(fēng)險(xiǎn)、包體積大自研 GridView 日歷業(yè)務(wù)耦合低、完全可控、體積小需要寫(xiě)周末/月切換邏輯自研一個(gè)單月網(wǎng)格的代碼量大概 100 行出頭主要邏輯就是算出當(dāng)月第一天是星期幾然后按42 6周×7列生成日期格子遇到空位就填充空白占位。實(shí)測(cè)下來(lái)很穩(wěn)后面要加農(nóng)歷、節(jié)日都是在自己代碼里加不用去適配別人封裝好的接口。3.2 相冊(cè)數(shù)據(jù)按日期聚合的模型設(shè)計(jì)要讓日歷知道“哪幾天有照片”首先要把媒體庫(kù)里的照片按日期分組。這一步的關(guān)鍵是日期鍵的設(shè)計(jì)。我定義了一個(gè)很小的模型class PhotoDayModel { final String dateKey; // 20250123精確到天 final int count; // 當(dāng)天照片數(shù) final String coverPath; // 當(dāng)天第一張照片路徑作為縮略圖 } class AlbumProvider extends ChangeNotifier { ListPhotoDayModel _days []; String selectedDateKey ; ListPhotoDayModel get days _days; void setSelectedDate(String key) { selectedDateKey key; notifyListeners(); } }dateKey的生成不要用日期字符串格式化直接拼有坑。比如某張照片的時(shí)間戳是凌晨 00:30如果按本地時(shí)區(qū)轉(zhuǎn)成YYYY-MM-DD可能還是今天但如果你把時(shí)間戳除以 86400000 再取整會(huì)因?yàn)闀r(shí)區(qū)偏移產(chǎn)生前后一天的偏差。我統(tǒng)一用本地時(shí)區(qū)做日期歸一化先把時(shí)間戳轉(zhuǎn)成DateTime再取year/month/day拼接這樣分組結(jié)果才和日歷格子嚴(yán)格對(duì)應(yīng)。3.3 日歷點(diǎn)擊與相冊(cè)網(wǎng)格之間的 Provider 聯(lián)動(dòng)日歷 Tab 和相冊(cè)網(wǎng)格的聯(lián)動(dòng)是這個(gè)項(xiàng)目里 Flutter 組件通信最核心的部分。我在日歷 Tab 的點(diǎn)擊事件里只做一件事調(diào)context.readAlbumProvider().setSelectedDate(dateKey)。這里重點(diǎn)說(shuō)一下context.read和context.watch的區(qū)別新手很容易搞混。context.watch會(huì)注冊(cè)監(jiān)聽(tīng)Provider 變化時(shí)當(dāng)前 widget 會(huì)重建context.read只是讀取狀態(tài)并調(diào)用方法不產(chǎn)生訂閱。日歷格子點(diǎn)擊時(shí)用context.read因?yàn)楦褡颖旧聿恍枰驗(yàn)槿掌谧兓亟ㄋ灰咽录鞒鋈ゾ托小6鄡?cè)網(wǎng)格部分用ConsumerAlbumProvider包裹Provider 一旦通知網(wǎng)格就自動(dòng)獲取新的分組數(shù)據(jù)刷新。這種分工是 Provider 用得比較正宗的方式既不用手動(dòng)傳回調(diào)也不會(huì)因?yàn)橐粋€(gè)日期變化把整個(gè)頁(yè)面全部重建一遍。3.4 日歷 Tab 中的月切換與性能優(yōu)化月切換我采用PageView.builder來(lái)做每一頁(yè)是一個(gè)日歷月初始定位到當(dāng)前月份左右滑動(dòng)切換。這里有個(gè)經(jīng)驗(yàn)PageView默認(rèn)會(huì)預(yù)加載相鄰頁(yè)面如果每頁(yè)都觸發(fā)一次昂貴的照片分組邏輯滑動(dòng)時(shí)會(huì)明顯卡頓。所以我做了兩層優(yōu)化第一月份頁(yè)面緩存。只緩存當(dāng)前頁(yè)和左右兩頁(yè)避免無(wú)限預(yù)加載。第二照片分組邏輯放進(jìn) Isolate。媒體庫(kù)照片多時(shí)按日期分組的純計(jì)算任務(wù)耗時(shí)可能達(dá)到幾百毫秒在主 Isolate 里會(huì)卡 UI。用compute把媒體庫(kù)原始數(shù)據(jù)發(fā)送到后臺(tái) Isolate在那里完成分組回來(lái)直接渲染網(wǎng)格。這個(gè)改動(dòng)對(duì)低端設(shè)備尤其有效滑動(dòng)日歷和滑動(dòng)相冊(cè)都能保持流暢。4. 相冊(cè)瀏覽與相機(jī)模塊讓日歷里的每一天都能回得去4.1 媒體庫(kù)讀取、權(quán)限申請(qǐng)與圖片緩存策略O(shè)penHarmony 上讀取相冊(cè)和 Android 不太一樣權(quán)限方面需要在ohos模塊的module.json5里聲明媒體庫(kù)權(quán)限{ requestPermissions: [ { name: ohos.permission.READ_IMAGEVIDEO }, { name: ohos.permission.WRITE_IMAGEVIDEO } ] }然后在 App 啟動(dòng)后向用戶申請(qǐng)授權(quán)。這里有一個(gè)細(xì)節(jié)如果只做瀏覽可以只申請(qǐng)讀取權(quán)限拍照并保存才需要寫(xiě)權(quán)限。權(quán)限聲明寧少勿多多了應(yīng)用商店審核和應(yīng)用啟動(dòng)彈窗都會(huì)比較煩人。讀取照片數(shù)據(jù)時(shí)媒體庫(kù)返回的是資源對(duì)象包含 URI 和修改時(shí)間等。我把它統(tǒng)一封裝到一個(gè)MediaLibraryService里上層完全不感知當(dāng)前是 OpenHarmony 還是安卓。照片在相冊(cè)網(wǎng)格里展示時(shí)千萬(wàn)不要直接用原圖路徑加載必須走縮略圖接口。一張 12MP 的原圖解碼后內(nèi)存占用輕松超過(guò) 40MB一個(gè)網(wǎng)格 9 張圖直接爆內(nèi)存。我的做法是請(qǐng)求指定尺寸的縮略圖服務(wù)器端或系統(tǒng)提供的縮略圖接口會(huì)按需生成App 側(cè)再把縮略圖做一層磁盤(pán)緩存重復(fù)滑動(dòng)時(shí)基本無(wú)感。4.2 相機(jī)接入在日歷中直接記錄當(dāng)下瞬間相機(jī)模塊原本計(jì)劃用 Flutter 的 camera 插件但 OpenHarmony 上插件適配并不總是那么及時(shí)。我的策略是先跑通一個(gè)最簡(jiǎn)單的拍照閉環(huán)再做擴(kuò)展。具體做法是先嘗試用image_picker拉起系統(tǒng)相機(jī)如果這個(gè)插件在當(dāng)前 OpenHarmony 版本上能正常工作就直接用代碼量和維護(hù)成本最低。如果拉起失敗走備選方案寫(xiě)一個(gè)MethodChannel在 OpenHarmony 原生側(cè)用 CameraKit 實(shí)現(xiàn)拍照拍完把圖片路徑通過(guò) channel 傳回 Flutter 側(cè)。平臺(tái)通道調(diào)用其實(shí)不神秘Flutter 側(cè)就三行代碼final path await MethodChannel(family_album/camera) .invokeMethodString(takePhoto);原生側(cè)在ohos工程的 Ability 里對(duì)應(yīng)處理takePhoto返回圖片路徑。拿到路徑后把照片插入媒體庫(kù)再刷新 Provider 的數(shù)據(jù)列表當(dāng)天日歷格子上立刻出現(xiàn)新照片角標(biāo)。這一條鏈路走通整個(gè)相冊(cè) App 就有了“記錄當(dāng)下”的能力拍完就能在日歷里看到。4.3 沒(méi)有照片的日期怎么處理日歷這個(gè)東西很誠(chéng)實(shí)有的日子有照片有的日子是空白。剛開(kāi)始我打算空白日期直接置灰不可點(diǎn)后來(lái)被家人吐槽說(shuō)“想看看那天發(fā)生了什么都沒(méi)辦法”。于是改了一下交互空白日期也可以點(diǎn)擊相冊(cè)網(wǎng)格區(qū)域顯示一個(gè)占位狀態(tài)文案大概是“這一天還沒(méi)有留下照片去拍一張吧”旁邊放一個(gè)拍照按鈕。這個(gè)交互改動(dòng)不大但對(duì)使用體驗(yàn)的提升很明顯。它把“沒(méi)有照片”從功能性缺陷轉(zhuǎn)變成一種行動(dòng)引導(dǎo)也讓日歷始終保持在主控地位。做這個(gè)功能時(shí)我把空狀態(tài)也放進(jìn)了相冊(cè) Tab 的邏輯里兩個(gè) Tab 共用一套狀態(tài)判斷避免出現(xiàn)“日歷能點(diǎn)進(jìn)去但相冊(cè)里一片空白”這種割裂感。5. 真機(jī)調(diào)試、渲染與性能優(yōu)化5.1 HDC 連接與日志查看技巧OpenHarmony 真機(jī)調(diào)試我全程用命令行 HDC比 IDE 里的圖形化面板效率高很多。核心就幾條命令hdc list targets # 查看當(dāng)前連接設(shè)備 hdc shell hilog | grep flutter # 實(shí)時(shí)過(guò)濾 Flutter 相關(guān)日志 hdc install xxx.hap # 安裝包Flutter 應(yīng)用在 OpenHarmony 上跑的時(shí)候Flutter 引擎自身的日志和 Dart 側(cè)debugPrint輸出都會(huì)進(jìn) hilog。過(guò)濾flutter關(guān)鍵詞基本能看到生命周期、Dart 異常和引擎錯(cuò)誤。如果在日志里看到DartError但堆棧不全我習(xí)慣在 Dart 側(cè)加一個(gè)全局FlutterError.onError鉤子把異常信息打點(diǎn)輸出配合日志分析定位快很多。5.2 圖片滑動(dòng)列表卡頓與內(nèi)存控制相冊(cè)網(wǎng)格滑動(dòng)卡頓是這類 App 的經(jīng)典問(wèn)題。第一版我直接把所有當(dāng)天照片的原圖縮略圖路徑交給Image.network類似的方式加載結(jié)果真機(jī)上一滑就掉幀后面壓了三個(gè)優(yōu)化網(wǎng)格項(xiàng)外層包RepaintBoundary避免格子滾動(dòng)時(shí)觸發(fā)兄弟節(jié)點(diǎn)的重繪只請(qǐng)求 200x200 尺寸的縮略圖內(nèi)存占用直接下降一個(gè)量級(jí)列表使用SliverGrid 懶加載每次只構(gòu)建可視區(qū)域內(nèi)的格子。還有一個(gè)容易被忽視的點(diǎn)Flutter 的Image組件默認(rèn)沒(méi)有磁盤(pán)緩存只有內(nèi)存緩存。所以我把媒體庫(kù)的縮略圖路徑做了一層簡(jiǎn)單的磁盤(pán)緩存映射第二次加載同一張圖時(shí)走緩存速度提升非常明顯。5.3 Impeller 渲染引擎與中文渲染相關(guān)注意我跑這個(gè)項(xiàng)目時(shí)Flutter 引擎已經(jīng)默認(rèn)啟用 Impeller 渲染。Impeller 在 OpenHarmony 上的表現(xiàn)取決于 GPU 驅(qū)動(dòng)對(duì)圖形接口的支持情況低端設(shè)備偶爾會(huì)出現(xiàn)奇怪的渲染異常比如頁(yè)面花屏、圓角矩形邊緣發(fā)虛。這類問(wèn)題排查思路先不要懷疑業(yè)務(wù)代碼先做一個(gè)對(duì)比實(shí)驗(yàn)把引擎切換到舊的 Skia 渲染路徑看看同樣的頁(yè)面是否恢復(fù)正常。如果確實(shí)是 Impeller 與設(shè)備 GPU 驅(qū)動(dòng)兼容性的問(wèn)題項(xiàng)目里就先關(guān)閉 Impeller等設(shè)備驅(qū)動(dòng)更新再開(kāi)回去。關(guān)閉方法按當(dāng)前 flutter_flutter 分支的文檔來(lái)通常是配置引擎的渲染參數(shù)。這里提醒一句如果你將來(lái)要適配一批不同品牌、不同 GPU 的 OpenHarmony 設(shè)備一定要把這個(gè)開(kāi)關(guān)做成可動(dòng)態(tài)配置的別寫(xiě)死在代碼里否則出廠后的排查會(huì)非常被動(dòng)。中文渲染方面OpenHarmony 的 Flutter 適配基本沒(méi)問(wèn)題但我遇到過(guò)自定義字體失效的情況尤其是設(shè)置字重或者斜體時(shí)部分設(shè)備會(huì)退化到系統(tǒng)默認(rèn)字體。這個(gè)問(wèn)題多半不是引擎缺陷而是字體文件子集不完整。最終方案是直接把需要的字重都用靜態(tài)字體文件打進(jìn)去避免動(dòng)態(tài)字體加載在低端設(shè)備上丟字。6. 復(fù)盤(pán)我在這個(gè)項(xiàng)目里踩過(guò)的實(shí)際坑6.1 Gradle 插件引用報(bào)錯(cuò)apply 命令式閉包構(gòu)建過(guò)程中遇到一個(gè)比較典型的報(bào)錯(cuò)原文大概是 “you are applying flutters main gradle plugin imperatively using the apply script method”。這個(gè)是因?yàn)樵趕ettings.gradle和build.gradle里用了老式的腳本方式引用 Flutter Gradle 插件而新的 flutter_flutter 分支要求通過(guò)插件 DSL 方式聲明。這個(gè)問(wèn)題的處理說(shuō)起來(lái)簡(jiǎn)單但真實(shí)排查過(guò)程比較曲折。報(bào)錯(cuò)只在你執(zhí)行 Gradle 任務(wù)時(shí)出現(xiàn)而且首次看跟 Flutter 關(guān)系不大容易往 Gradle 版本方向查。我修復(fù)方式是在settings.gradle里用pluginManagement聲明插件坐標(biāo)和版本工程模塊里再用plugins { id ... }引用刪掉原來(lái)apply from:的寫(xiě)法。之后就再也沒(méi)出現(xiàn)過(guò)這個(gè)報(bào)錯(cuò)。順帶一提類似的問(wèn)題在安卓側(cè)也有但 OpenHarmony 的構(gòu)建鏈更容易因?yàn)檫@個(gè)問(wèn)題中斷因?yàn)樗?Gradle 插件和 Flutter 插件之間版本匹配比安卓側(cè)更敏感。6.2 組件通信從總線模式改造成 Provider 的教訓(xùn)項(xiàng)目早期圖省事我用了一個(gè)全局事件總線日歷點(diǎn)擊發(fā)一個(gè)事件相冊(cè)頁(yè)面監(jiān)聽(tīng)事件刷新。功能當(dāng)時(shí)也能跑但兩周后維護(hù)的時(shí)候我發(fā)現(xiàn)一個(gè)問(wèn)題事件在代碼里滿天飛搜索一個(gè)事件的發(fā)射點(diǎn)和監(jiān)聽(tīng)點(diǎn)全靠肉眼而且事件發(fā)多了還會(huì)出現(xiàn)重復(fù)監(jiān)聽(tīng)、內(nèi)存泄漏的隱患。后來(lái)我把組件通信統(tǒng)一收斂到 Provider規(guī)則很簡(jiǎn)單跨頁(yè)面共享的數(shù)據(jù)模型放 Provider頁(yè)面內(nèi)部臨時(shí)狀態(tài)用 StatefulWidget 或局部 State只在事件不需要觸發(fā) UI 重建時(shí)才考慮輕量事件 bus。改完之后代碼可讀性提升了一個(gè)檔次。日歷頁(yè)和相冊(cè)頁(yè)之間的通信從“我監(jiān)聽(tīng)你、你通知我”的隱式鏈路變成“我改 Provider 狀態(tài)、你 watch 這個(gè)狀態(tài)”的顯式數(shù)據(jù)流。排查問(wèn)題的時(shí)候直接看 Provider 的狀態(tài)變化比翻事件監(jiān)聽(tīng)列表快十倍。6.3 從 Android 遷移到 OpenHarmony 時(shí)的 API 差異清單如果之前做過(guò) Android 版相冊(cè)遷移到 OpenHarmony 時(shí)不能只改一下包名就完事。媒體庫(kù)這一層 API 差異非常大我列一個(gè)實(shí)際遷移時(shí)最實(shí)用的對(duì)照表能力AndroidOpenHarmony權(quán)限聲明AndroidManifest.xmlmodule.json5媒體庫(kù)查詢MediaStore ContentResolverMediaLibraryManager FetchOptions縮略圖Loader 系列 Glide 等第三方系統(tǒng)縮略圖接口需指定尺寸文件路徑Uri File 操作URI 為主部分場(chǎng)景走 filePath相冊(cè)插入MediaStore insertMediaAssetChangeRequest這份對(duì)照表看著簡(jiǎn)單真正遷移的時(shí)候每一個(gè)差異都可能憋出 bug。尤其是縮略圖這部分Android 習(xí)慣用 Glide 一把梭OpenHarmony 上用不上得回到系統(tǒng)縮略圖接口的思路上來(lái)。做這個(gè)項(xiàng)目踩了一圈坑之后我最大的體會(huì)是Flutter for OpenHarmony 目前的適配已經(jīng)能支撐起一個(gè)完整的、可日常使用的應(yīng)用但你要做好心理準(zhǔn)備真正消耗時(shí)間的不是寫(xiě)頁(yè)面而是摸清兩個(gè)生態(tài)之間那些“看起來(lái)一樣但實(shí)際不一樣”的邊界。如果你準(zhǔn)備做同類項(xiàng)目我建議先把媒體庫(kù)讀取、權(quán)限、真機(jī)連接這三樣跑通再用日歷 Tab 作為主力場(chǎng)景去驅(qū)動(dòng)其他功能整個(gè)開(kāi)發(fā)節(jié)奏會(huì)順很多。