換工具:從px/rem到OpenHarmony集成實(shí)踐)
做 Web 前端的兄弟應(yīng)該都有過(guò)這種體驗(yàn)設(shè)計(jì)稿標(biāo)的是 1920 寬的桌面端到了移動(dòng)端要按照 375 寬去還原上午還在算 px 轉(zhuǎn) rem下午又要算 dp 轉(zhuǎn) pt碰上老項(xiàng)目里根字號(hào)被動(dòng)態(tài)改過(guò)一堆相對(duì)單位直接變成玄學(xué)。我自己就經(jīng)常栽在這種基礎(chǔ)換算上以前總是臨時(shí)打開(kāi)在線工具等半天廣告還要擔(dān)心頁(yè)面里塞的腳本把數(shù)據(jù)改壞。后來(lái)決定干脆用 Flutter 擼一個(gè)純離線的 Web 開(kāi)發(fā)助手 App正好趕上 OpenHarmony 生態(tài)在逐步成熟我就順手把這套 Flutter 代碼也跑到了 OpenHarmony 設(shè)備上。這篇文章是這個(gè)系列的第一篇聚焦最常用也最容易被忽視的“單位轉(zhuǎn)換”模塊——用 Flutter 實(shí)現(xiàn)一個(gè)跨平臺(tái)、離線的單位換算工具并完整跑通 Flutter for OpenHarmony 的工程集成。這篇文章適合兩類(lèi)人一類(lèi)是想在 OpenHarmony 上跑 Flutter 應(yīng)用的移動(dòng)開(kāi)發(fā)者另一類(lèi)是前端轉(zhuǎn)客戶(hù)端、想用 Flutter 解決日常效率問(wèn)題的開(kāi)發(fā)者。我會(huì)把從需求拆解、核心算法、Provider 狀態(tài)管理到 OpenHarmony 工程集成、常見(jiàn)坑排查的全過(guò)程都鋪開(kāi)保證不是貼幾段代碼就完事。1. 項(xiàng)目整體設(shè)計(jì)與技術(shù)選型思路1.1 需求拆解單位轉(zhuǎn)換到底在解決什么問(wèn)題單位轉(zhuǎn)換這個(gè)模塊看起來(lái)簡(jiǎn)單真正拆開(kāi)以后其實(shí)有兩種完全不同的需求場(chǎng)景。第一種場(chǎng)景是“前端稿還原”。UI 設(shè)計(jì)師給的稿子可能是 750 寬的 iOS 風(fēng)格也可能是 1920 寬的 Web 稿。前端開(kāi)發(fā)需要把設(shè)計(jì)稿里的 px 換算成 rem、vw、dp、sp 等實(shí)際落地單位。這里就涉及到幾個(gè)關(guān)鍵參數(shù)設(shè)計(jì)稿寬度、目標(biāo)屏幕寬度、根字號(hào)大小、設(shè)備像素比。比如設(shè)計(jì)稿寬度是 750px目標(biāo)屏幕寬度是 375px那么 1px 的稿子在移動(dòng)端應(yīng)該是 0.5px 嗎不對(duì)這得看整個(gè)頁(yè)面是等比縮放還是流式布局。實(shí)際開(kāi)發(fā)里大多數(shù)人會(huì)用一個(gè)基準(zhǔn)寬度比如 750 設(shè)計(jì)稿對(duì)應(yīng) 375 邏輯寬度那么換算公式就是目標(biāo)值 設(shè)計(jì)稿值 * 目標(biāo)邏輯寬度 / 設(shè)計(jì)稿寬度。如果再套 rem還需要除以根字號(hào)。第二種場(chǎng)景是“移動(dòng)端密度換算”。Android 的 dp/dip、iOS 的 pt、Flutter 里的邏輯像素這些不是一回事。Android 在 160dpi 的屏幕上 1dp 1px在 320dpi 屏幕上 1dp 2pxiOS 的 pt 在正常屏幕上 1pt 1px在 Retina 屏幕上 1pt 2px 或 3pxFlutter 的邏輯像素和 Android dp 本質(zhì)一致。再加上 pt點(diǎn)、pc、inch、cm、mm 這些絕對(duì)單位換算關(guān)系足夠?qū)懸恍《喂ぞ邘?kù)了。所以這個(gè) App 的功能需求非常明確輸入一個(gè)數(shù)值選擇源單位和目標(biāo)單位系統(tǒng)按當(dāng)前場(chǎng)景自動(dòng)補(bǔ)充參數(shù)根字號(hào)、視口寬高等實(shí)時(shí)給出換算結(jié)果并且支持復(fù)制結(jié)果、保留精度、離線可用。我希望打開(kāi) App 就能算不聯(lián)網(wǎng)、不彈廣告、不傳數(shù)據(jù)。1.2 技術(shù)選型對(duì)比為什么是 Flutter而不是 ArkTS在 OpenHarmony 生態(tài)里做應(yīng)用官方主推的是 ArkTS ArkUI這套組合是 OpenHarmony 原生開(kāi)發(fā)的首選語(yǔ)法上類(lèi)似 TypeScriptUI 聲明式寫(xiě)法也很快。但我的情況比較特殊這個(gè) Web 開(kāi)發(fā)助手將來(lái)要覆蓋 Android、iOS、Web、Windows 等多個(gè)平臺(tái)如果直接用 ArkTS 寫(xiě)就只能鎖定在 OpenHarmony 一個(gè)平臺(tái)代碼復(fù)用基本為零。于是我把目光放到了 Flutter for OpenHarmony 上。這里必須先說(shuō)清楚Flutter for OpenHarmony 不是一個(gè)魔法方案它是 OpenHarmony 社區(qū)維護(hù)的 Flutter 引擎移植版目標(biāo)是把 Flutter 的“一套代碼多端運(yùn)行”能力延伸到 OpenHarmony。它復(fù)用了 Dart 層和 Widget 層底層渲染引擎在 OpenHarmony 上也有自己的適配實(shí)現(xiàn)。我選擇 Flutter 的核心理由有三個(gè)第一UI 一致性強(qiáng)。Flutter 自繪渲染引擎不依賴(lài)系統(tǒng)原生控件所以在不同設(shè)備上看到的界面幾乎一致這對(duì)工具類(lèi) App 很重要我不想在 Android 上微調(diào)一遍到 OpenHarmony 上再調(diào)一遍。第二生態(tài)復(fù)用。pub.dev 上的大量包比如狀態(tài)管理的 provider、高精度計(jì)算的 decimal都能在 Flutter for OpenHarmony 工程里直接使用省去很多造輪子的時(shí)間。第三熱重載開(kāi)發(fā)效率高。雖然 OpenHarmony 上的熱重載支持還不像 Android 那么成熟但大部分 Dart 代碼改動(dòng)仍然能快速生效。當(dāng)然我也認(rèn)真對(duì)比過(guò) ArkTS 方案如果你是只做 OpenHarmony 原生應(yīng)用、不關(guān)心多端復(fù)用那 ArkTS 確實(shí)更輕更穩(wěn)畢竟它不需要額外移植引擎性能上限也更高。這個(gè)工具類(lèi) App 的定位決定了“多端復(fù)用”優(yōu)先于“單端極致”所以 Flutter 是我的選擇。另外提一下渲染引擎Flutter 3.10 引入的 Impeller 渲染引擎在 iOS/Android 上已經(jīng)逐步取代 Skia在 OpenHarmony 移植版上目前還是以 Skia 為主Impeller 的適配還在推進(jìn)中不過(guò)就單位轉(zhuǎn)換這種輕量 UI 場(chǎng)景渲染引擎差異感知不強(qiáng)。1.3 應(yīng)用架構(gòu)與目錄規(guī)劃這個(gè) App 我采用輕量 MVVM 架構(gòu)View 層用 StatelessWidget 組合頁(yè)面ViewModel 層用 Provider 管理狀態(tài)Model 層是純 Dart 的換算引擎。目錄結(jié)構(gòu)如下lib/ ├── main.dart ├── models/ │ ├── unit.dart // 單位定義與分類(lèi) │ └── converter.dart // 換算引擎純 Dart 邏輯 ├── providers/ │ └── unit_converter_provider.dart ├── screens/ │ ├── home_screen.dart │ └── unit_converter_screen.dart └── utils/ └── formatter.dartmodels 和 utils 里的代碼不依賴(lài)任何 Flutter 組件這樣方便以后單獨(dú)做單元測(cè)試也方便以后把換算能力擴(kuò)展到其他平臺(tái)。2. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)2.1 單位定義與原子化換算算法單位轉(zhuǎn)換最容易踩的坑是把所有單位組合都寫(xiě)死比如寫(xiě)一堆pxToRem、dpToPt、vwToVh函數(shù)這樣代碼會(huì)爆炸。正確做法是給所有單位定義一個(gè)統(tǒng)一的“基準(zhǔn)單位”換算時(shí)先把源單位轉(zhuǎn)成基準(zhǔn)單位再把基準(zhǔn)單位轉(zhuǎn)成目標(biāo)單位。對(duì)于絕對(duì)長(zhǎng)度單位我用px做基準(zhǔn)。換算關(guān)系如下1 inch 96 css px1 cm 96 / 2.54 px ≈ 37.79527559055118 px1 mm 96 / 25.4 px ≈ 3.779527559055118 px1 pt 96 / 72 px ≈ 1.3333333333333333 px1 pc 12 pt 16 px對(duì)于邏輯單位需要帶上上下文參數(shù)dp/dippx dp * (dpi / 160)sp按dp換算后再乘以系統(tǒng)字體縮放系數(shù)rempx rem * rootFontSizeempx em * currentElementFontSizevwpx vw * viewportWidth / 100vhpx vh * viewportHeight / 100有了這個(gè)基準(zhǔn)體系換算邏輯就變成兩步。我用 Dart 代碼組織了一個(gè)Unit枚舉和Converter類(lèi)核心換算函數(shù)大致長(zhǎng)這樣class Converter { static double toBase(double value, Unit unit, UnitContext ctx) { switch (unit.family) { case UnitFamily.absolute: return value * unit.toBaseFactor; case UnitFamily.density: return value * (ctx.dpi / 160); case UnitFamily.relative: switch (unit) { case Unit.rem: return value * ctx.rootFontSize; case Unit.em: return value * ctx.elementFontSize; case Unit.vw: return value * ctx.viewportWidth / 100; case Unit.vh: return value * ctx.viewportHeight / 100; // ... } } } static double fromBase(double baseValue, Unit unit, UnitContext ctx) { // toBase 的逆運(yùn)算根據(jù)單位族反向換算 } static double convert(double value, Unit from, Unit to, UnitContext ctx) { double baseValue toBase(value, from, ctx); return fromBase(baseValue, to, ctx); } }這里最關(guān)鍵的一點(diǎn)是UnitContext這個(gè)上下文對(duì)象它包含了dpi、rootFontSize、elementFontSize、viewportWidth、viewportHeight。當(dāng)用戶(hù)選擇 rem 或 vw 這類(lèi)相對(duì)單位時(shí)我再動(dòng)態(tài)顯示對(duì)應(yīng)的參數(shù)輸入框不選就不展示避免頁(yè)面被一堆輸入框堆滿(mǎn)。2.2 精度處理為什么 double 不夠用單位轉(zhuǎn)換的另一個(gè)大坑是浮點(diǎn)精度。CSS 的 px 換算里經(jīng)常出現(xiàn)像 96/72 這樣的無(wú)限小數(shù)如果用 double 直接乘很容易得到1.3333333333333333帶上誤差多個(gè)單位連續(xù)換算誤差會(huì)累積。所以我在 pubspec.yaml 里引入了decimal包用十進(jìn)制高精度計(jì)算換算完成后保留 6 位小數(shù)再把尾隨的 0 去掉。final result Decimal.parse(96) / Decimal.parse(72);注意一點(diǎn)數(shù)值輸入用 TextField 拿到的是字符串不要直接轉(zhuǎn) double先校驗(yàn)是合法數(shù)字再送進(jìn)換算引擎。非法輸入比如空字符串、多個(gè)小數(shù)點(diǎn)直接顯示“請(qǐng)輸入有效數(shù)字”不參與計(jì)算避免崩潰。2.3 組件通信與 Provider 狀態(tài)管理實(shí)戰(zhàn)在單位轉(zhuǎn)換頁(yè)輸入值、源單位、目標(biāo)單位、上下文參數(shù)之間是相互關(guān)聯(lián)的。比如用戶(hù)選了“rem”下面的根字號(hào)輸入框就要出現(xiàn)用戶(hù)改了源單位結(jié)果要立刻重算用戶(hù)切換黑白主題組件也要跟著刷新。如果用setState管理所有狀態(tài)都得堆在 HomeScreen 里代碼會(huì)越來(lái)越亂。我選擇的是provider包。核心思路是把所有和單位轉(zhuǎn)換相關(guān)的狀態(tài)集中到一個(gè)ChangeNotifier里用Consumer精準(zhǔn)監(jiān)聽(tīng)需要刷新的區(qū)域其余區(qū)域保持不動(dòng)。class UnitConverterProvider extends ChangeNotifier { String inputValue ; Unit sourceUnit Unit.px; Unit targetUnit Unit.rem; UnitContext contextParams UnitContext.defaults(); String? get result { final num double.tryParse(inputValue); if (num null) return null; return Converter.convert(num, sourceUnit, targetUnit, contextParams).toString(); } void updateInput(String value) { inputValue value; notifyListeners(); } void updateSource(Unit unit) { sourceUnit unit; notifyListeners(); } // ... }頁(yè)面里這樣用ConsumerUnitConverterProvider( builder: (context, converter, child) { return Text( converter.result ?? 等待輸入, style: Theme.of(context).textTheme.headlineMedium, ); }, )組件通信方面還有一個(gè)容易踩的坑在回調(diào)函數(shù)里不要用context.watch否則會(huì)導(dǎo)致不必要的 rebuild。我習(xí)慣在onChanged里用context.readUnitConverterProvider().updateInput(...)因?yàn)榛卣{(diào)只需要觸發(fā)更新不需要監(jiān)聽(tīng)變化。另外Selector可以進(jìn)一步縮小刷新范圍比如只有源單位變化時(shí)單位選擇器自己重建結(jié)果區(qū)域不受影響。選擇器的 UI 我用DropdownButton包了一層單位名稱(chēng)顯示為中文像素、點(diǎn)、厘米等內(nèi)部 value 用枚舉這樣界面友好也不會(huì)把枚舉值暴露給用戶(hù)。2.4 UI 布局與多尺寸適配單位轉(zhuǎn)換頁(yè)的 UI 我分成三塊頂部輸入?yún)^(qū)中間結(jié)果卡片底部快速換算記錄。頂部輸入?yún)^(qū)是一個(gè)TextField旁邊跟兩個(gè)單位選擇DropdownButton在輸入相對(duì)單位時(shí)下方通過(guò)AnimatedSize展開(kāi)一行參數(shù)輸入框。中間結(jié)果卡片用大字號(hào)展示結(jié)果方便截圖分享底部是一個(gè)“最近換算”列表用ListView保存歷史記錄點(diǎn)一下可以回填??紤]到手機(jī)橫屏、豎屏、平板上布局差異大我沒(méi)有寫(xiě)死寬度而是用LayoutBuilder判斷寬高比寬度大于 600 時(shí)將輸入?yún)^(qū)和結(jié)果卡片并排擺放否則上下排列。這樣在手機(jī)、平板、OpenHarmony 設(shè)備上都能自適應(yīng)。字號(hào)上我沒(méi)有完全跟隨系統(tǒng)字體縮放因?yàn)楣ぞ哳?lèi)頁(yè)面如果字體被放大很可能導(dǎo)致卡片溢出所以我用MediaQuery.textScalerOf做了一個(gè)統(tǒng)一設(shè)置在 App 支持范圍內(nèi)限制最大縮放系數(shù)為 1.3保證布局穩(wěn)定。3. 實(shí)操過(guò)程與核心環(huán)節(jié)實(shí)現(xiàn)3.1 開(kāi)發(fā)環(huán)境搭建Flutter SDK 與 OpenHarmony 工具鏈實(shí)操第一步是搭環(huán)境這一步最費(fèi)時(shí)間但也是最容易勸退人的。我按 Windows 和 macOS 分別說(shuō)下經(jīng)驗(yàn)。首先從 Flutter 官方倉(cāng)庫(kù)下載對(duì)應(yīng)版本的 Flutter SDK這里要特別注意跑 OpenHarmony 需要 Flutter 的ohos分支或者使用 OpenHarmony 社區(qū)提供的flutter_flutter倉(cāng)庫(kù)。下載后把 SDK 的bin目錄加到環(huán)境變量PATH里。第二步是安裝 DevEco Studio這是 OpenHarmony 應(yīng)用開(kāi)發(fā)的 IDE它內(nèi)部集成了 OpenHarmony SDK 和 hvigor 構(gòu)建工具。安裝完后在 DevEco Studio 里下載需要的 OpenHarmony SDK 版本記下 SDK 路徑后面配置 Flutter 要用。第三步是讓 Flutter 識(shí)別 OpenHarmony 平臺(tái)。執(zhí)行flutter config --enable-ohos flutter doctor如果在flutter doctor里看到OpenHarmony相關(guān)條目已經(jīng)打勾說(shuō)明環(huán)境識(shí)別成功。如果沒(méi)看到就需要檢查環(huán)境變量OHOS_SDK_HOME是否指向正確的 OpenHarmony SDK 目錄。搭建過(guò)程中我遇到過(guò)一個(gè)問(wèn)題flutter create默認(rèn)不會(huì)生成 OpenHarmony 平臺(tái)目錄需要顯式指定flutter create --platforms ohos,android,ios --org com.example web_assistant創(chuàng)建完成后項(xiàng)目里會(huì)出現(xiàn)ohos目錄這就是 OpenHarmony 宿主工程的殼。到這里環(huán)境就算通了。3.2 工程集成與 HAR/AAR 構(gòu)建機(jī)制Flutter 和 OpenHarmony 工程的集成方式和 Android 類(lèi)似Flutter 工程作為庫(kù)模塊宿主工程通過(guò)依賴(lài) Flutter 的構(gòu)建產(chǎn)物Android 上是 AAROpenHarmony 上是 HAR/HAP來(lái)加載。實(shí)際項(xiàng)目里我不建議手動(dòng)復(fù)制產(chǎn)物更推薦用 DevEco Studio 直接打開(kāi) Flutter 工程生成的 ohos 目錄然后通過(guò) hvigor 自動(dòng)完成依賴(lài)注入。pubspec.yaml 里需要引入兩個(gè)關(guān)鍵包dependencies: flutter: sdk: flutter provider: ^6.1.1 decimal: ^2.3.0然后在 ohos 工程的oh-package.json5中把 Flutter 提供的flutter模塊加為依賴(lài)同時(shí)在hvigorfile.ts里注冊(cè) Flutter 插件。這一塊不同的 Flutter for OpenHarmony 版本配置略有差異建議以社區(qū)模板為準(zhǔn)。核心機(jī)制是宿主應(yīng)用啟動(dòng)時(shí)加載 Flutter 引擎并把 Dart 代碼打包進(jìn) HAP 包內(nèi)運(yùn)行時(shí)通過(guò) FlutterView 控件渲染 UI。這里單獨(dú)講一下“Flutter AAR”這個(gè)熱詞其實(shí)在 Android 集成中flutter build aar會(huì)生成 AAR 產(chǎn)物供原生工程引用。OpenHarmony 的集成思路類(lèi)似只不過(guò)格式變成了 OpenHarmony 的 HAR。理解了這一點(diǎn)你就不會(huì)被兩套名詞繞暈本質(zhì)都是“把 Flutter 引擎和 Dart 業(yè)務(wù)包成一個(gè)庫(kù)嵌入到原生工程”。3.3 核心代碼實(shí)現(xiàn)換算引擎 Provider UI下面給出這個(gè)項(xiàng)目里最核心的換算引擎實(shí)現(xiàn)我做了精簡(jiǎn)去掉了注釋和邊界處理的冗余部分方便你看結(jié)構(gòu)enum UnitFamily { absolute, density, relative } enum Unit { px(UnitFamily.absolute, 1), pt(UnitFamily.absolute, 96 / 72), pc(UnitFamily.absolute, 16), inch(UnitFamily.absolute, 96), cm(UnitFamily.absolute, 96 / 2.54), mm(UnitFamily.absolute, 96 / 25.4), dp(UnitFamily.density, 1), rem(UnitFamily.relative, 1), em(UnitFamily.relative, 1), vw(UnitFamily.relative, 1), vh(UnitFamily.relative, 1); const Unit(this.family, this.toBaseFactor); final UnitFamily family; final double toBaseFactor; }換算引擎里絕對(duì)單位直接乘以toBaseFactor密度單位需要dpi相對(duì)單位需要上下文。我寫(xiě)了一個(gè)UnitContext來(lái)封裝這幾個(gè)可變參數(shù)class UnitContext { final double dpi; final double rootFontSize; final double elementFontSize; final double viewportWidth; final double viewportHeight; const UnitContext({ this.dpi 160, this.rootFontSize 16, this.elementFontSize 16, this.viewportWidth 375, this.viewportHeight 667, }); }默認(rèn)值不是隨便填的dpi 160是 Android 的基準(zhǔn)密度rootFontSize 16是大多數(shù)瀏覽器默認(rèn)根字號(hào)視口 375x667 對(duì)應(yīng) iPhone SE 一類(lèi)的常用小屏邏輯尺寸。這些默認(rèn)值保證用戶(hù)不填額外參數(shù)時(shí)換算結(jié)果也有意義。UI 構(gòu)建的核心代碼我用一個(gè)典型的Scaffold頁(yè)面展示。輸入框監(jiān)聽(tīng)輸入實(shí)時(shí)通過(guò) Provider 更新TextField( controller: _controller, keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r[0-9.]))], onChanged: (value) context.readUnitConverterProvider().updateInput(value), decoration: const InputDecoration(labelText: 數(shù)值), )這段代碼有幾個(gè)細(xì)節(jié)值得說(shuō)FilteringTextInputFormatter.allow(RegExp(r[0-9.]))可以過(guò)濾掉非法字符但要注意它允許輸入多個(gè)小數(shù)點(diǎn)所以我額外加了校驗(yàn)邏輯在 Provider 的updateInput里用tryParse失敗就不更新結(jié)果。鍵盤(pán)類(lèi)型設(shè)為numberWithOptions(decimal: true)后移動(dòng)端會(huì)彈出純數(shù)字鍵盤(pán)省去切換符號(hào)的麻煩。3.4 運(yùn)行調(diào)試與 OpenHarmony 設(shè)備適配環(huán)境配好后連接 OpenHarmony 手機(jī)或者打開(kāi) DevEco Studio 模擬器執(zhí)行flutter run -d ohos首次運(yùn)行需要構(gòu)建 HAP 包時(shí)間比較長(zhǎng)建議耐心等待。運(yùn)行起來(lái)后大部分 Dart 代碼改動(dòng)可以通過(guò)熱重啟生效但底層插件或原生代碼改動(dòng)需要重新構(gòu)建。在調(diào)試 OpenHarmony 設(shè)備時(shí)我發(fā)現(xiàn)一個(gè)和 Android 明顯不同的點(diǎn)剪貼板復(fù)制功能需要申請(qǐng)權(quán)限。單位轉(zhuǎn)換結(jié)果要支持一鍵復(fù)制需要調(diào)用系統(tǒng)剪貼板而 OpenHarmony 對(duì)剪貼板的訪問(wèn)有權(quán)限控制。我在module.json5里聲明了剪貼板相關(guān)權(quán)限同時(shí)做了降級(jí)處理如果復(fù)制失敗就用 SnackBar 提示用戶(hù)長(zhǎng)按結(jié)果文本手動(dòng)復(fù)制。此外單位轉(zhuǎn)換 App 不需要網(wǎng)絡(luò)所以我沒(méi)有申請(qǐng)任何網(wǎng)絡(luò)權(quán)限順便做了一次隱私自查App 不聯(lián)網(wǎng)、不采集數(shù)據(jù)、不埋點(diǎn)。這也是我選擇離線方案的重要原因——這類(lèi)工具用著放心。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄4.1 環(huán)境與構(gòu)建問(wèn)題速查表我把自己實(shí)操中遇到過(guò)的問(wèn)題整理成了一個(gè)速查表其中幾個(gè)命中率非常高。問(wèn)題現(xiàn)象可能原因排查與解決flutter doctor不顯示 OpenHarmony 條目未執(zhí)行flutter config --enable-ohos或 SDK 路徑未配置重新執(zhí)行 config 并檢查OHOS_SDK_HOME環(huán)境變量新建項(xiàng)目后跑不起來(lái)創(chuàng)建項(xiàng)目時(shí)未加--platforms ohos重新用flutter create --platforms ohos ...創(chuàng)建或手動(dòng)添加 ohos 目錄報(bào)you are applying flutters main gradle plugin imperatively using the apply method項(xiàng)目模板里用了舊的 Gradle apply 插件方式改用 settings.gradle 里的 pluginManagement 引入 Flutter 插件按官方模板遷移運(yùn)行時(shí)日志出現(xiàn)dart_vm_initializer.cc(41) Unhandled ExceptionDart 層未捕獲異常通常是 Provider 未初始化或空安全類(lèi)型問(wèn)題檢查main.dart是否用MultiProvider包住根組件用runZonedGuarded捕獲全局異常App 在 OpenHarmony 設(shè)備上白屏Flutter 引擎加載失敗可能缺少宿主依賴(lài)確認(rèn) ohos 目錄有完整的flutter依賴(lài)重新 build HAP復(fù)制結(jié)果提示失敗剪貼板權(quán)限未聲明在 module.json5 中添加剪貼板權(quán)限并重新構(gòu)建上面這些里面最折騰我的是 Gradle 插件遷移問(wèn)題。舊模板里在android/build.gradle頂部用apply method: flutter引入插件新版本改成在settings.gradle里用pluginManagement引入。遷移時(shí)記得把根目錄的android/build.gradle里的 apply 語(yǔ)句刪掉否則兩個(gè)地方都要執(zhí)行就會(huì)報(bào)這個(gè)錯(cuò)。4.2 Provider 狀態(tài)管理的坑和解決狀態(tài)管理雖然簡(jiǎn)化了架構(gòu)但用不好也會(huì)出問(wèn)題。我遇到過(guò)一個(gè)典型的場(chǎng)景單位選擇器下拉切換后整個(gè)頁(yè)面居然重建了輸入框里的值丟了。查了半天才發(fā)現(xiàn)問(wèn)題出在我把Consumer包在了整個(gè)頁(yè)面外層導(dǎo)致輸入框也被監(jiān)聽(tīng)。解決辦法是把Consumer拆細(xì)輸入框區(qū)域用普通StatefulWidget結(jié)果區(qū)域和參數(shù)區(qū)域各自獨(dú)立監(jiān)聽(tīng)。還有一個(gè)高頻坑在onChanged回調(diào)里用context.watch導(dǎo)致每次輸入都會(huì)觸發(fā)整棵組件樹(shù) rebuild輸入卡頓。正確的姿勢(shì)是onChanged: (value) { context.readUnitConverterProvider().updateInput(value); }read不會(huì)訂閱通知只負(fù)責(zé)調(diào)方法這樣可以避免無(wú)限循環(huán)或無(wú)效刷新。另外如果用了Selector優(yōu)化刷新粒度一定要在要監(jiān)聽(tīng)的實(shí)體上重寫(xiě)和hashCode。比如監(jiān)聽(tīng)Unit枚舉不用重寫(xiě)但監(jiān)聽(tīng)一個(gè)自定義對(duì)象就得重寫(xiě)否則Selector會(huì)一直認(rèn)為對(duì)象沒(méi)變不觸發(fā)更新。4.3 數(shù)據(jù)精度與邊界輸入被用戶(hù)按出來(lái)的問(wèn)題單位轉(zhuǎn)換工具最容易出 bug 的地方在邊界輸入。比如用戶(hù)輸入1e10是科學(xué)計(jì)數(shù)法輸入001.5前面有零輸入-3帶了負(fù)號(hào)。我的輸入過(guò)濾規(guī)則只允許數(shù)字和小數(shù)點(diǎn)所以-3和1e10會(huì)被過(guò)濾掉但001.5是合法的換算時(shí)最好用Decimal.tryParse和規(guī)范化刪除前導(dǎo)零。我再補(bǔ)充一個(gè)校驗(yàn)結(jié)果如果超過(guò)9999999999比如換算到很大的單位就強(qiáng)制保留兩位小數(shù)并用逗號(hào)千分位展示防止名稱(chēng)溢出。還有一個(gè)小體驗(yàn)問(wèn)題當(dāng)用戶(hù)在 TextField 里連續(xù)輸入兩個(gè)小數(shù)點(diǎn)比如1..5我的tryParse會(huì)失敗但不應(yīng)該直接拋異常而是保持上一次有效結(jié)果不變并在輸入框下方顯示一條輕提示。這樣用戶(hù)誤操作時(shí)不會(huì)不知所措。4.4 OpenHarmony XTS 認(rèn)證與上架前的最后幾步如果想把這個(gè) App 上架到 OpenHarmony 應(yīng)用市場(chǎng)不能只跑通本地還要過(guò)一遍 XTS 兼容性測(cè)試。XTS 是一套兼容性驗(yàn)收工具會(huì)檢查應(yīng)用是否調(diào)用了非公開(kāi)接口、是否濫用了權(quán)限、是否能夠適配不同分辨率。我的兩個(gè)建議第一不要在業(yè)務(wù)代碼里依賴(lài)任何 OpenHarmony 私有 API所有平臺(tái)能力盡量通過(guò) Flutter 插件層封裝第二在 DevEco Studio 里跑一遍現(xiàn)成的 XTS 測(cè)試套件重點(diǎn)檢查剪貼板和字體縮放相關(guān)的用例。我跑 XTS 時(shí)第一次因?yàn)樽煮w縮放的問(wèn)題掛了原因是我在MediaQuery層限制了最大縮放系數(shù)而測(cè)試用例要求 App 在超大字體下不能有文字截?cái)?。最后我調(diào)整了卡片布局把文本區(qū)改成Expanded才把這一條過(guò)掉。這種問(wèn)題在普通功能測(cè)試?yán)锔景l(fā)現(xiàn)不了但市場(chǎng)審核會(huì)卡所以提前跑 XTS 非常值。5. 實(shí)操心得與后續(xù)擴(kuò)展方向最后再說(shuō)一點(diǎn)我對(duì)這套技術(shù)棧的個(gè)人感受。從純 Flutter 項(xiàng)目遷移到 Flutter for OpenHarmony最大的阻力不是 Dart 代碼而是工程構(gòu)建鏈。Flutter 和 OpenHarmony 兩套工具鏈疊加后構(gòu)建過(guò)程會(huì)慢不少而且錯(cuò)誤提示經(jīng)常藏在 hvigor 日志深處需要耐心去翻。我的建議是開(kāi)發(fā)階段先在 Flutter 默認(rèn)平臺(tái)上把功能邏輯全部調(diào)通最后再切到 OpenHarmony 做集成測(cè)試。這樣能把“業(yè)務(wù)問(wèn)題”和“平臺(tái)問(wèn)題”分開(kāi)處理排錯(cuò)效率高很多。單位轉(zhuǎn)換只是這個(gè) Web 開(kāi)發(fā)助手的第一步。后面我準(zhǔn)備繼續(xù)加顏色格式換算HEX/RGB/HSL、URL 編解碼、正則表達(dá)式測(cè)試、JSON 格式化這幾個(gè)高頻工具每個(gè)工具都做成獨(dú)立的 Provider 頁(yè)面模塊復(fù)用這同一套架構(gòu)。到時(shí)候我也會(huì)繼續(xù)整理成博文分享出來(lái)。如果你也在折騰 Flutter 和 OpenHarmony 的集成歡迎一起交流我踩過(guò)的坑你大概率能少踩幾個(gè)。