置頁(yè)開發(fā)與平臺(tái)通道實(shí)踐)
如果你問我一個(gè)二手物品置換App里最不起眼但最容易翻車的模塊是哪個(gè)我的答案一定是設(shè)置頁(yè)。剛接手這個(gè)項(xiàng)目時(shí)我也覺得這個(gè)模塊簡(jiǎn)單得很幾個(gè)列表項(xiàng)、幾個(gè)開關(guān)、跳轉(zhuǎn)幾個(gè)二級(jí)頁(yè)面半天就能搞定??烧娴接肍lutter適配OpenHarmony設(shè)備跑起來才發(fā)現(xiàn)事情沒那么簡(jiǎn)單——文件路徑取不到、版本號(hào)讀不出來、消息開關(guān)不同步一個(gè)比一個(gè)磨人。這篇文章就把我們用Flutter在OpenHarmony上落地二手置換App設(shè)置功能的全過程拆開講一遍從需求拆解到UI封裝從平臺(tái)通道適配到真機(jī)踩坑給正在折騰同一條路的你省點(diǎn)時(shí)間。1. 項(xiàng)目背景與需求拆解設(shè)置頁(yè)比想象中更重要1.1 為什么這個(gè)項(xiàng)目會(huì)用Flutter來做OpenHarmony應(yīng)用先說背景。這個(gè)二手物品置換App的核心業(yè)務(wù)是讓用戶發(fā)布閑置物品、按分類搜索、發(fā)起交換或低價(jià)交易同時(shí)支持同城線下見面驗(yàn)貨。最初只做了Android端后來業(yè)務(wù)方要求覆蓋更多設(shè)備形態(tài)包括平板和不少基于OpenHarmony的國(guó)產(chǎn)終端。團(tuán)隊(duì)評(píng)估后決定引入Flutter統(tǒng)一UI層。原因是這樣的業(yè)務(wù)頁(yè)面——首頁(yè)信息流、物品詳情、IM聊天、訂單流程——加起來有三十多個(gè)頁(yè)面如果每個(gè)平臺(tái)各寫一套人力完全撐不住。Flutter的優(yōu)勢(shì)在于渲染引擎自繪UI表現(xiàn)跨平臺(tái)高度一致而且Dart語(yǔ)言做狀態(tài)管理、網(wǎng)絡(luò)請(qǐng)求、本地化都非常順手。至于平臺(tái)差異能力比如消息通知、WebView容器、文件目錄等統(tǒng)一走M(jìn)ethodChannel也就是平臺(tái)通道。設(shè)置模塊恰好是最具代表性的“跨端試金石”。它既有純UI的東西又有大量涉及系統(tǒng)能力的調(diào)用。把設(shè)置頁(yè)跑通基本就能摸清這個(gè)App在OpenHarmony上還有哪些隱藏問題。1.2 設(shè)置模塊的需求清單在動(dòng)手寫代碼之前我們先把需求拆成了下面這張表。這里的關(guān)鍵不是把功能列全而是搞清楚每一條到底屬于UI展示、本地狀態(tài)、還是系統(tǒng)能力。功能模塊具體項(xiàng)涉及能力實(shí)現(xiàn)方式賬號(hào)管理個(gè)人信息展示、修改昵稱、修改手機(jī)號(hào)用戶數(shù)據(jù)Flutter 業(yè)務(wù)接口安全設(shè)置退出登錄登錄態(tài)清理Flutter 本地存儲(chǔ)消息通知交易消息開關(guān)、社區(qū)消息開關(guān)、促銷消息開關(guān)系統(tǒng)通知權(quán)限、本地開關(guān)狀態(tài)Flutter MethodChannel通用設(shè)置清除緩存、檢查更新、主題模式文件目錄、應(yīng)用市場(chǎng)Flutter path_provider關(guān)于版本號(hào)、用戶協(xié)議、隱私政策、開源許可應(yīng)用信息、WebViewFlutter MethodChannel幫助反饋意見反饋、常見問題頁(yè)面跳轉(zhuǎn)Flutter拆完這個(gè)表你會(huì)發(fā)現(xiàn)設(shè)置頁(yè)的價(jià)值不只是提供一個(gè)入口列表。用戶對(duì)你的App靠不靠譜的判斷很多時(shí)候是從設(shè)置頁(yè)的細(xì)節(jié)感受出來的。比如退出登錄按鈕放在哪里、清除緩存有沒有即時(shí)反饋、版本號(hào)是不是準(zhǔn)確這些體驗(yàn)直接影響信任感。1.3 哪些功能留在Flutter層哪些必須走原生通道劃分標(biāo)準(zhǔn)很簡(jiǎn)單不依賴設(shè)備底層能力且數(shù)據(jù)可以自己維護(hù)的全留在Flutter層必須觸發(fā)系統(tǒng)行為或讀取系統(tǒng)信息的才走原生通道。具體來說用戶協(xié)議和隱私政策頁(yè)面我們用Flutter內(nèi)置的WebView視圖容器打開但OpenHarmony上Flutter的WebView插件適配還不完整這個(gè)后面會(huì)成為坑。版本號(hào)在一個(gè)端上可以直接用插件讀取但換到OpenHarmony后同樣的代碼會(huì)拋異常。消息通知開關(guān)的顯示不能只看本地存的值還要去查詢系統(tǒng)通知權(quán)限否則用戶在系統(tǒng)設(shè)置里關(guān)了通知App里的開關(guān)還亮著雙方就對(duì)不上賬。做技術(shù)選型時(shí)我心里要反復(fù)權(quán)衡的就是這類問題。設(shè)計(jì)原則是Flutter層只負(fù)責(zé)狀態(tài)展示和用戶交互所有“拿不準(zhǔn)能不能跨端”的調(diào)用都封到service層后面這樣出了兼容問題只改一處。1.4 當(dāng)前的適配現(xiàn)實(shí)要先講清楚這里必須說一句實(shí)在話Flutter在OpenHarmony上的運(yùn)行時(shí)目前主要是靠社區(qū)維護(hù)的fork版本跟進(jìn)主分支并不是所有Flutter插件都有OpenHarmony實(shí)現(xiàn)。有的插件只有Android和iOS目錄編譯到OpenHarmony時(shí)會(huì)直接報(bào)錯(cuò)。你可以在項(xiàng)目的.flutter-plugins-dependencies文件里看哪些插件被加載了凡是只有android和ios路徑、沒有ohos路徑的都需要自己補(bǔ)平臺(tái)實(shí)現(xiàn)。這不是項(xiàng)目團(tuán)隊(duì)能繞開的成本。我們當(dāng)時(shí)跑原生通道時(shí)前前后后補(bǔ)了大概幾個(gè)原生接口代碼量不大但排查過程非常燒時(shí)間。所以如果你打算在OpenHarmony上做Flutter項(xiàng)目最好提前做一輪插件兼容性盤點(diǎn)。2. 工程環(huán)境與目錄組織先讓Flutter在OpenHarmony上跑起來2.1 環(huán)境依賴與SDK準(zhǔn)備Flutter構(gòu)建OpenHarmony應(yīng)用環(huán)境準(zhǔn)備跟普通Flutter開發(fā)有一點(diǎn)區(qū)別。我們用的大致流程是這樣的# 拉取帶OpenHarmony適配的Flutter SDK社區(qū)維護(hù)的fork版本 git clone -b ohos https://gitee.com/openharmony-sig/flutter_flutter.git export PATH$PWD/flutter_flutter/bin:$PATH # 安裝OpenHarmony SDK與DevEco工具并配置環(huán)境變量 # OHOS_SDK_HOME 指向OpenHarmony SDK目錄 # 驗(yàn)證環(huán)境 flutter doctor -v需要注意不同時(shí)期的適配分支名可能不同具體以倉(cāng)庫(kù)說明為準(zhǔn)。環(huán)境變量配置好之后用flutter doctor能看到OpenHarmony相關(guān)的檢查項(xiàng)類似OpenHarmony toolchain is available這種提示。如果缺失依賴按提示安裝就行這一關(guān)沒有太多技術(shù)含量但很磨人。2.2 項(xiàng)目目錄如何規(guī)劃工程創(chuàng)建好后默認(rèn)會(huì)生成ohos目錄作為OpenHarmony原生殼工程這個(gè)目錄負(fù)責(zé)最終打包成hap安裝包。而Dart代碼全部放在lib目錄下。我們內(nèi)部把設(shè)置模塊單獨(dú)作為一個(gè)feature來維護(hù)lib/ src/ app.dart features/ settings/ data/ # 本地狀態(tài)管理與持久化封裝 widgets/ # 通用設(shè)置項(xiàng)組件 pages/ # 設(shè)置一級(jí)頁(yè)與二級(jí)頁(yè) services/ # 平臺(tái)通道封裝不直接暴露MethodChannel l10n/ # 本地化文案為什么按feature而不是按類型來分因?yàn)樵O(shè)置模塊后續(xù)一定會(huì)擴(kuò)展功能比如增加“切換賬號(hào)”“深色模式跟隨系統(tǒng)”等如果所有頁(yè)面都堆在pages目錄下很快會(huì)變得沒法看。按feature內(nèi)聚之后每個(gè)模塊自上而下形成閉環(huán)找問題的范圍能控制在一個(gè)目錄里。2.3 編譯、安裝與首跑驗(yàn)證配置完成后用DevEco打開ohos目錄構(gòu)建hap包然后通過命令行安裝到OpenHarmony真機(jī)hdc install entry/build/default/outputs/default/entry-default-signed.hapFlutter引擎會(huì)作為原生庫(kù)打包進(jìn)hap里。第一次啟動(dòng)可能比普通App慢一些因?yàn)橐鲆娉跏蓟_@個(gè)階段我最常遇到的問題是簽名配置不對(duì)導(dǎo)致安裝失敗報(bào)錯(cuò)是SignatureNotFoundException處理辦法是在構(gòu)建配置里補(bǔ)充簽名信息。跑通這一步之后Flutter層的代碼修改可以通過增量編譯同步到設(shè)備。2.4 模塊初始化與路由約束設(shè)置模塊入口我們是按頁(yè)面掛載路由來管理的沒有做成全局單例。這樣做的原因是設(shè)置頁(yè)本身不承擔(dān)核心業(yè)務(wù)狀態(tài)每次進(jìn)入都重新拉取用戶摘要和開關(guān)配置能避免在多個(gè)頁(yè)面共享可變單例時(shí)出現(xiàn)臟數(shù)據(jù)。但如果你的設(shè)置頁(yè)里有一些需要長(zhǎng)期緩存的配置比如主題色那還是建議把這些配置單獨(dú)拆成一個(gè)ConfigService來持有。3. 設(shè)置頁(yè)UI架構(gòu)一組能復(fù)用的設(shè)置項(xiàng)組件3.1 頁(yè)面骨架與分組設(shè)計(jì)設(shè)置頁(yè)整體結(jié)構(gòu)是Scaffold AppBar ListView列表按業(yè)務(wù)分成三個(gè)區(qū)賬號(hào)安全區(qū)、通知與通用區(qū)、關(guān)于區(qū)。這樣用戶掃一眼就知道設(shè)置項(xiàng)歸屬哪個(gè)分類也不會(huì)因?yàn)閹资畟€(gè)列表項(xiàng)平鋪而顯得雜亂。Scaffold( appBar: AppBar(title: Text(設(shè)置)), body: ListView( children: [ _buildAccountSection(), _buildNotificationSection(), _buildGeneralSection(), _buildAboutSection(), ], ), )每個(gè)分區(qū)內(nèi)部又用SettingGroup組件統(tǒng)一包裹確保分組標(biāo)題的字體、間距、分割線風(fēng)格一致。這里要注意的是不要使用Card給每一行單獨(dú)加邊框因?yàn)榭ㄆ短缀箝g距容易失控在低分辨率設(shè)備上看起來非常擁擠。3.2 封裝一個(gè)可配置的SettingItem設(shè)置頁(yè)每一行幾乎都是左標(biāo)題、右側(cè)值和可能的開關(guān)變化點(diǎn)集中在圖標(biāo)、跳轉(zhuǎn)、開關(guān)回調(diào)。我們封裝了一個(gè)SettingItem組件參數(shù)大致這樣class SettingItem extends StatelessWidget { final String title; final String? subtitle; final IconData? leadingIcon; final Widget? trailing; final VoidCallback? onTap; final bool showDivider; const SettingItem({ super.key, required this.title, this.subtitle, this.leadingIcon, this.trailing, this.onTap, this.showDivider true, }); override Widget build(BuildContext context) { return InkWell( onTap: onTap, child: Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row( children: [ if (leadingIcon ! null) Icon(leadingIcon), SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle(fontSize: 16)), if (subtitle ! null) Text(subtitle!, style: TextStyle(fontSize: 12, color: Colors.grey)), ], ), ), if (trailing ! null) trailing!, ], ), ), ); } }封裝的時(shí)候有個(gè)容易忽視的點(diǎn)onTap必須放在InkWell上而不是整個(gè)Row上否則在Material3下點(diǎn)擊水波紋的區(qū)域不完整視覺效果很怪。3.3 開關(guān)項(xiàng)與消息通知的交互細(xì)節(jié)通知類開關(guān)我們直接用SwitchListTile但自己包了一層SettingSwitchItem把value、onChanged、title透?jìng)鞒鋈?。真正麻煩的地方不在UI而在于處理狀態(tài)回寫。用戶的習(xí)慣是快速開關(guān)一秒鐘內(nèi)可能連續(xù)切換多次。如果每次變更都立刻發(fā)起平臺(tái)通道查詢和持久化不僅費(fèi)電還容易出現(xiàn)競(jìng)態(tài)上一次寫入失敗UI卻保留了成功狀態(tài)。我們的做法是先在Flutter層做樂觀更新UI立即改變?nèi)缓螽惒綄懭氡镜卮鎯?chǔ)并調(diào)用原生通道如果原生通道返回失敗再用一個(gè)SnackBar提示用戶狀態(tài)未同步并回滾開關(guān)。這樣做下來用戶的感知流暢度提升很明顯。3.4 主題模式與深淺色適配設(shè)置頁(yè)里還做了一個(gè)主題模式選擇跟隨系統(tǒng)、淺色、深色。這個(gè)功能實(shí)現(xiàn)不難enum AppThemeMode { system, light, dark } ThemeMode _resolveThemeMode(AppThemeMode mode) { switch (mode) { case AppThemeMode.system: return ThemeMode.system; case AppThemeMode.light: return ThemeMode.light; case AppThemeMode.dark: return ThemeMode.dark; } }真正的坑在于OpenHarmony部分設(shè)備對(duì)系統(tǒng)深色模式的調(diào)度跟Android不太一樣默認(rèn)值獲取可能延遲。所以我們?cè)趩?dòng)時(shí)讀配置、設(shè)置themeMode后還在MaterialApp外面包了一層短時(shí)間的等待等系統(tǒng)MediaQuery返回穩(wěn)定值后再構(gòu)建首頁(yè)。這個(gè)細(xì)節(jié)后來被證實(shí)非常有用否則用戶會(huì)看到閃一下白屏再變黑的現(xiàn)象。4. 核心設(shè)置功能逐一落地4.1 用戶信息展示與退出登錄設(shè)置頁(yè)頂部是一張用戶摘要卡片頭像、昵稱、手機(jī)號(hào)脫敏展示。點(diǎn)擊后進(jìn)入個(gè)人資料頁(yè)。這一塊本身不復(fù)雜重點(diǎn)是退出登錄。退出按鈕放在賬號(hào)安全區(qū)的底部點(diǎn)擊后彈出二次確認(rèn)Dialog文案明確說明退出后收藏和瀏覽記錄不會(huì)被刪除但需要重新登錄才能使用購(gòu)買和發(fā)布功能。確認(rèn)后執(zhí)行以下步驟調(diào)后端接口退出忽略網(wǎng)絡(luò)錯(cuò)誤保證本地一定能退出清理本地Token、用戶緩存、購(gòu)物車草稿跳轉(zhuǎn)到登錄頁(yè)并清空路由棧。順序很重要。如果先跳轉(zhuǎn)再清數(shù)據(jù)登錄頁(yè)可能在初始化時(shí)讀到半截用戶信息。我們的經(jīng)驗(yàn)是先清數(shù)據(jù)再跳轉(zhuǎn)而且跳轉(zhuǎn)用pushAndRemoveUntil避免用戶按返回鍵回到已退出的頁(yè)面。4.2 清除緩存計(jì)算與刪除清除緩存做了兩個(gè)狀態(tài)未點(diǎn)擊時(shí)展示當(dāng)前緩存大小點(diǎn)擊后先計(jì)算并彈出Dialog提示確認(rèn)確認(rèn)后展示一個(gè)轉(zhuǎn)菊花刪除完成后顯示緩存已清空。緩存大小的計(jì)算依賴path_provider獲取臨時(shí)目錄。思路是這樣的Futureint _calculateCacheSize() async { final Directory tempDir await getTemporaryDirectory(); if (!await tempDir.exists()) return 0; int total 0; await for (final entity in tempDir.list(recursive: true, followLinks: false)) { if (entity is File) { total await entity.length(); } } return total; }這個(gè)過程中有個(gè)性能問題如果目錄里文件數(shù)量特別多遞歸遍歷會(huì)造成卡頓。所以我們把計(jì)算工作放到一個(gè)Isolate里去跑避免阻塞UI線程。刪除的時(shí)候同樣在Isolate里執(zhí)行刪除前給關(guān)鍵路徑加了一個(gè)白名單防止誤刪登錄臨時(shí)憑證。雖然臨時(shí)目錄里一般不會(huì)放重要東西但多一道檢查總是好的。4.3 關(guān)于頁(yè)與版本號(hào)獲取關(guān)于頁(yè)包括版本號(hào)、用戶協(xié)議、隱私政策、開源許可列表。版本號(hào)在Android上可以用package_info_plus讀但在OpenHarmony上這個(gè)插件當(dāng)時(shí)沒有完整實(shí)現(xiàn)于是我們通過MethodChannel讀取static const MethodChannel _channel MethodChannel(com.example.app.settings/platform); FutureString getAppVersion() async { final String version await _channel.invokeMethod(getAppVersion); return version; }原生側(cè)的邏輯很簡(jiǎn)單從配置里讀出版本名和版本號(hào)拼成x.y.zbuild格式返回給Dart層。封裝好之后其他模塊比如更新提醒也復(fù)用了同一個(gè)通道沒有重復(fù)造輪子。4.4 消息通知開關(guān)的完整鏈路消息通知有三個(gè)開關(guān)交易通知、社區(qū)通知、促銷通知。我們希望每個(gè)開關(guān)的狀態(tài)都存到SharedPreferences里并且在修改時(shí)同步到原生。這里有一個(gè)用戶偏好邏輯交易通知必須能獨(dú)立開關(guān)而且只要關(guān)掉交易通知即使其他通知開著也不影響交易消息的攔截。這是后端推送策略決定的前端開關(guān)狀態(tài)只是一個(gè)展示入口。所以開關(guān)的默認(rèn)值必須由后端下發(fā)的用戶配置決定不能用本地寫死的默認(rèn)值。如果后端配置拉取失敗我們給一個(gè)默認(rèn)的“全部開啟”方案但會(huì)在頁(yè)面頂部用一條淺灰色提示條告訴用戶配置暫未同步。4.5 收貨地址與常見問題頁(yè)二手置換App里設(shè)置頁(yè)還要作為收貨地址的入口。點(diǎn)擊后跳轉(zhuǎn)到地址管理二級(jí)頁(yè)面這個(gè)頁(yè)面負(fù)責(zé)增刪改地址并把選中的默認(rèn)地址回傳設(shè)置頁(yè)做展示。地址數(shù)據(jù)存于本地?cái)?shù)據(jù)庫(kù)不經(jīng)過后端適用于買家自提之外的郵寄場(chǎng)景。常見問題頁(yè)我們采用靜態(tài)JSON驅(qū)動(dòng)渲染每條問題一個(gè)標(biāo)題、一個(gè)正文展開和收起用ExpansionTile實(shí)現(xiàn)。這樣做的好處是運(yùn)營(yíng)同學(xué)要改內(nèi)容時(shí)只更新資源文件不需要發(fā)版。5. OpenHarmony適配過程中真實(shí)踩過的坑5.1 文件路徑差異與兼容第一個(gè)坑來自path_provider在OpenHarmony上返回的路徑結(jié)構(gòu)。Android上getTemporaryDirectory()返回的是/data/user/0/{包名}/cache而OpenHarmony上返回的是類似/data/storage/el2/base/haps/entry/cache的路徑。我們的清理代碼一開始用了硬編碼的/cache后綴拼接結(jié)果在OpenHarmony上算出來的目錄根本不存在。解決方案是徹底放棄手動(dòng)拼接路徑一律使用path_provider或者原生通道返回的語(yǔ)義化路徑并在用之前做存在性判斷。還有一個(gè)細(xì)節(jié)是OpenHarmony的文件系統(tǒng)區(qū)分el1和el2前者用于設(shè)備級(jí)數(shù)據(jù)后者用于用戶級(jí)數(shù)據(jù)清緩存業(yè)務(wù)一般只動(dòng)el2的用戶目錄。5.2 消息通知開關(guān)在系統(tǒng)級(jí)別不同步設(shè)置頁(yè)的開關(guān)是本地記憶的但用戶完全可以去系統(tǒng)設(shè)置里把App的通知權(quán)限關(guān)掉。這導(dǎo)致兩邊不同步Flutter側(cè)開關(guān)顯示開系統(tǒng)卻已經(jīng)屏蔽了通知。我們最初沒意識(shí)到這個(gè)問題后來測(cè)試真機(jī)時(shí)發(fā)現(xiàn)通知收不到排查了半天才發(fā)現(xiàn)是系統(tǒng)開關(guān)被手動(dòng)關(guān)了。修正方法是設(shè)置頁(yè)每次進(jìn)入時(shí)通過MethodChannel查詢系統(tǒng)通知權(quán)限狀態(tài)。如果系統(tǒng)權(quán)限是關(guān)閉的不管本地配置是什么三個(gè)通知開關(guān)都置灰并提示用戶去系統(tǒng)設(shè)置打開。雖然查系統(tǒng)權(quán)限也需要在原生側(cè)補(bǔ)接口但這一步是必須的。5.3 平臺(tái)通道命名沖突項(xiàng)目里多個(gè)模塊都使用Flutter的MethodChannel最初很多通道名都叫default或者channel。合并代碼后在OpenHarmony真機(jī)上動(dòng)不動(dòng)報(bào)MissingPluginException。原因其實(shí)很簡(jiǎn)單同一個(gè)Name的通道在原生側(cè)被覆蓋注冊(cè)導(dǎo)致后注冊(cè)的模塊收不到前一個(gè)模塊的消息。后來我們把通道名全部改成帶模塊前綴的格式比如com.example.app.settings/notification、com.example.app.settings/platform。這個(gè)改動(dòng)是一次性遷移工作但做完之后平臺(tái)通道相關(guān)的毛刺幾乎絕跡。建議所有通道名從一開始就按包名.模塊名/能力名來命名別偷懶。5.4 字體渲染與字號(hào)縮放OpenHarmony默認(rèn)字體跟Android不完全一樣同一個(gè)fontSize: 14在部分設(shè)備上看起來更小。而且如果用戶開了系統(tǒng)的字體縮放設(shè)置項(xiàng)里的標(biāo)題會(huì)被截?cái)?。我們的處理方式是設(shè)置項(xiàng)標(biāo)題的TextStyle鎖定在邏輯尺寸并對(duì)textScaleFactor做一次上限約束比如最大不超過1.3。再配合maxLines: 1和overflow: TextOverflow.ellipsis。這樣就算部分設(shè)備縮放很激進(jìn)頁(yè)面也不會(huì)整個(gè)崩壞。5.5 生命周期與頁(yè)面恢復(fù)設(shè)置頁(yè)在后臺(tái)被系統(tǒng)殺掉再回前臺(tái)時(shí)會(huì)出現(xiàn)開關(guān)狀態(tài)和實(shí)際狀態(tài)不一致的問題。根因是我們只在頁(yè)面initState時(shí)讀了一次配置。后來改成在WidgetsBindingObserver的didChangeAppLifecycleState里當(dāng)App從后臺(tái)恢復(fù)時(shí)重新拉取通知權(quán)限和緩存大小。這個(gè)改動(dòng)成本很小但非常影響體驗(yàn)。5.6 日志與調(diào)試OpenHarmony真機(jī)調(diào)試用hdc工具類似adb。抓Flutter側(cè)日志時(shí)如果是release包Dart層的print不會(huì)有輸出所以調(diào)試期一定要用debug包。原生側(cè)日志在系統(tǒng)日志里過濾關(guān)鍵字找到進(jìn)程。還有一個(gè)便利手段是通過DevEco的日志面板查看MethodChannel調(diào)用流程能清楚看到方法名和傳參。有時(shí)候Dart說調(diào)用了原生原生說沒收到絕大多數(shù)是通道名對(duì)不上。6. 測(cè)試、性能與后續(xù)要做的事6.1 真機(jī)與模擬器的差異我們一開始主要用模擬器調(diào)試設(shè)置頁(yè)跑得很順以為適配工作差不多了。上了真機(jī)才發(fā)現(xiàn)模擬器里很多系統(tǒng)調(diào)用是空實(shí)現(xiàn)比如通知權(quán)限查詢永遠(yuǎn)返回grantedWebView也不一定真的加載。所以適配類工作必須早一點(diǎn)上真機(jī)。建議團(tuán)隊(duì)準(zhǔn)備至少一臺(tái)OpenHarmony真機(jī)作為測(cè)試基準(zhǔn)別只依賴模擬器跑冒煙。6.2 設(shè)置頁(yè)的性能注意點(diǎn)設(shè)置頁(yè)雖然簡(jiǎn)單性能問題仍然存在。幾個(gè)要注意的地方列表項(xiàng)全部使用const構(gòu)造避免頁(yè)面重建時(shí)無(wú)謂的組件diff緩存大小計(jì)算只在進(jìn)入頁(yè)面時(shí)進(jìn)行不要做成實(shí)時(shí)輪詢每次開關(guān)切換不重建整個(gè)ListView只更新對(duì)應(yīng)StatefulWidget清除緩存時(shí)讓按鈕進(jìn)入loading態(tài)防止用戶連續(xù)點(diǎn)擊。6.3 可落地的Widget測(cè)試設(shè)置頁(yè)非常適合做Widget測(cè)試因?yàn)樗慕换ツJ角逦?。我們寫了一個(gè)冒煙用例保證頁(yè)面能正常渲染出所有分區(qū)和關(guān)鍵設(shè)置項(xiàng)。測(cè)試代碼大致是這樣的testWidgets(設(shè)置頁(yè)渲染關(guān)鍵設(shè)置項(xiàng), (tester) async { await tester.pumpWidget(const App()); expect(find.text(賬號(hào)與安全), findsOneWidget); expect(find.text(清除緩存), findsOneWidget); expect(find.text(關(guān)于我們), findsOneWidget); });這類測(cè)試不用覆蓋所有二級(jí)頁(yè)面但能攔住最基礎(chǔ)的“頁(yè)面白屏”和“關(guān)鍵入口丟失”問題。6.4 后續(xù)擴(kuò)展方向設(shè)置功能做完后我們規(guī)劃了幾件事多語(yǔ)言文案的完整覆蓋、無(wú)障礙屏幕閱讀支持、遠(yuǎn)程配置開關(guān)讓運(yùn)營(yíng)無(wú)需發(fā)版就能控制功能灰度以及把設(shè)置頁(yè)的常用操作接上埋點(diǎn)系統(tǒng)。比如“退出登錄點(diǎn)擊率”“清除緩存使用人數(shù)”這些指標(biāo)能直接幫助判斷新登錄引導(dǎo)和存儲(chǔ)優(yōu)化策略是否有效。我個(gè)人在實(shí)際操作中的體會(huì)是設(shè)置頁(yè)是所有基礎(chǔ)能力的“最后一道門”。把設(shè)置功能做得穩(wěn)不只是讓App看起來完整更是給自己留了一條高效驗(yàn)證平臺(tái)能力的測(cè)試通道。所以如果你正在做類似的跨端App別小瞧這個(gè)模塊。先把設(shè)置頁(yè)從里到外啃下來后面所有復(fù)雜功能都有了底氣。