ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flutter打造离线单位转换工具:从px/rem到OpenHarmony集成实践

Flutter打造离线单位转换工具:从px/rem到OpenHarmony集成实践 做 Web 前端的兄弟应该都有过这种体验设计稿标的是 1920 宽的桌面端到了移动端要按照 375 宽去还原上午还在算 px 转 rem下午又要算 dp 转 pt碰上老项目里根字号被动态改过一堆相对单位直接变成玄学。我自己就经常栽在这种基础换算上以前总是临时打开在线工具等半天广告还要担心页面里塞的脚本把数据改坏。后来决定干脆用 Flutter 撸一个纯离线的 Web 开发助手 App正好赶上 OpenHarmony 生态在逐步成熟我就顺手把这套 Flutter 代码也跑到了 OpenHarmony 设备上。这篇文章是这个系列的第一篇聚焦最常用也最容易被忽视的“单位转换”模块——用 Flutter 实现一个跨平台、离线的单位换算工具并完整跑通 Flutter for OpenHarmony 的工程集成。这篇文章适合两类人一类是想在 OpenHarmony 上跑 Flutter 应用的移动开发者另一类是前端转客户端、想用 Flutter 解决日常效率问题的开发者。我会把从需求拆解、核心算法、Provider 状态管理到 OpenHarmony 工程集成、常见坑排查的全过程都铺开保证不是贴几段代码就完事。1. 项目整体设计与技术选型思路1.1 需求拆解单位转换到底在解决什么问题单位转换这个模块看起来简单真正拆开以后其实有两种完全不同的需求场景。第一种场景是“前端稿还原”。UI 设计师给的稿子可能是 750 宽的 iOS 风格也可能是 1920 宽的 Web 稿。前端开发需要把设计稿里的 px 换算成 rem、vw、dp、sp 等实际落地单位。这里就涉及到几个关键参数设计稿宽度、目标屏幕宽度、根字号大小、设备像素比。比如设计稿宽度是 750px目标屏幕宽度是 375px那么 1px 的稿子在移动端应该是 0.5px 吗不对这得看整个页面是等比缩放还是流式布局。实际开发里大多数人会用一个基准宽度比如 750 设计稿对应 375 逻辑宽度那么换算公式就是目标值 设计稿值 * 目标逻辑宽度 / 设计稿宽度。如果再套 rem还需要除以根字号。第二种场景是“移动端密度换算”。Android 的 dp/dip、iOS 的 pt、Flutter 里的逻辑像素这些不是一回事。Android 在 160dpi 的屏幕上 1dp 1px在 320dpi 屏幕上 1dp 2pxiOS 的 pt 在正常屏幕上 1pt 1px在 Retina 屏幕上 1pt 2px 或 3pxFlutter 的逻辑像素和 Android dp 本质一致。再加上 pt点、pc、inch、cm、mm 这些绝对单位换算关系足够写一小段工具库了。所以这个 App 的功能需求非常明确输入一个数值选择源单位和目标单位系统按当前场景自动补充参数根字号、视口宽高等实时给出换算结果并且支持复制结果、保留精度、离线可用。我希望打开 App 就能算不联网、不弹广告、不传数据。1.2 技术选型对比为什么是 Flutter而不是 ArkTS在 OpenHarmony 生态里做应用官方主推的是 ArkTS ArkUI这套组合是 OpenHarmony 原生开发的首选语法上类似 TypeScriptUI 声明式写法也很快。但我的情况比较特殊这个 Web 开发助手将来要覆盖 Android、iOS、Web、Windows 等多个平台如果直接用 ArkTS 写就只能锁定在 OpenHarmony 一个平台代码复用基本为零。于是我把目光放到了 Flutter for OpenHarmony 上。这里必须先说清楚Flutter for OpenHarmony 不是一个魔法方案它是 OpenHarmony 社区维护的 Flutter 引擎移植版目标是把 Flutter 的“一套代码多端运行”能力延伸到 OpenHarmony。它复用了 Dart 层和 Widget 层底层渲染引擎在 OpenHarmony 上也有自己的适配实现。我选择 Flutter 的核心理由有三个第一UI 一致性强。Flutter 自绘渲染引擎不依赖系统原生控件所以在不同设备上看到的界面几乎一致这对工具类 App 很重要我不想在 Android 上微调一遍到 OpenHarmony 上再调一遍。第二生态复用。pub.dev 上的大量包比如状态管理的 provider、高精度计算的 decimal都能在 Flutter for OpenHarmony 工程里直接使用省去很多造轮子的时间。第三热重载开发效率高。虽然 OpenHarmony 上的热重载支持还不像 Android 那么成熟但大部分 Dart 代码改动仍然能快速生效。当然我也认真对比过 ArkTS 方案如果你是只做 OpenHarmony 原生应用、不关心多端复用那 ArkTS 确实更轻更稳毕竟它不需要额外移植引擎性能上限也更高。这个工具类 App 的定位决定了“多端复用”优先于“单端极致”所以 Flutter 是我的选择。另外提一下渲染引擎Flutter 3.10 引入的 Impeller 渲染引擎在 iOS/Android 上已经逐步取代 Skia在 OpenHarmony 移植版上目前还是以 Skia 为主Impeller 的适配还在推进中不过就单位转换这种轻量 UI 场景渲染引擎差异感知不强。1.3 应用架构与目录规划这个 App 我采用轻量 MVVM 架构View 层用 StatelessWidget 组合页面ViewModel 层用 Provider 管理状态Model 层是纯 Dart 的换算引擎。目录结构如下lib/ ├── main.dart ├── models/ │ ├── unit.dart // 单位定义与分类 │ └── converter.dart // 换算引擎纯 Dart 逻辑 ├── providers/ │ └── unit_converter_provider.dart ├── screens/ │ ├── home_screen.dart │ └── unit_converter_screen.dart └── utils/ └── formatter.dartmodels 和 utils 里的代码不依赖任何 Flutter 组件这样方便以后单独做单元测试也方便以后把换算能力扩展到其他平台。2. 核心细节解析与实操要点2.1 单位定义与原子化换算算法单位转换最容易踩的坑是把所有单位组合都写死比如写一堆pxToRem、dpToPt、vwToVh函数这样代码会爆炸。正确做法是给所有单位定义一个统一的“基准单位”换算时先把源单位转成基准单位再把基准单位转成目标单位。对于绝对长度单位我用px做基准。换算关系如下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对于逻辑单位需要带上上下文参数dp/dippx dp * (dpi / 160)sp按dp换算后再乘以系统字体缩放系数rempx rem * rootFontSizeempx em * currentElementFontSizevwpx vw * viewportWidth / 100vhpx vh * viewportHeight / 100有了这个基准体系换算逻辑就变成两步。我用 Dart 代码组织了一个Unit枚举和Converter类核心换算函数大致长这样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 的逆运算根据单位族反向换算 } static double convert(double value, Unit from, Unit to, UnitContext ctx) { double baseValue toBase(value, from, ctx); return fromBase(baseValue, to, ctx); } }这里最关键的一点是UnitContext这个上下文对象它包含了dpi、rootFontSize、elementFontSize、viewportWidth、viewportHeight。当用户选择 rem 或 vw 这类相对单位时我再动态显示对应的参数输入框不选就不展示避免页面被一堆输入框堆满。2.2 精度处理为什么 double 不够用单位转换的另一个大坑是浮点精度。CSS 的 px 换算里经常出现像 96/72 这样的无限小数如果用 double 直接乘很容易得到1.3333333333333333带上误差多个单位连续换算误差会累积。所以我在 pubspec.yaml 里引入了decimal包用十进制高精度计算换算完成后保留 6 位小数再把尾随的 0 去掉。final result Decimal.parse(96) / Decimal.parse(72);注意一点数值输入用 TextField 拿到的是字符串不要直接转 double先校验是合法数字再送进换算引擎。非法输入比如空字符串、多个小数点直接显示“请输入有效数字”不参与计算避免崩溃。2.3 组件通信与 Provider 状态管理实战在单位转换页输入值、源单位、目标单位、上下文参数之间是相互关联的。比如用户选了“rem”下面的根字号输入框就要出现用户改了源单位结果要立刻重算用户切换黑白主题组件也要跟着刷新。如果用setState管理所有状态都得堆在 HomeScreen 里代码会越来越乱。我选择的是provider包。核心思路是把所有和单位转换相关的状态集中到一个ChangeNotifier里用Consumer精准监听需要刷新的区域其余区域保持不动。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(); } // ... }页面里这样用ConsumerUnitConverterProvider( builder: (context, converter, child) { return Text( converter.result ?? 等待输入, style: Theme.of(context).textTheme.headlineMedium, ); }, )组件通信方面还有一个容易踩的坑在回调函数里不要用context.watch否则会导致不必要的 rebuild。我习惯在onChanged里用context.readUnitConverterProvider().updateInput(...)因为回调只需要触发更新不需要监听变化。另外Selector可以进一步缩小刷新范围比如只有源单位变化时单位选择器自己重建结果区域不受影响。选择器的 UI 我用DropdownButton包了一层单位名称显示为中文像素、点、厘米等内部 value 用枚举这样界面友好也不会把枚举值暴露给用户。2.4 UI 布局与多尺寸适配单位转换页的 UI 我分成三块顶部输入区中间结果卡片底部快速换算记录。顶部输入区是一个TextField旁边跟两个单位选择DropdownButton在输入相对单位时下方通过AnimatedSize展开一行参数输入框。中间结果卡片用大字号展示结果方便截图分享底部是一个“最近换算”列表用ListView保存历史记录点一下可以回填。考虑到手机横屏、竖屏、平板上布局差异大我没有写死宽度而是用LayoutBuilder判断宽高比宽度大于 600 时将输入区和结果卡片并排摆放否则上下排列。这样在手机、平板、OpenHarmony 设备上都能自适应。字号上我没有完全跟随系统字体缩放因为工具类页面如果字体被放大很可能导致卡片溢出所以我用MediaQuery.textScalerOf做了一个统一设置在 App 支持范围内限制最大缩放系数为 1.3保证布局稳定。3. 实操过程与核心环节实现3.1 开发环境搭建Flutter SDK 与 OpenHarmony 工具链实操第一步是搭环境这一步最费时间但也是最容易劝退人的。我按 Windows 和 macOS 分别说下经验。首先从 Flutter 官方仓库下载对应版本的 Flutter SDK这里要特别注意跑 OpenHarmony 需要 Flutter 的ohos分支或者使用 OpenHarmony 社区提供的flutter_flutter仓库。下载后把 SDK 的bin目录加到环境变量PATH里。第二步是安装 DevEco Studio这是 OpenHarmony 应用开发的 IDE它内部集成了 OpenHarmony SDK 和 hvigor 构建工具。安装完后在 DevEco Studio 里下载需要的 OpenHarmony SDK 版本记下 SDK 路径后面配置 Flutter 要用。第三步是让 Flutter 识别 OpenHarmony 平台。执行flutter config --enable-ohos flutter doctor如果在flutter doctor里看到OpenHarmony相关条目已经打勾说明环境识别成功。如果没看到就需要检查环境变量OHOS_SDK_HOME是否指向正确的 OpenHarmony SDK 目录。搭建过程中我遇到过一个问题flutter create默认不会生成 OpenHarmony 平台目录需要显式指定flutter create --platforms ohos,android,ios --org com.example web_assistant创建完成后项目里会出现ohos目录这就是 OpenHarmony 宿主工程的壳。到这里环境就算通了。3.2 工程集成与 HAR/AAR 构建机制Flutter 和 OpenHarmony 工程的集成方式和 Android 类似Flutter 工程作为库模块宿主工程通过依赖 Flutter 的构建产物Android 上是 AAROpenHarmony 上是 HAR/HAP来加载。实际项目里我不建议手动复制产物更推荐用 DevEco Studio 直接打开 Flutter 工程生成的 ohos 目录然后通过 hvigor 自动完成依赖注入。pubspec.yaml 里需要引入两个关键包dependencies: flutter: sdk: flutter provider: ^6.1.1 decimal: ^2.3.0然后在 ohos 工程的oh-package.json5中把 Flutter 提供的flutter模块加为依赖同时在hvigorfile.ts里注册 Flutter 插件。这一块不同的 Flutter for OpenHarmony 版本配置略有差异建议以社区模板为准。核心机制是宿主应用启动时加载 Flutter 引擎并把 Dart 代码打包进 HAP 包内运行时通过 FlutterView 控件渲染 UI。这里单独讲一下“Flutter AAR”这个热词其实在 Android 集成中flutter build aar会生成 AAR 产物供原生工程引用。OpenHarmony 的集成思路类似只不过格式变成了 OpenHarmony 的 HAR。理解了这一点你就不会被两套名词绕晕本质都是“把 Flutter 引擎和 Dart 业务包成一个库嵌入到原生工程”。3.3 核心代码实现换算引擎 Provider UI下面给出这个项目里最核心的换算引擎实现我做了精简去掉了注释和边界处理的冗余部分方便你看结构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; }换算引擎里绝对单位直接乘以toBaseFactor密度单位需要dpi相对单位需要上下文。我写了一个UnitContext来封装这几个可变参数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, }); }默认值不是随便填的dpi 160是 Android 的基准密度rootFontSize 16是大多数浏览器默认根字号视口 375x667 对应 iPhone SE 一类的常用小屏逻辑尺寸。这些默认值保证用户不填额外参数时换算结果也有意义。UI 构建的核心代码我用一个典型的Scaffold页面展示。输入框监听输入实时通过 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: 数值), )这段代码有几个细节值得说FilteringTextInputFormatter.allow(RegExp(r[0-9.]))可以过滤掉非法字符但要注意它允许输入多个小数点所以我额外加了校验逻辑在 Provider 的updateInput里用tryParse失败就不更新结果。键盘类型设为numberWithOptions(decimal: true)后移动端会弹出纯数字键盘省去切换符号的麻烦。3.4 运行调试与 OpenHarmony 设备适配环境配好后连接 OpenHarmony 手机或者打开 DevEco Studio 模拟器执行flutter run -d ohos首次运行需要构建 HAP 包时间比较长建议耐心等待。运行起来后大部分 Dart 代码改动可以通过热重启生效但底层插件或原生代码改动需要重新构建。在调试 OpenHarmony 设备时我发现一个和 Android 明显不同的点剪贴板复制功能需要申请权限。单位转换结果要支持一键复制需要调用系统剪贴板而 OpenHarmony 对剪贴板的访问有权限控制。我在module.json5里声明了剪贴板相关权限同时做了降级处理如果复制失败就用 SnackBar 提示用户长按结果文本手动复制。此外单位转换 App 不需要网络所以我没有申请任何网络权限顺便做了一次隐私自查App 不联网、不采集数据、不埋点。这也是我选择离线方案的重要原因——这类工具用着放心。4. 常见问题与排查技巧实录4.1 环境与构建问题速查表我把自己实操中遇到过的问题整理成了一个速查表其中几个命中率非常高。问题现象可能原因排查与解决flutter doctor不显示 OpenHarmony 条目未执行flutter config --enable-ohos或 SDK 路径未配置重新执行 config 并检查OHOS_SDK_HOME环境变量新建项目后跑不起来创建项目时未加--platforms ohos重新用flutter create --platforms ohos ...创建或手动添加 ohos 目录报you are applying flutters main gradle plugin imperatively using the apply method项目模板里用了旧的 Gradle apply 插件方式改用 settings.gradle 里的 pluginManagement 引入 Flutter 插件按官方模板迁移运行时日志出现dart_vm_initializer.cc(41) Unhandled ExceptionDart 层未捕获异常通常是 Provider 未初始化或空安全类型问题检查main.dart是否用MultiProvider包住根组件用runZonedGuarded捕获全局异常App 在 OpenHarmony 设备上白屏Flutter 引擎加载失败可能缺少宿主依赖确认 ohos 目录有完整的flutter依赖重新 build HAP复制结果提示失败剪贴板权限未声明在 module.json5 中添加剪贴板权限并重新构建上面这些里面最折腾我的是 Gradle 插件迁移问题。旧模板里在android/build.gradle顶部用apply method: flutter引入插件新版本改成在settings.gradle里用pluginManagement引入。迁移时记得把根目录的android/build.gradle里的 apply 语句删掉否则两个地方都要执行就会报这个错。4.2 Provider 状态管理的坑和解决状态管理虽然简化了架构但用不好也会出问题。我遇到过一个典型的场景单位选择器下拉切换后整个页面居然重建了输入框里的值丢了。查了半天才发现问题出在我把Consumer包在了整个页面外层导致输入框也被监听。解决办法是把Consumer拆细输入框区域用普通StatefulWidget结果区域和参数区域各自独立监听。还有一个高频坑在onChanged回调里用context.watch导致每次输入都会触发整棵组件树 rebuild输入卡顿。正确的姿势是onChanged: (value) { context.readUnitConverterProvider().updateInput(value); }read不会订阅通知只负责调方法这样可以避免无限循环或无效刷新。另外如果用了Selector优化刷新粒度一定要在要监听的实体上重写和hashCode。比如监听Unit枚举不用重写但监听一个自定义对象就得重写否则Selector会一直认为对象没变不触发更新。4.3 数据精度与边界输入被用户按出来的问题单位转换工具最容易出 bug 的地方在边界输入。比如用户输入1e10是科学计数法输入001.5前面有零输入-3带了负号。我的输入过滤规则只允许数字和小数点所以-3和1e10会被过滤掉但001.5是合法的换算时最好用Decimal.tryParse和规范化删除前导零。我再补充一个校验结果如果超过9999999999比如换算到很大的单位就强制保留两位小数并用逗号千分位展示防止名称溢出。还有一个小体验问题当用户在 TextField 里连续输入两个小数点比如1..5我的tryParse会失败但不应该直接抛异常而是保持上一次有效结果不变并在输入框下方显示一条轻提示。这样用户误操作时不会不知所措。4.4 OpenHarmony XTS 认证与上架前的最后几步如果想把这个 App 上架到 OpenHarmony 应用市场不能只跑通本地还要过一遍 XTS 兼容性测试。XTS 是一套兼容性验收工具会检查应用是否调用了非公开接口、是否滥用了权限、是否能够适配不同分辨率。我的两个建议第一不要在业务代码里依赖任何 OpenHarmony 私有 API所有平台能力尽量通过 Flutter 插件层封装第二在 DevEco Studio 里跑一遍现成的 XTS 测试套件重点检查剪贴板和字体缩放相关的用例。我跑 XTS 时第一次因为字体缩放的问题挂了原因是我在MediaQuery层限制了最大缩放系数而测试用例要求 App 在超大字体下不能有文字截断。最后我调整了卡片布局把文本区改成Expanded才把这一条过掉。这种问题在普通功能测试里根本发现不了但市场审核会卡所以提前跑 XTS 非常值。5. 实操心得与后续扩展方向最后再说一点我对这套技术栈的个人感受。从纯 Flutter 项目迁移到 Flutter for OpenHarmony最大的阻力不是 Dart 代码而是工程构建链。Flutter 和 OpenHarmony 两套工具链叠加后构建过程会慢不少而且错误提示经常藏在 hvigor 日志深处需要耐心去翻。我的建议是开发阶段先在 Flutter 默认平台上把功能逻辑全部调通最后再切到 OpenHarmony 做集成测试。这样能把“业务问题”和“平台问题”分开处理排错效率高很多。单位转换只是这个 Web 开发助手的第一步。后面我准备继续加颜色格式换算HEX/RGB/HSL、URL 编解码、正则表达式测试、JSON 格式化这几个高频工具每个工具都做成独立的 Provider 页面模块复用这同一套架构。到时候我也会继续整理成博文分享出来。如果你也在折腾 Flutter 和 OpenHarmony 的集成欢迎一起交流我踩过的坑你大概率能少踩几个。
返回列表