☰
鸿蒙Flutter适配实践:纯Dart库numbers_to_text迁移与财务封装
2026/9/30 8:11:54 网站建设 项目流程

1. numbers_to_text 到底为什么值得搬进鸿蒙工程

1.1 财务与无障碍场景里“数字说不出口”的尴尬

我这次适配的起因其实挺实际。鸿蒙应用里做对账功能时,界面要展示一笔 123456.78 元的订单,普通用户扫一眼数字符号没问题,但遇到语音播报、无障碍朗读、客服聊天自动回复这类场景,系统读出来的就是 “一二三四五六点七八”,体验非常差。财务侧的同事提了个需求:能不能把金额转成自然语言,比如 “twelve thousand three hundred forty-five point seven eight”,或者中文语境下的“十二万三千四百四十五点七八”。

一开始我想过自己写转换逻辑:按数字位拆解,拼单位,处理负数和小数。写了两天后放弃了。字符串拼接看起来简单,但边界条件极多:负零、千分位、teen 数词、单复数、连字符、小数部分的不同读法、货币单位的位置……业务代码里混着这种逻辑,后续维护就是灾难。

用 numbers_to_text 这类现成的三方库,等于把“数字转自然语言”的标准答案直接拿过来。它的核心价值不是省几行代码,而是把一套经过大量用例验证的规则集放到你手里,你只需要关心业务侧怎么调用它。鸿蒙化适配听起来是个大工程,但这个库是纯 Dart 实现,没有原生代码,理论上迁移成本很低,真正花时间的是把“能用”变成“可信”。

1.2 这个库的能力边界与设计思路

numbers_to_text 的定位很专一:把数值对象转换成英文读法文本。常见用法是先实例化转换器,再传入数字,拿回字符串:

import 'package:numbers_to_text/numbers_to_text.dart'; final converter = NumberToText(); print(converter.convert(12345)); // twelve thousand three hundred forty-five

它内部处理的基本规则包括:

  • 整数的分段读法,按 thousand、million、billion 等量级分组;
  • 100 以内的组合词,例如 21 转成 twenty-one,中间带连字符;
  • teen 数词的特殊形态,比如 thirteen 不是 threeteen;
  • 负数、小数、零值的处理;
  • 可选支持货币表述,例如把金额转成 “one thousand two hundred dollars and thirty-four cents”。

在决定引入之前,我特意把库源码翻了一遍。它的核心逻辑没有依赖外部插件,全部是纯 Dart 代码加少量测试用例。这一点非常关键,后面做鸿蒙化适配时,平台差异的风险基本被限制在 Dart 运行时层面。

库的短板也很明显:不支持中文、日文这类需要量词体系的语言,也不处理东亚财务场景里的“大写金额”。所以用法上要把它定位成一个“英文数字表达引擎”,中文场景需要在其上做二次封装,这才引出了后面“财务治理底座”这件事。

1.3 为什么不直接用 intl 或自己写正则

很多 Flutter 工程里已经引了 intl 来处理日期和数字格式化。但 intl 的 NumberFormat 解决的是1,234.56这种货币/千分位展示,它不负责“读出来”。两者解决的问题是相邻但不同的:一个面向视觉排版,一个面向语言表达。财务系统里常常两者都需要,但功能上无法互相替代。

自己写正则或者状态机就更不划算了。你还要考虑负数读法、小数点的读法、整数部分为零的情况、超长数字的 overflow 风险。这些用例在 numbers_to_text 里已经覆盖了,而且有现成的单元测试可以对齐。与其从零造规则,不如把能力接进来,把精力留给鸿蒙适配和业务封装。

2. 鸿蒙化适配不是“复制源码”,而是四项工程决策

2.1 鸿蒙 Flutter 生态的现状决定了适配方式

鸿蒙 NEXT 之后,应用不再走 AOSP 兼容层,Flutter 应用要跑在鸿蒙设备上,得使用适配了 OpenHarmony 的 Flutter 分支。我这次用的是社区维护的 flutter_ohos 系列工具链,创建工程、构建 HAP 包的流程和标准 Flutter 命令有差异,需要单独装一套环境。

