ARTICLE DETAIL

资讯详情

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

Flutter三方库鸿蒙化适配:numbers_to_text数字转文字与中文金额大写实践

Flutter三方库鸿蒙化适配:numbers_to_text数字转文字与中文金额大写实践 最近在整理一批 Flutter 三方库的鸿蒙化适配第一个拿到桌面上反复看的就是 numbers_to_text。这个库做的是本地化数值转换把数字变成人类语言比如把 1234567 变成“一百二十三万四千五百六十七”。单看功能似乎不大但一旦落到鸿蒙应用的财务、语音播报、无障碍读屏这些场景它就成了一个不能含糊的底座。尤其是在国内语境下合同金额、报销单、发票校验里的中文数字和银行大写靠手写正则和一堆 if else 很容易翻车直接用经过验证的转换库会省掉很多心力。这篇文章不打算只贴一段复制依赖就能跑的代码而是把 numbers_to_text 在鸿蒙工程里从引入到跑通、再到扩展成财务大写能力的过程完整拆一遍。我会尽量讲清楚每一步背后的原因为什么纯 Dart 库也依然要做适配、鸿蒙端的语言环境怎么拿、金额大写规则为什么不能靠 numbers_to_text 一把梭。如果你正在做 Flutter 鸿蒙迁移或者只是想在自己的鸿蒙应用里做一套靠谱的中文数字与金额格式化工具这篇文章应该能给你省下好几天的踩坑时间。1. 这个库到底解决什么问题以及为什么值得做适配1.1 数字转文字在很多业务里是“刚需”先不聊鸿蒙先说数字转文字这个能力本身。人在阅读一串长数字时比如 158230987眼睛需要先分组、再逐位辨认心智负担很大。但如果你把它写成“一亿五千八百二十三万零九百八十七”哪怕是不太擅长数字的人也能快速建立量级概念。这就是 numbers_to_text 这类本地化数值转换库的核心价值把机器友好的数字表达转换成人类友好的语言表达。在很多业务场景里这个能力根本不是锦上添花而是硬需求。最典型的是财务场景合同里的金额大写、报销单的校验、发票打印时金额的“银行大写”都需要把阿拉伯数字转成中文大写或中文小写。再比如语音播报如果 App 直接读“余额 12345.67 元”听感极差用户一耳朵根本反应不过来是“一万两千三百四十五点六七”还是“十二万三千”。无障碍读屏也有同样的诉求。游戏里的计分、教程里的数学题面、以及儿童教育应用里的数字认知都需要把数字转成口语化的文本。numbers_to_text 这个库的优势在于它支持的语言很多英语、西语、葡萄牙语、法语、德语、意大利语、波兰语、罗马尼亚语、俄语、印地语、中文繁简体都有覆盖。对于要做海外市场又想兼顾国内本地化的团队一个库能同时覆盖两种业态的转换需求非常省事。它内部基本是纯 Dart 实现不依赖 Android 或 iOS 原生代码这也是我一开始以为“换个工程就能跑”的原因。1.2 纯 Dart 库不等于零适配这里必须先把一个误区掰开纯 Dart 库在鸿蒙上“能编译”和“能像原生端一样稳定工作”是两回事。纯 Dart 的优点很明显Dart 运行时在鸿蒙的 Flutter 环境里是完整支持的所以 numbers_to_text 的核心转换逻辑确实可以直接复用不需要你用 ArkTS 再写一份。但“引入依赖”只是答案的一半。你要面对的是整个鸿蒙工程链路的变化Flutter SDK 的分支不同、构建工具是 hvigor 而不是 Gradle、插件加载方式更像“模块集成”而不是“aar/jar 依赖”、原生侧交互要用鸿蒙的 API 能力而不是 Android API。这些链路性的差异才是鸿蒙化适配的真正工作量。举一个很具体的例子numbers_to_text 只是做转换它本身不需要拿系统参数但如果我要让它在鸿蒙应用里做到“跟随系统语言自动切换”我就需要从鸿蒙系统侧读取当前语言偏好再把这个值传给 Dart 端。读取系统语言这个动作在 Android 上可以写Locale.getDefault()在鸿蒙上就得走鸿蒙应用能力框架里的配置接口。这种差异不是 numbers_to_text 造成的而是平台通道造成的。所以我会把它定义为“库加鸿蒙运行环境的联合适配”而不是“改造库本身”。1.3 主要的适配层次我把这个库的鸿蒙化工作拆成了三个层次方便理解优先级。第一层是依赖可用性。确认 numbers_to_text 在鸿蒙 Flutter 工程的 Dart 侧可以被解析、编译、运行不引入不支持的 FFI 或原生插件。这一层通常半小时内就能搞定。第二层是能力一致性。同一个输入在 Android、iOS、鸿蒙三端上要得到完全一致的输出。数字转换这种纯算法逻辑很容易做到但要注意畸形输入、超大数、负数、小数点的处理行为是否一致。这一层靠测试用例来保证。第三层是平台差异化能力。比如读取系统语言和国家地区再比如某些转换场景需要依赖系统货币符号、地区格式。这些能力必须通过鸿蒙的 Platform Channel 来补齐。理解了这三个层次后面所有步骤都会变得清晰先让库跑起来再补测试最后处理平台差异。下面我从环境准备开始逐步讲实操。2. 适配前准备把鸿蒙 Flutter 工程和工具链理干净2.1 鸿蒙 Flutter SDK 怎么选做鸿蒙 Flutter 适配第一步不是写代码而是确认你手上的 Flutter SDK 是支持鸿蒙的版本。OpenHarmony 社区维护了一个针对 HarmonyOS 的 Flutter SDK 分支和 Google 官方的 Flutter SDK 分支在使用习惯上有差异比如某些命令的返回信息、插件注册机制、以及内部引擎的 API 都有所不同。你需要做的第一件事是检查当前环境。我一般先用flutter doctor -v看一遍整体状态再确认flutter --version。如果输出的版本信息里能看到OpenHarmony或HarmonyOS相关标记说明分支选对了。如果你是在 DevEco Studio 里直接创建 Flutter 工程通常它会自动帮你关联到配套的 Flutter SDK路径在设置里可以查。这里有一个我踩过的坑工程里如果同时存在多个 Flutter SDK 路径很容易导致 Dart 版本不一致编译时会出现“The current configured Flutter SDK is not known to be fully supported”之类的提示。如果你也碰到这句话别慌先检查flutter config里的 SDK 路径是不是指向了鸿蒙分支尤其要注意不要用官方稳定版去构建鸿蒙工程否则后面会有一堆莫名其妙的报错。2.2 pubspec 与依赖解析SDK 就绪后就可以把 numbers_to_text 加入到工程依赖里了。在pubspec.yaml中添加dependencies: flutter: sdk: flutter numbers_to_text: ^3.2.0然后执行flutter pub get。这一步如果顺利说明这个库的 Dart 侧依赖没有踩雷。不过 pub get 成功只代表“能拉到包”不代表“能编译”。我建议你顺手做一次依赖树检查flutter pub deps --stylecompact重点看 numbers_to_text 有没有传递性依赖。它本身很干净一般不会有问题。但如果你后续在同一个工程里接了其他数值处理库就要小心intl这类包因为intl的版本可能和 Flutter SDK 内置的版本冲突。如果真的冲突优先选择把依赖版本抬高到和目标 SDK 兼容的版本而不是强行覆盖 pubspec 约束。另外一个容易被忽略的问题是flutter_test的依赖。如果你在鸿蒙环境下跑单元测试pubspec 里的dev_dependencies中flutter_test版本必须和 SDK 匹配。在 OpenHarmony 分支下有些版本的flutter_test不能直接在桌面环境里跑需要调整成“忽略测试”或改在鸿蒙设备上跑。这个细节我会在后面的测试部分再展开。2.3 构建工具链从 Gradle 到 hvigor适配到这个阶段你会发现鸿蒙工程与原生的 Flutter 工程在底层构建上完全换了套路。Android 工程依赖 Gradle而鸿蒙工程依赖 hvigor构建脚本和任务名都不一样。这意味着你在 Android 上的一句./gradlew assembleRelease在鸿蒙这里要换成hvigorw配合 HAP 打包命令。如果你是在 DevEco Studio 里打开鸿蒙 Flutter 工程IDE 一般会帮你把 hvigor 相关的东西配好。但我建议至少要手动跑一次命令行构建因为 CI 环境大概率用得上。先找到工程根目录下的hvigorw然后执行./hvigorw assembleHap首次构建会特别慢因为要下载鸿蒙 SDK 的公共依赖还会编译 Flutter 引擎相关的加载库。耐心等即可如果出现签名相关报错说明设备调试用的证书没有配置需要先到AppScope/app.json5里确认signingConfigs是否完整。构建工具链的坑大多集中在环境变量上。鸿蒙 SDK 的路径、Flutter SDK 的路径、Node 版本任何一个不对都会报错。我建议在工作目录下建一个local.properties文件把sdk.dir显式写出来这样可以避免 IDE 和命令行读到不一致的配置。3. 核心实现让数字表达在鸿蒙上“语言级直观”3.1 理解 numbers_to_text 的语言规则在动手之前至少要弄明白这个库的基本调用方式。以中文为例大概是这样import package:numbers_to_text/numbers_to_text.dart; final converter NumbersToText(language: Language.ZHT); final result converter.toText(1234567); // 结果大致为一百二十三万四千五百六十七不同版本的 API 名称可能有差异比如toText可能叫convert或者要传NumberToTextRequest。我这里强调的是思路不是让你照搬方法名真到你本机上打开包源码看一眼接口定义就知道怎么调了。这个库处理中文的方法值得夸一句它对“零”的规则处理得比较细致。比如 10001 会转成“一万零一”而不是“一万零零一”10010 会转成“一万零一十”。这些细节看起来简单但你手动写规则时很容易出错尤其是连续多个零的情况。语言规则引擎的价值就在这它把零处理、单位分级、负数符号、小数点读法都封装好了。中文里还有繁简体之分。Language.ZHT是繁体中文Language.ZHH是简体中文。如果你面向大陆市场要用ZHH。如果面向港澳台再考虑ZHT。这个选择不要写死最佳实践是跟随系统语言动态决定。3.2 解决“语言环境从哪来”的问题要让转换行为跟随系统设置Dart 侧就得知道设备当前是什么语言。虽然 Flutter 框架本身也提供PlatformDispatcher.instance.locale这类 API但在鸿蒙分支上这个值不一定能反映用户后来在系统设置里切换的语言因为底层的实现和 Android 不完全一致。我的做法是在鸿蒙侧主动拿一次系统语言通过 MethodChannel 发给 Dart 端。这样既不依赖 Flutter 框架内部对鸿蒙的适配完整度也能保证拿到的值一定来自系统层后续切换语言时只需要重新拉取一次。Dart 侧定义一个通道import package:flutter/services.dart; class LocaleBridge { static const MethodChannel _channel MethodChannel( com.example.numeric_formatter/locale, ); static FutureString getSystemLanguage() async { try { final result await _channel.invokeMethodString(getSystemLanguage); return result ?? zh_Hans; } on PlatformException catch (_) { return zh_Hans; } } }鸿蒙原生侧用 ArkTS 注册这个 Handler返回系统当前语言。不同版本的鸿蒙 SDK 获取语言的方式不太一样你现在看到的是通过getCapability或资源管理接口来拿具体 API 以你本机的 SDK 为准。只要保证返回值的格式统一成语言_地区风格就行比如zh_CN。知道系统语言后再决定用ZHH还是ZHT这样基本能做到“系统切语言数字表达跟着变”。3.3 财务场景扩展从“数字转文字”到“金额大写”numbers_to_text 的定位就是通用数字转文字它不会帮你输出“壹佰贰拾叁万肆仟伍佰陆拾柒元整”这种银行大写。国内财务场景真正缺的其实是这个能力这也是我在标题里把“财务治理底座”单独拎出来的原因。那怎么在不重写轮子的前提下实现金额大写我的思路是用 numbers_to_text 做整数部分的转换基础自己再补一套“人民币大写规则”。这类规则并不复杂核心就是两步第一步把数字拆成整数部分和小数部分第二步把整数部分映射到“壹贰叁肆伍陆柒捌玖”单位映射到“拾佰仟万亿”小数部分映射到“角分”。这里有个和通用数字转换不同的点银行大写里数字要全部替换为大写汉字而 numbers_to_text 默认给的是普通汉字。所以我并不建议直接拿 numbers_to_text 的输出做字符串替换因为通用表达里“零”的出现规则和银行大写里“零”的出现规则有细微差别。更稳妥的做法是你写一个专门的大写转换函数把整数拆解逻辑自己处理用类似下面的模式String toRmbUpper(num amount) { final intPart amount.floor(); final decimalPart ((amount - intPart) * 100).round(); // 这里实现“亿/万/仟/佰/拾”逐级拆解与“零”的合并规则 // 返回类似壹佰贰拾叁万肆仟伍佰陆拾柒元捌角玖分 }你可能会问那 numbers_to_text 在这里还有什么用它仍然有价值因为在很多业务里系统展示文案用的是“一百二十三万四千五百六十七元”而不是银行大写。一个是给人读的一个是给财务凭证用的。两个输出可以并存日常展示走 numbers_to_text财务专用走大写规则。这样既省了重复造轮子又保证了专业场景的合规表达。3.4 适配中要关注的 Unicode 与文本格式很多人会在数字转文字后直接拼字符串展示结果遇到换行、对齐、标点符号不统一的问题。鸿蒙的文本渲染引擎对 Unicode 的支持总体没问题但你要注意几个细节。第一个是中文标点。转换结果里的逗号、句号、小数点要统一用全角还是半角这取决于你的展示容器和阅读场景。财务报告里多用全角中文标点代码日志里反而建议半角避免日志系统解析出错。第二个是负号和括号。财务场景里负数金额经常写成“壹佰贰拾叁”而不是“负壹佰贰拾叁”这种表达习惯不能靠库解决得在你自己的格式化层处理。我会在调用层做一层适配金额小于零时先取绝对值转换再根据业务需求决定是加“负”字还是加括号。第三个是超大数和精度。Dart 的num类型在处理超过一定位数的大数时小数部分可能会有浮点误差。如果你要转换的金额涉及分位精确计算我建议在业务层先把单位换成“分”存储也就是用整数去算最后再转成元展示。这是金融系统的通行做法也能避免一堆精度问题。4. 实操记录把 numbers_to_text 落地到鸿蒙工程4.1 建立工程结构我的落地路径不是新建一个鸿蒙工程而是在已有的 Flutter 鸿蒙工程里加模块。如果你是从零开始建议先通过 DevEco Studio 创建一个带 Flutter 模块的鸿蒙工程然后保持目录结构清晰至少要有这几个部分dart侧代码放业务逻辑ohos目录放鸿蒙原生入口与平台通道test目录放转换测试。下面是简化过的目录示意my_harmony_app/ ├── ohos/ │ ├── entry/ │ │ ├── src/main/ets/ │ │ │ ├── entryability/ │ │ │ └── pages/ │ └── build-profile.json5 ├── lib/ │ ├── main.dart │ └── services/ │ └── number_locale_bridge.dart ├── test/ │ └── numbers_conversion_test.dart ├── pubspec.yaml └── hvigorw我特意把lib/services单独建目录因为后面你很可能不止一个平台通道把通道逻辑收敛到一个目录里维护成本会低很多。原生侧的 EntryAbility 只负责注册不做具体业务这也是一个值得坚持的规范。4.2 原生侧注册 MethodChannel鸿蒙原生侧注册 Handler 时我的做法是在EntryAbility的初始化流程里挂载。代码如下依然以你本机 SDK 的 API 为准import { MethodChannel } from ohos/flutter_ohos; function registerNumberChannels() { const channel new MethodChannel(com.example.numeric_formatter/locale); channel.setMethodCallHandler((call) { if (call.method getSystemLanguage) { const lang getSystemLanguageFromConfig(); return Promise.resolve(lang); } return Promise.reject(unsupported method); }); }注册时间尽量靠前最好在 Flutter 页面加载完成前完成否则 Dart 侧第一次调用时可能拿不到响应白白触发异常分支。同时我在同一套结构里预留了第二个通道用来返回当前地区使用的货币代码。因为某些场景下你可能需要显示“CNY”“USD”这样的币种符号单纯靠系统语言推断货币不一定准。这个通道的编码思路和语言通道一致无非就是改为调用货币相关接口。4.3 编写本地化转换服务类原生侧通道建好后Dart 侧就可以提供一个统一的服务类把语言获取、转换、财务大写封装在一起业务页面尽量不直接依赖 numbers_to_text 的 API。这样将来如果你想把转换库换成自研实现只改服务类不用动页面代码。一个简化版本的服务类大概是这样的class NumberLocalizationService { FutureString toText(num value) async { final lang await LocaleBridge.getSystemLanguage(); final language lang.startsWith(zh) ? Language.ZHH : Language.ENGLISH; final converter NumbersToText(language: language); return converter.toText(value); } FutureString toRmbUpper(num value) async { // 调用自研人民币大写逻辑 } }实际开发里我还会加一个缓存层把语言参数缓存起来避免每次调用数字转换都去走一遍原生通道。因为通道调用虽然快但在高频场景下也算不必要的开销。缓存可以做成App 启动时取一次系统切换语言时通过通知刷新业务侧每天基本都是同一个语言环境没必要频繁拉取。测试的时候我建议覆盖这些边界0、负数、带小数的金额、万位和亿位的交界处、连续零的数字。尤其是连续零的用例建议把 1001、10001、100001、10000001 这几个值都测一遍。你会发现很多转换库在这些值的处理上差距很大踩过坑的人自然懂。4.4 打包 HAP 与真机验证本地调试阶段通过后就要打包成 HAP 上真机验证了。鸿蒙的构建和 Flutter 的flutter build不是同一条命令你需要通过 hvigor 构建出 HAP 包然后在 DevEco Studio 里跑签名与安装。打包时有一个容易忽略的点Flutter 资源的打包。如果你在pubspec.yaml里声明了 assets鸿蒙端的构建脚本要把这些 assets 一并打进 HAP有时候还会要求你在ohos模块的module.json5里配置资源目录。否则你会发现本地 IDE 运行一切正常但打包之后某些图片或语言配置文件找不到。签名问题也是高频报错点。鸿蒙应用安装到真机上需要签名调试签名和发布签名是两套体系。我建议趁早配置好自动签名不然每台设备都要手动导入证书团队协作时特别耽误事。打包验证时我还会随手检查日志里有没有 Flutter 引擎相关的 warning。很多问题在 debug 模式下不出现release 模式下才会冒出来这类问题通常和引擎裁剪有关比如 release 包去掉了部分 UTF 相关的文本处理能力导致某些字符显示异常。真机 HDMI 投屏看一遍展示效果比在 IDE 里看截图更靠谱。5. 常见问题与排查思路5.1 错误码 2300056 这类运行时错误怎么定位在鸿蒙开发群里经常有人发类似“Android 请求正常鸿蒙请求返回 2300056”的帖子。这个错误码出现在网络请求相关的场景里比较多含义对应网络不可用或访问受限。需要注意的是这类错误有时候并不是网络真的不可用而是缺少权限声明或者域名没加到网络安全配置里。虽然 2300056 跟 numbers_to_text 本身没有直接关系但如果你把一个带网络请求的鸿蒙应用和数字转换功能一起做联调非常容易被这个错误打断节奏。我的建议是先把它当作“环境错误”而不是“代码错误”来查第一步看module.json5里面有没有声明网络权限第二步确认请求地址是否支持 HTTPS第三步看是不是设备本身连了受限网络。大部分情况都是这三板斧能解决。5.2 数字转换结果在 Release 包中不一致我遇到过一种情况同一个数字在 Debug 包转换正常在 Release 包里变成了奇怪的符号。排查到最后发现是 Flutter 引擎的 release 模式做了 tree shaking某些字符映射表没有被完整打进包里。这个问题和鸿蒙适配没有绝对关系在 Android 上也可能出现只是鸿蒙的打包链路更需要你手动确认。遇到这种问题优先做两件事第一升级到 OpenHarmony 社区当前推荐的稳定版 Flutter SDK很多字符缺失问题是早期分支的 bug第二添加一个“预启动自检”在 App 启动时内置一段固定数字转换测试如果结果和预期不符直接上报日志。这比用户反馈“显示乱码”再排查要高效得多。5.3 中文数字处理的边界条件用 numbers_to_text 时要特别留意超大数。虽然 Dart 的num能存储很大的整数但中文数字表达里“亿”以上还有“兆”“京”等更大的单位不同库的支持程度不一样。如果你的业务金额很少超过万亿级基本不用担心。但做数据大屏、政务报表这类项目建议先确认库在 10^12 以上量级的表现不够再自己补。另一个边界是“零”。中文数字表达式里零的出现非常有讲究10010 是“一万零一十”而不是“一万零十”1000010001 是“十亿零一万零一”这些规则手动维护起来很头疼。numbers_to_text 在这点上做得够用但你在测试时一定要把这些连续零用例铺进去防止未来升级库版本时行为变化。小数部分也同样需要测试。1.5 转成中文应该是“一点五”而不是“一点五零”2.05 应该是“二点零五”而不是“二点五”。如果库的旧版本处理不到位建议做一层后处理把末尾多余的零去掉。5.4 问题排查速查表为了方便查阅我把上面提到的问题整理成一张表现象可能原因排查顺序pub get 报依赖冲突传递依赖与 Flutter SDK 版本不匹配检查依赖树调整版本约束锁定直接依赖构建报 SDK 不支持的提示Flutter SDK 选了非鸿蒙分支重新配置 SDK 路径确认分支版本真机安装失败签名配置或证书未正确导入检查签名配置重新生成调试证书数字转换结果在 Release 和 Debug 不一致引擎字符映射表被裁剪升级引擎版本做启动自检系统语言切换后转换语言未变缓存未刷新或通道未重新调用监听系统语言变更刷新缓存HAP 包内资源找不到assets 未配置到鸿蒙模块检查 module.json5 资源配置网络请求返回 2300056权限缺失或网络环境限制排查权限声明、HTTPS、设备网络这张表不止适用于 numbers_to_text很多 Flutter 库迁移到鸿蒙时都会遇到类似的问题。本质都是同一件事Dart 层逻辑容易迁移平台层的接口、配置、构建链路需要逐项对齐。6. 我在适配中总结的经验用 numbers_to_text 做鸿蒙化适配这件事技术上并不复杂真正花时间的反而是那些“预期之外”的部分。我最大的体会是纯 Dart 库的鸿蒙化不是“把库塞进工程”这么简单你在 Android 上没注意过的系统语言获取、资源打包、Release 引擎裁剪到了鸿蒙环境里都会重新冒出来。尤其在一个 App 同时维护 Android 和鸿蒙双端时哪怕转换逻辑一模一样两端的验证路径也不能互相替代。测试用例是我最想强调的一点。数字转换这类功能边界值一旦有 bug业务方在正式环境很容易发现而且影响往往很严重。我会在工程里保留一组固定的回归用例包含 0、负数、小数、万亿级、连续零、繁简体切换每次升级依赖、切换引擎或者调整构建配置后都跑一遍。这个习惯帮我挡掉过至少两次升级库版本带来的回归。如果你是在做财务相关 App建议不要把 numbers_to_text 的输出直接作为银行大写凭证那套规则必须独立实现并单独测试。数字转文字解决的是“人能顺利读懂”金额大写解决的是“财务合规”两者目标不同我强烈建议不要让一个函数同时承担两种职责。后面如果你要继续扩展可以考虑把转换能力做成一个独立的本地化服务模块英文数字、中文数字、银行大写、货币格式化统一收口再给鸿蒙端补上系统语言监听。这样不仅 numbers_to_text 能用未来接入其他格式化库也只需要替换服务内部实现对外接口完全不受影响。这大概就是鸿蒙化适配里最值得投入的那部分。
返回列表