资讯动态

Flutter鸿蒙化大整数精度问题与json_bigint适配实践

发布时间:2026/10/23 5:01:15 来源:尧图企业网站定制
不卖关子先还原一个我处理过的线上问题。后端接口返回一个订单号7262231312374189563前端 Flutter 页面显示成了7262231312374189440。一开始排查方向全在网络、缓存、后端折腾了大半天最后定位到dart:convert里的默认 JSON 解码数字一旦越过 53 位二进制的安全精度区间就会在转换过程中被悄悄截断。这不是 Flutter 专属的问题鸿蒙端跑同一套 Flutter 代码只要数据在 Flutter 与 ArkTS 之间仍用数字类型传递同样会丢精度。我的做法是在工程里接入json_bigint这个三方库并针对鸿蒙的 Flutter 工程做了一次完整适配依赖引入、解析器替换、模型字段改造、跨层通道字符串化都踩过一遍坑。这篇文章就把这次适配的完整路径写出来适合正在做 Flutter 鸿蒙化、并且接口里有大量大整数字段的团队参考。1. 为什么大整数 JSON 解析会丢精度1.1 一个 19 位数字是怎么“被改写的”很多同学第一次遇到大整数精度问题都在浏览器控制台里JSON.parse({id:12345678901234567890})打出来变成12345678901234567000。原因是 JavaScript 的 Number 类型基于 IEEE 754 双精度浮点只有 53 位二进制有效数字可以精确表示的整数上限是9007199254740991也就是2^53 - 1。超过这个值尾数只能靠四舍五入近似表现出来就是后面的位数开始变成 0、8、4 这类看似随机的尾巴。这个问题到了 Flutter 这边并没有消失。Dart 原生 VM 的int确实是 64 位最直观的理解是它能装下一个9223372036854775807听起来好像没有精度问题。但 Flutter 工程不一定只跑在原生环境一旦编译到 Web 端Dart 的int会被映射成 JS Number超过2^53的整数立刻丢精度。更隐蔽的是如果模型字段在某个地方被定义成了double或者num解析阶段就会走到浮点分支整型精度当场报废。1.2 Dart 原生也安全别太早下结论我在测试时专门确认过在原生 Dart 虚拟机里jsonDecode({id:9223372036854775807})确实可以解析成准确的int因为9223372036854775807刚好是 int64 的最大值。很多人会因此得出“原生端没问题不用处理”的结论这个结论在大整数不超过 int64 范围时勉强成立但工程上要小心三个场景后端使用雪花算法生成 ID字段类型是 64 位有符号 long单看是安全的可一旦进入 JS 环境就超过2^53Web 端和部分跨端场景直接丢精度。数据从 Flutter 通过通道传给鸿蒙原生层通道的消息编解码如果用了 JS Number 表示法就会出现“进入时是对的出来时变了”的诡异现象。模型层不写int而写num或dynamic解析出的数字在某些分支会被统一转成 double。还有一个容易被忽略的细节即使数字不超过 int64 上限Dart 里数字经过round()、toDouble()、除法和格式化等操作后也很可能被提升为浮点数。浮点数一旦参与稳定精确的整型就不再稳定。所以“原生 VM 安全”只能当作一个乐观假设不能作为工程依据。1.3 鸿蒙端比想象中多了一层风险鸿蒙应用里的 Flutter 通常不会单独存在而是要跟 ArkTS 侧的能力互相调用。Flutter 通过 MethodChannel 或 EventChannel 把数据传给 ArkTS 时引擎层会把 Dart 对象编码成标准消息标准消息对大整数没有特殊保护。ArkTS 的 Number 同样是 JS 风格的双精度浮点JSON.parse一个超出安全范围的整数字面量解析出来的值照样带尾巴。我后来复盘时把这个问题总结成一句话大整数精度不是某一层的职责而是整条数据链路都要遵守的约定。后端返回的是数字Dart 解析成 BigInt模型层转成字符串Flutter 通道再传字符串ArkTS 侧也按字符串接收任何一环私自把它变回 number精度就没了。1.4 哪些业务最容易被大整数坑到订单号、流水号、支付单号这类字段长度通常有 18 到 19 位接近 int64 上限也是丢精度最高发的地方。雪花 ID、账号 UID、文件编号很多分布式生成策略产出的就是大整数。金融、积分、奖励等需要精确计价的场景金额字段如果带小数再加上 64 位 ID双精度浮点根本扛不住。部分系统返回的高精度时间戳比如微秒甚至纳秒级在 JS 里同样会变成近似值。2. json_bigint 的解法与适配思路2.1 这个库到底做了什么json_bigint并不是什么黑魔法它的核心思路是在 JSON 解码阶段拦截数字判断当前数字是否超出普通数值的安全范围如果超出就把它解析成BigInt如果业务还希望保留原始字符串形式也有对应的字符串模式可选。传统jsonDecode把数字丢进浮点通道后就再也回不来了而json_bigint在进入类型系统之前就把“大整数语义”保存了下来。适用姿势很简单把它加到pubspec.yamldependencies: json_bigint: ^版本号按你工程实际情况定然后解析时不再使用dart:convert里的jsonDecode改为使用该库提供的解析入口。具体函数名可能因版本不同略有区别常见的是jsonBigIntParse或直接导入包后会暴露一个解码器你只需要保证工程里所有 JSON 解析都走同一个入口即可。2.2 它保留精度的底层逻辑从原理上说标准的 JSON 解析器会把数字 token 送到数值解析器里解析器根据是否有小数点来判断是 int 还是 double。json_bigint做的事情是在 token 级别介入先判断 token 是否属于整数再看它是否超过当前环境的安全整数范围如果是就调用BigInt.tryParse或者类似的路径直接生成BigInt。因为 BigInt 在 Dart 中没有位数上限只要内存足够多长的整数字面量都能原样保留。这里有个关键区别用正则或字符串替换去捞大整数和解析器在 token 级别处理可靠性完全不同。正则容易把小数、指数表示、科学计数法搞混而 tokenizer 级别能准确识别出哪些是真正的整数 token。这也是为什么我强烈建议直接用现成库而不是自己写一个 JsonParser。2.3 “纯 Dart 库为什么还要适配鸿蒙”json_bigint本身是纯 Dart 实现理论上在鸿蒙的 Flutter 引擎里可以直接运行不需要改 C 代码。但实际工程接入时适配工作一点都不少依赖能不能顺利拉下来版本兼容性是否满足鸿蒙 Flutter SDK 的 Dart 版本要求需要验证。存量代码里可能到处都是jsonDecode不改解析入口就相当于没接入。模型字段类型要从原来的int或double改成BigInt、String或动态判断。Flutter 与 ArkTS 之间的通道层如果不做字符串化数据照样在原生侧丢精度。所以这里的“适配”更多是工程层面的适配解析入口统一、模型类型改造、跨层数据格式化。把这四件事做干净才算真正跑通了整条链路。3. 鸿蒙化适配实战流程3.1 环境评估与依赖引入先确认你手上的鸿蒙 Flutter 工程是基于哪个 Flutter SDK 版本。通常鸿蒙适配版会跟随上游 Flutter 的某个版本因此在引入json_bigint之前先跑一次flutter pub add json_bigint然后看解析结果里 Dart SDK 的约束是否满足。如果出现版本冲突手动在pubspec.yaml里改成兼容版本即可。还有一个容易被忽略的点鸿蒙工程的 package 管理有时会走内部镜像或本地仓库如果网络策略比较严格flutter pub get可能无法直接访问默认的 pub 源。这时候可以把json_bigint的源码放进本地依赖目录通过路径引用dependencies: json_bigint: path: ./third_party/json_bigint这种做法在团队内部私有化、离线构建的场景下很实用。但要注意锁死版本避免后续更新时行为不一致。3.2 统一 JSON 解析入口我建议不管工程大小都建一个JsonUtil来做统一入口。这样后续所有解析行为都被收口排查问题的时候只需要看一个文件。// lib/core/json_util.dart import dart:convert as conv; import package:json_bigint/json_bigint.dart as jb; class JsonUtil { /// 统一 JSON 解码入口。 /// 默认情况下大整数会变成 BigInt。 /// [forceString] 开启后所有数字都会尽量以字符串形式解析适合需要原样保存的字段。 static MapString, dynamic decode(String source, {bool forceString false}) { if (forceString) { // 使用 json_bigint 的字符串模式函数名以你依赖的版本为准 return jb.jsonBigIntParse(source, requireString: true); } return jb.jsonBigIntParse(source); } /// 统一 JSON 编码入口。 /// 处理 BigInt 字段不能直接被标准 jsonEncode 序列化的问题。 static String encode(Object data) { return conv.jsonEncode(data, toEncodable: (dynamic value) { if (value is BigInt) return value.toString(); return value; }); } }注意不同版本的json_bigint导出的函数名和参数可能不同。建议主要关注“能解析 BigInt”这个结果而不是死记 API 名称。部分版本通过注册JsonDecoder的方式工作解析入口仍然是jsonDecode但解析行为已经被替换。建好JsonUtil之后全局搜索jsonDecode(把所有涉及 JSON 解析的地方统一替换为JsonUtil.decode(。替换过程要小心dart:convert里其他方法比如jsonEncode、jsonDecode同时出现在一个文件里容易漏改。3.3 模型字段与序列化适配解析层改成JsonUtil只是第一步模型层如果还写死int id运行时照样报类型错误。这里推荐两种做法字段直接定义成BigInt适合需要参与计算的场景。字段定义成String适合只展示、透传、拼接的场景。实际业务里订单号、UID 这类字段基本只做展示和透传所以我更推荐String。可以写一个统一的转换函数避免每个模型里都写一堆类型判断String bigIntToString(Object? value) { if (value null) return ; if (value is BigInt) return value.toString(); if (value is int) return value.toString(); if (value is double) return value.toString(); return value.toString(); }模型示例class Order { final String orderId; final int count; final String? remark; Order({ required this.orderId, required this.count, this.remark, }); factory Order.fromJson(MapString, dynamic json) { return Order( orderId: bigIntToString(json[orderId]), count: json[count] is int ? json[count] as int : int.tryParse(json[count].toString()) ?? 0, remark: json[remark] as String?, ); } MapString, dynamic toJson() { return { orderId: orderId, count: count, remark: remark, }; } }toJson里之所以不会出问题是因为orderId已经是字符串序列化时不会触发 BigInt 异常。如果模型里某些字段还是BigInt编码时必须通过JsonUtil.encode的toEncodable转成字符串。3.4 跨 ArkTS 通道必须字符串化这一步是整个鸿蒙适配里最容易踩坑的地方也是真正区别于普通 Flutter 工程的地方。Flutter 与鸿蒙 ArkTS 交互本质上是把 Dart 对象编码成标准消息后传给原生侧。标准消息对数字的处理依赖引擎实现很多情况下会把超出安全范围的 Dart int 转成 JS Number 再传过去结果就是一进通道就丢精度。我在项目里定的规矩很简单通道边界不传大整数只传字符串。比如 Flutter 主动调用鸿蒙侧能力const channel MethodChannel(com.example.harmony.bridge); Futurevoid reportOrder(Order order) async { await channel.invokeMethod(reportOrder, { orderId: order.orderId.toString(), timestamp: DateTime.now().millisecondsSinceEpoch.toString(), }); }鸿蒙侧 ArkTS 代码这样收private onReportOrder(param: Mapstring, Object): void { const orderId: string param.get(orderId) as string; const timestamp: string param.get(timestamp) as string; // 千万不要把 orderId 再转成 number }反过来鸿蒙侧回调给 Flutter 时也要注意不能提前用JSON.parse把大数字解析成 number。如果鸿蒙原生层拿到的是原始 JSON 字符串最好原样把字符串交给 Flutter由JsonUtil.decode来统一解析const rawJson: string {id: 9223372036854775807}; const result new Mapstring, Object(); result.set(payload, rawJson); this.handler.reply(result);Flutter 侧接收时再解析final result await channel.invokeMethod(getPayload); final payload result[payload] as String; final data JsonUtil.decode(payload);这条链路看起来绕实际上是保精度的最短路径。只要任何一环提前变成 number精度就回不来了。重要MethodChannel 方法的参数设计文档里建议明确写上“禁止将 ID 等可能超过 2^53 的字段作为 number 传递”。这条约定比任何代码都管用因为团队成员换了一茬又一茬只有规范能留下。3.5 存量代码多怎么批量替换如果工程规模大存量jsonDecode很多不建议一次性全量替换。可以用一个折中方案在 API 请求层和数据层入口做解析收口UI 层临时展示可以先用原有逻辑等数据流稳定后再逐步切换。具体来说把网络库底层返回的 response.body 统一改成JsonUtil.decode处理业务层拿到的 Map 里大整数字段就已经是 BigInt 或字符串了。那些绕开网络库、自己单独jsonDecode的工具类再单独批量改。另外要注意有些场景存在二次解析第一次已经用JsonUtil.decode把 JSON 变成了 Map第二次又有人把这个 Map 重新jsonEncode成字符串再jsonDecode精度在这一来一回当中就丢了。排查问题时全局搜jsonDecode和jsonEncode看有没有成对出现的位置。4. 验证、测试与问题排查4.1 设计一份可靠的精度测试用例适配做完必须用测试固定住行为。我建议至少覆盖这几个输入9007199254740991也就是2^53 - 1验证安全边界。9223372036854775807int64 最大值验证原生大数。123456789012345678901234567890远超 int64验证 BigInt 兜底。带小数的数字验证普通浮点字段不受影响。一个基础单测可以这样写test(大整数 JSON 解析成 BigInt 且不丢精度, () { final source {id:123456789012345678901234567890}; final decoded JsonUtil.decode(source); expect(decoded[id], isABigInt()); expect(decoded[id].toString(), 123456789012345678901234567890); }); test(普通整数保持普通 int, () { final decoded JsonUtil.decode({count:10}); expect(decoded[count], 10); }); test(BigInt 可以被安全编码, () { final source JsonUtil.encode({id: BigInt.parse(9223372036854775807)}); expect(source, contains(9223372036854775807)); });这几个用例跑通说明解析和序列化两条路都通了。跨端测试再补一个通道用例从 Flutter 传字符串给 ArkTSArkTS 原样返回两侧断言字符串一致。4.2 实战中遇到的典型问题问题一字段明明是int却报type int is not a subtype of type BigInt这个报错通常是因为某个字段在模型层写了final int id;但解析结果已经变成了 BigInt。处理方式不是把 BigInt 强转 int而是把模型字段改成BigInt或String。如果业务上确实需要整数参与计算用 BigInt 计算不要贪图省事用 int。问题二Converting object to an encodable object failed: Instance of BigInt这是 jsonEncode 不认识BigInt。解决方式是用JsonUtil.encode统一编码在toEncodable里把 BigInt 转字符串。问题三通道传过去后鸿蒙侧拿到的是科学计数法比如1.2345678901234568e29这说明 Flutter 侧把数字当成普通 number 传过去了。通道参数里的值一定要先toString()不要在参数对象里留 Dart int。问题四部分接口正常部分接口还是丢精度大概率是某个接口的数据没有走统一解析入口或者经过一次jsonEncode再解析。全局搜一遍jsonDecode看有没有漏网之鱼。4.3 排查速查表现象主要原因处理方向页面 ID 尾部变成 000 或 8默认 jsonDecode 将大数字转为浮点全部走 JsonUtil.decode模型报 int 不是 BigInt 子类型字段类型写死 int解析结果却是 BigInt字段类型改为 BigInt 或 String序列化报 encodable object failedjsonEncode 不识别 BigInt使用 JsonUtil.encode 并配置 toEncodableArkTS 拿到科学计数法数字通道里用 number 传参精度丢失通道参数改为字符串传递JSON 经过两次解析后数字变化第一次解析成 BigInt重新编码再解析时又被默认解析器处理避免二次解析统一收口同一接口在 Web 端和鸿蒙端表现不一致Web 端 JS Number 天然丢精度鸿蒙端通道也可能丢全链路字符串化跨端统一用 BigInt5. 一点个人建议和心得聊完技术细节最后分享几点体会。第一json_bigint只是解决方案的一部分它负责“解析层保留精度”但真正让整条链路稳定下来的是“通道层字符串化”这条约定。我见过太多只改了解析器、没改通道场景结果 Flutter 内部数据没问题一调鸿蒙原生能力就原形毕露。第二跟服务端约定数据契约是最好的兜底。凡是可能超过2^53的字段直接让后端返回字符串不要返回数字。如果后端暂时改不了客户端用 BigInt 兜住解析层的精度跨端用字符串保证传输不丢。第三BigInt 在日常开发里有一些隐性影响。比如排序、比较、显示时BigInt 和 int、String 的表现都不一样。项目里最好有一个标准工具类专门处理 BigInt 的转换和比较不要在每个业务页里各自实现。最后给一个小技巧在集成测试里加一个“遍历所有模型”的用例构造一份包含所有大整数字段的 JSON 样例依次进模型、出模型、再进模型断言字符串一致。这个用例看着笨但每次升级依赖、换 Flutter 版本、调鸿蒙 SDK 时都能第一时间告诉你精度链路有没有被改坏。数据精度这事大多数时候不出问题一出问题就特别难查提前把它锁在测试里是最划算的投资。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