这个现状直接影响适配路径:标准 Flutter 工程里能跑通的依赖,在鸿蒙环境下不能默认信任。包括 dart pub 的源地址、插件里如果有 MethodChannel 调用的原生实现、构建时生成 touch 文件的行为,都可能因为本机 Flutter 工具链不同而出现偏差。对于纯 Dart 库,适配工作会轻很多,但流程不能省,每一步都要验证。

2.2 纯 Dart 库与原生插件的适配难度差异

给刚接触鸿蒙化适配的同学划个重点:Flutter 插件分成两类。

第一类是纯 Dart 包,比如 numbers_to_text。它不碰原生 API,理论上代码可以原样复用,也不需要为鸿蒙单独开发原生桥接层。风险集中在依赖解析、运行时行为和构建产物上。

第二类是平台插件,内部通过 MethodChannel、EventChannel 或者 PlatformView 访问 iOS / Android 原生能力。这类插件即使是标准 Flutter 场景也要写两套原生代码,鸿蒙化时需要额外补一套 OpenHarmony 原生实现,工作量大得多。

numbers_to_text 属于第一类中的“甜点案例”。它本身没有原生代码,适配成果几乎可以平移到任何鸿蒙 Flutter 工程里。你真正要做的,是在鸿蒙工具链下重新跑一遍依赖解析、编译、测试和打包验证,确认它在目标运行时上行为一致。

2.3 适配评估清单:动手前先回答四个问题

我自己在开始前会先做一张评估表,把风险可视化:

检查项重点内容结论判断
依赖树该库是否依赖其他 Flutter/Dart 包numbers_to_text 依赖极少,风险低
平台能力是否用到 dart:io、dart:ui 等平台相关实现纯逻辑库,通常只用基础集合类型
构建链pub get 能否在鸿蒙 Flutter 分支下解析成功需要在镜像源和 lock 文件上做检查
运行时相同输入在 ohos 模拟器与 Android 上的输出是否一致用单元测试固化为回归基线

回答完这四个问题,适配策略就清晰了:直接引源码、跑测试、固化基线、再封装。不需要写任何原生桥接层。

3. 正式适配:环境初始化、依赖改造和验证链路

3.1 工具链准备与鸿蒙工程初始化

适配的第一步是确保本机工具链是“鸿蒙版”的。我用到的环境是这样组合的:

  • DevEco Studio 负责打开鸿蒙工程、管理 SDK;
  • 鸿蒙 Flutter 分支工具链负责解析 Dart 依赖、编译 Flutter 侧代码、最终构建 HAP;
  • ohpm 处理鸿蒙原生侧的依赖,如果你的工程完全不涉及原生插件,这一步基本不会用到。

工具链装好后,我用鸿蒙分支的 flutter 命令创建了工程:

flutter create --platforms ohos . --project-name sample_numbers_to_text

注意这里--platforms ohos在标准 Flutter 里是不存在的参数,只有装好鸿蒙分支之后才会生效。创建完成后,工程里会出现一个ohos目录,里面是鸿蒙侧的壳工程结构,后续构建 HAP 就是从这里走的。

3.2 pubspec 改造:本地路径依赖比远程依赖更可控

numbers_to_text 本身在 pub.dev 上有版本,直接把版本号写进pubspec.yaml通常就能拉取。

但我在鸿蒙分支下实际执行flutter pub get时,遇到过远程源解析超时的情况。社区常用的解法是配置镜像源,比如把 PUB_HOSTED_URL 指到可用镜像。我的建议是:在适配和调试阶段,改成本地路径依赖会更稳。

dependencies: numbers_to_text: path: ./third_party/numbers_to_text

把三方库的完整源码放到工程的third_party目录下,以path形式引用。好处有三个:

  1. 不受远程源波动影响,pub get 秒过;
  2. 方便直接阅读和修改库内部实现,排查问题时不用在.pub-cache和工程之间来回跳;
  3. 后续如果要做针对性修改,比如补丁逻辑,直接在本地源上维护,build 产物一致。

坏处也明显:升级依赖版本时需要手动同步上游。我的习惯是在本地目录里保留一个VERSION文件记录上游版本号,同时把修改点用注释标记出来。这类本地依赖在团队协作时要特别注意,最好配套一份同步脚本,避免 A 机器改了、B 机器不知情。

3.3 单元测试迁移:把库自带用例跑成鸿蒙回归基线

依赖接上之后,第一件事不是写业务代码,而是把库自带的测试用例跑一遍。

我先把test/目录下的用例原封不动复制到工程里,用鸿蒙 Flutter 分支执行:

flutter test

这一步非常能暴露问题。如果库内部用了dart:io的Platform相关能力,在鸿蒙运行时可能会表现出细微差异。实测下来 numbers_to_text 整组测试在鸿蒙分支下全部通过,说明它的核心规则不依赖具体系统平台。这个结果给我吃了定心丸,后续业务侧怎么用它都不用担心基础规则错乱。

不过“测试通过”只是第一步。真正值得做的事情,是把这些用例当作回归基线的种子:把业务侧可能用到的高危数据也加进去,比如超大金额、带负数的对账结果、超过两位小数的场景。库保证了通用能力,业务用例保证你封装的边界符合财务口径。

3.4 示例工程上鸿蒙模拟器的验证路径

单元测试通过不代表端上表现没问题。我拉起了鸿蒙模拟器,把 example 工程的页面跑起来,做了一个最简单的输入输出验证:输入123456.78、-0.99、0、1000000007四组数据,看界面文本是否正确显示。

这个验证阶段需要注意的是:模拟器和真机的字体渲染不同,长文本不要只检查屏幕显示,还要把转换结果打印到日志里做字符级比对。数字转文本场景最怕两个问题,一是文本截断肉眼看不出来,二是 Unicode 空格或连字符显示异常。我习惯用debugPrint输出原始字符串,再拿回调结果在业务逻辑里做二次断言。

4. 从“demo 能跑”到“财务治理底座”的二次封装

4.1 中文金额大写的补全方案

numbers_to_text 输出的是英文读法,直接塞进中文财务单据里不合适。财务治理底座要解决的第一个问题,就是把英文引擎的产物与中文财务口径融合。

我做的第一层封装是中文“金额大写”转化。财务上有明确规范,比如 123456.78 要输出“壹拾贰万叁仟肆佰伍拾陆元柒角捌分”,这里有几个细节容易被忽略:

  • 阿拉伯数字零的多种表达,比如连续零合并成“零”;
  • 元整规则:小数全为零时补“整”字;
  • 角分规则:有角无分时补“零”分,有分无角时角位写“零”;
  • 负数的表达,财务上通常写作“负”或括号形式。

方案上我选择了“结合而非替代”:整数部分的英文规则交给 numbers_to_text 处理,中文大写单独写一个转换函数,底层共用同一个“按量级分组”的思路。这样做的好处是,后续如果要支持日文、韩文或其他语言,不需要重写英文引擎,只需要扩展不同语言的量级词表。

4.2 精度处理:财务计算里的 double 是“合法的坑”

数字转文本这个功能本身不复杂,但它跟金额打交道,必然牵扯精度问题。财务系统里最忌讳直接用 double 做运算和展示,0.1 + 0.2 的浮点误差在账目对齐时会让对不上账的排查变得极其痛苦。

所以在封装层我做了几件事:

  1. 入参定为String或int(以“分”为单位),而不是double;
  2. 需要转换时先把字符串解析成整数分,再拆分成元、角、分三部分;
  3. 在财务场景里尽量让业务测把金额以“分”为单位传入,由封装层决定小数点的位置。

这样就把精度问题拦截在入口之外。如果你直接拿一个double传给 numbers_to_text,输出的文本可能跟你 UI 上显示的金额不一致。用户看到界面上是 0.30,语音播报却读成 0.3,这种不一致对财务应用是致命的。

4.3 统一本地化服务与审计留痕

二次封装的目的不是做一堆零散函数,而是收敛成一个统一的服务接口。我最终在工程里落地的形态是这样的:

class FinancialNumberTextService { static String toEnglishReadable(String amountInFen) { ... } static String toChineseUpperCase(String amountInFen) { ... } static String toLocalizedText(String amountInFen, Locale locale) { ... } }

所有业务侧只依赖FinancialNumberTextService,不直接 import numbers_to_text。将来更换底层库或者扩展语言支持时,只改 service 内部实现即可。

财务治理底座的另一个关键点是审计留痕。每次转换,我都会把原始入参、目标语言、版本号、转换结果写到日志链路里。这样财务对账如果出现“某笔金额播报和凭证不一致”的投诉,可以直接回溯是哪一版代码、哪个入参导致的。

5. 实际适配中踩过的坑与完整排查链路

5.1 依赖解析失败:pub get 的“假成功”与真失败

第一次在鸿蒙分支下执行flutter pub get,终端显示成功,但构建时一直报找不到 numbers_to_text 的符号。排查链路是这样的:

第一步,看pubspec.lock里有没有该包记录。一看发现 lock 文件里确实有,但版本号指向的缓存目录不存在,典型的“缓存残留”问题。

第二步,清理本地 pub 缓存,重新拉取。执行flutter clean后,在工程目录删掉.dart_tool和pubspec.lock,重新 pub get。这个操作在标准 Flutter 里很常见,但在鸿蒙分支下需要用鸿蒙工具的 flutter 命令去做,不能混用两个工具链。

第三步,验证依赖是否真正进入工程。打开.dart_tool/package_config.json,检查 numbers_to_text 的解析路径是否指向正确位置。

这次排查的关键教训是:终端输出“success”不一定是真成功,以 package_config 的内容为准。依赖解析类问题,90% 可以用“清缓存、删 lock、重新解析”三板斧解决。

5.2 构建产物里的行为差异:测试通过不代表 HAP 内一致

单元测试全部通过之后,我第一次打 HAP 包时却翻车了:同一组测试数据在测试环境里输出正常,打包后在真机上调试时,小数部分的表述和预期不一致。

排查思路:先怀疑 Numbers 库在小数处理上有平台分支,翻了源码发现根本没有平台判断。再怀疑构建压缩过程影响了字符串常量,被压缩的常量在 Dart 里一般不改变行为。最后定位到业务侧代码:我在封装层用toStringAsFixed(2)对 double 做了舍入后再转文本,Android 的舍入规则和鸿蒙底层实现存在一个边界差异。

这个坑的教训是:跨平台适配时,问题常常不在三方库,而在你叠加的封装逻辑里。排查时先检查自己包的那一层有没有用到平台相关的 API,再往下挖依赖库。建议在封装层写一批边界用例,比如0.005、1.005、-0.005,把这些值在测试环境和真机上各跑一遍,结果不一致就直接定位到舍入函数上。

5.3 包体积增量:合理评估鸿蒙 HAP 的体积成本

引入一个三方库,多少会增加 HAP 体积。numbers_to_text 本身的代码量不大,实测下来对 HAP 体积的影响在可接受范围内,但如果你引的是整个库的 example 或多余扩展,体积会明显上涨。

我在接入时做了两个约束:

  1. 只引入主库,不引入 example 相关代码;
  2. 用dart pub deps检查是否有多余传递依赖,numbers_to_text 是零传递依赖起步,干净利落。

打包后对比,HAP 体积增量约 12KB 左右。对这个量级的收益(一套可靠数字转文本能力)来说,性价比很高。如果你的工程对体积非常敏感,可以考虑按需裁剪源码,把不用的货币扩展去掉,能再压缩一部分体积。

5.4 给后续适配者的检查清单

踩完这些坑之后,我把流程固化成了清单,给团队其他人复用:

  1. 确认工具链版本:鸿蒙 Flutter 分支与 DevEco Studio 版本配对;
  2. 用本地路径依赖替代直接 pub get,减少源波动影响;
  3. 先跑库自带测试,再补业务侧高危数据用例;
  4. 封装层不要直接用 double,用“分”为单位的整数串;
  5. 舍入逻辑统一用封装层函数,禁止业务侧自行舍入;
  6. HAP 打包后,在真机上重新跑一遍边界数据验证;
  7. 记录审计日志,把转换版本和入参固化下来。

这套清单现在是我所有鸿蒙 Flutter 工程接入通用 Dart 库的固定流程,不只针对 numbers_to_text,换成其他纯 Dart 逻辑库同样适用。

最后再补一句个人体会:三方库鸿蒙化适配,最耗时往往不是改代码,而是把“验证环境”和“回归基线”搭起来。这两件事做扎实了,后续升级依赖和扩展业务场景都会轻松很多。如果你正在做类似适配,我建议先把测试基础设施铺好,再碰业务代码,顺序反了会一边踩坑一边救火。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询