☰
Flutter鸿蒙适配:json_bigint解决JSON大整数精度丢失实战
2026/10/5 7:36:45 网站建设 项目流程

Flutter 项目迁到鸿蒙之后,我最先踩爆的坑不是组件兼容,也不是 PlatformView,而是一个我一直以为绝不会出事的 JSON 大整数解析问题。后台接口文档里写着"订单号请用字符串接收",结果前端同学图省事直接拿jsonDecode一把梭,鸿蒙测试机上订单号后四位全部变成 0。你大概也听过json_bigint这个三方库,它专门解决 JSON 解析时大整数被转成 double、导致尾数丢失的问题。这篇文章就是我把json_bigint完整适配到鸿蒙 Flutter 工程的实战记录,包括原理、接入步骤、构建期和运行期的坑,以及一套可以照抄的回归测试思路,给正在做鸿蒙化适配的同学一个参考。

1. 一场"订单对不上账"的精度事故

1.1 事故现场:20 位订单号变成了科学计数法

事情是这样的。我们有个后台接口返回的订单号是 20 位的数字字符串,类似73262411820010618880。在 Android 和 iOS 的 Flutter 版本上一直没问题,因为服务端其实把订单号放在两个字段里:一个是字符串类型的orderNoStr,另一个是数字类型的orderNoNum,前者给展示用,后者给对账用。结果对接鸿蒙的时候,有同事图方便,直接用了orderNoNum这个字段去展示。

一上鸿蒙测试机就乱了。订单列表里有的订单号后四位变成 0000,有的变成科学计数法,最离谱的是一个订单号直接和另一个重复了。第一反应是服务端返回错了、或者鸿蒙的网络库有问题,查了半天日志才发现,问题出在最基础的 JSON 解析环节——dart:convert自带的jsonDecode在解析超长整数字面量时,并没有按我们以为的"完整保留整数"来处理。

1.2 根因定位:不是接口坏了,是解析策略的问题

JSON 标准里数字类型只有一种,没有 int、long、bigint 的区分。Dart 默认的jsonDecode遇到一个整数,会先尝试用 64 位 int 接收;一旦超出 int64 范围,或者数字本身带小数点,就会退化成 double。而 double 对"精确整数"是有上限的,超过2^53 - 1(也就是 9007199254740991)的整数,double 就无法一一精确表示,只能做舍入。

我们那个 20 位订单号73262411820010618880远超 2 的 53 次方,更远超 int64 上限。默认解析器要么把它当成一个 double,要么在极端情况下直接抛异常。无论哪种结果,订单号都已经失真。这不是鸿蒙独有的问题,是所有使用标准 JSON 解析逻辑的运行时都会遇到的,只是以前 Android 和 iOS 版本里没人踩到,鸿蒙适配时换人接手、代码路径一变,就炸出来了。

1.3 为什么鸿蒙端要单独把这件事拎出来

你可能觉得"那我直接改用字符串字段不就行了"。但现实是,很多接口的字段类型不是你前端能控制的,尤其是对接第三方、硬件设备、或者历史遗留服务端,数字大整数字段到处都是。更隐蔽的是另一种情况:数据经过 ArkTS/JS 运行时中转,或者从 WebView、平台通道传过来时,整数已经被转成 double 了,等到了 Dart 侧你看到的就已经是脏数据。

所以鸿蒙化适配这件事,不是简单把jsonDecode换成json_bigint就完了,而是要在一开始就明确:凡是可能超过安全精度的整数,必须在解析层统一接管,而不是散落在各个业务页面碰运气。

2. 默认 jsonDecode 与 json_bigint 的解析差异

2.1 dart:convert 的整数解析逻辑

要理解 json_bigint 的价值,先得知道默认解析器具体做了什么。Dart 的JsonDecoder在扫描到一个数字 token 时,大致会走这样一套判断:

  • 数字不包含小数点、也不包含指数部分时,优先按整数解析,看能不能装进 64 位 int;
  • 如果整数超出 64 位范围,默认处理会尝试转成 double,或者直接判定无法解析;
  • 数字带小数或指数时,一律按 double 解析。

这套策略本身没问题,问题在于"超出 int64 范围的整数"在真实业务里并不少见。你以为9223372036854775808这种数字只是理论值,实际上雪花 ID、设备序列号、支付流水号,随便一个都可能撞上。到了 Flutter Web 上更麻烦,连 2 的 53 次方都能丢精度。

2.2 json_bigint 做了什么

json_bigint 的核心思路,是把"数字 token 如何解析"的控制权从默认解析器手里拿回来。它重写了数字解析逻辑:仍然优先尝试 int,如果 int 装不下,就回退到 Dart 内置的BigInt,而不是直接放弃或转成 double。BigInt 是任意精度的,理论上你有多大的整数,它就能装多大。

具体到使用上,核心对象通常是BigIntJsonDecoder。不同版本 API 略有差异,以你在 pub 上拉到的版本为准,但大体是这样用的:

import 'package:json_bigint/json_bigint.dart'; void main() { const raw = ''' { "orderId": 73262411820010618880, "amount": 12.5 } '''; final decoded = BigIntJsonDecoder().decode(raw) as Map<String, dynamic>; print(decoded['orderId']); // 73262411820010618880,BigInt 类型 print(decoded['orderId'] is BigInt); // true print(decoded['amount']); // 12.5,小数保持 double }

注意一点:小数部分它不会动,仍然按 double 处理,因为小数本来就不可能用整数精确表示;它只管整数 token 的边界问题。

2.3 几种大整数处理方案的取舍

我在项目里实际比较过四种方案,这里直接列个对比给你:

方案改动量风险适用场景
默认 jsonDecode + 字符串字段兜底小字段多时容易漏,历史接口不可控接口自己说了算、改动不频繁
手动递归解析 + int.parse/BigInt.parse中嵌套结构容易漏,代码侵入性强只有个别字段有问题
服务端全部改成字符串返回大所有端都要配合改,联调成本高后端你能完全掌控
json_bigint 统一解码器小需要统一入口,类型上要注意 BigInt 赋值多端跨平台、大整数字段较多

作为长期维护的项目,我最终选了 json_bigint。原因很简单:它把精度问题收敛在了解析层,业务代码拿到的是一个完整的、语义明确的值,而不是业务层到处做int.parse或者toString补救。

2.4 一个最容易误导人的对比实验

如果你在 Android 模拟器上跑默认jsonDecode('{"id": 9007199254740993}'),大概率会得到一个 int 值 9007199254740993,看起来没丢精度。于是很容易得出"这个库根本没用"的结论。但同样一段代码,跑在 Flutter Web 上,或者数据经过 JS 运行时中转,结果就变成了 9007199254740992。同一个 App,两个平台行为不一致,这才是最坑的地方。

鸿蒙生态下面临的也是这种环境差异问题。所以我的建议是:**不管当前测试机跑起来对不对,只要项目有跨端诉求、或者字段可能超过 int64,就统一上 json_bigint,把不确定性消灭在源头。**不要等线上真出了精度事故再回来补。

3. 鸿蒙化接入前要确认的四件事

3.1 Flutter SDK 分支与 Dart 版本

鸿蒙 Flutter 工程通常基于 OpenHarmony 社区维护的 Flutter SDK 分支,自带 Dart SDK。这里第一件事就是确认当前分支的 Dart 版本支持不支持你想要的 json_bigint 版本。

json_bigint 本身是一个纯 Dart 包,理论上只依赖 Dart SDK 的基础能力,但不同版本对 SDK 下限要求不同。如果拉取的是最新版,而鸿蒙 Flutter 分支内置的 Dart 版本偏老,编译时会直接报类似The current Dart SDK version is 2.x, but json_bigint requires >= 2.18的错误。解决办法是去 pub.dev 查一下你所用版本对应的 SDK 约束,手动锁一个兼容版本,而不是无脑升级。

3.2 pubspec.yaml 里的依赖锁定建议

接入的时候不要写通配符^让依赖随便浮动,建议锁定到具体版本。我踩过这个坑:json_bigint 升级一个小版本后,默认解析行为发生变化,结果整个订单模块的回归测试跑了一片红。

dependencies: flutter: sdk: flutter json_bigint: 5.0.0 # 固定版本,不要用 ^

鸿蒙构建链目前不比 Android 成熟,依赖解析偶尔会有缓存问题,固定版本至少能让你在排查时少一个变量。

3.3 先跑最小工程,不要直接改大项目

这是我最想强调的一点。鸿蒙 Flutter 适配期间,工程里可能同时存在十几个插件不兼容、构建脚本报错、签名配置缺失等各种问题。如果你直接在大项目里引入 json_bigint 然后发现编译不过,你根本分不清是 json_bigint 的问题还是别的插件的问题。

正确做法是新建一个最小 Flutter 工程,只加 json_bigint 一个依赖,写一段包含超长整数和小数的 JSON,跑通解码、编码、展示三个动作。这一关过了,再回大工程接。前后不过二十分钟,能帮你省半天排查时间。

3.4 检查数据传递链路中有没有被提前转成 double

json_bigint 只能在"原始 JSON 文本"这一层保护精度。如果你的数据不是从网络直接拿字符串解析,而是先从原生侧通过 MethodChannel 传了一个已经解析好的Map,那么对不起,大整数在原生侧就已经被揉成 double 了,json_bigint 看到的是残废数据,救不回来。

所以接入之前,建议把所有大整数字段的数据链路拉一遍:网络请求拿到的是不是String?有没有经过 JSON 字符串拼接?有没有在 Dart 侧先jsonDecode过一次再二次解析?如果链路已经脏了,先改链路,再谈解析策略。这个检查比写代码重要。

4. 接入过程中最容易卡住的构建与运行问题

4.1 DevEco 同步 Flutter 依赖的顺序问题

现在鸿蒙 Flutter 工程的常规打开方式是:用 DevEco Studio 打开工程根目录下的ohos目录,然后等待工程同步。问题在于,如果你刚在pubspec.yaml里加了 json_bigint,还没执行flutter pub get就直接去 DevEco 里 Sync,大概率会出现依赖找不到的情况。

我遇到的报错信息五花八门,有的直接提示找不到包,有的同步成功但运行时类不存在。后来摸索出的稳定顺序是:

  1. 在工程根目录先执行flutter pub get;
  2. 确认.dart_tool/package_config.json里能看到 json_bigint 的路径;
  3. 确认ohos/.flutter-plugins-dependencies这个文件被重新生成;
  4. 再打开 DevEco 做同步和构建。

pub get之所以必须,是因为鸿蒙的依赖解析会去读 Flutter 生成的中间文件,而不是自己重新解析 pubspec。顺序反了,构建系统就找不到这个包。

4.2 纯 Dart 包被误判成原生插件的问题

json_bigint 是纯 Dart 包,本来不应该参与原生插件的注册流程。但如果你和我一样,图省事把依赖指向了一个 Git fork 版本,就很容易踩到一个隐蔽的坑:fork 仓库里如果带了原生目录(比如android/、ios/、甚至ohos/),Flutter 工具链会把它当成平台插件,在鸿蒙侧触发原生编译。

结果就是 json_bigint 一个纯解析库,莫名其妙报出一些 CMake、NDK 或者 hvigor 的错误。我当时排查了很久,最后把依赖从 Git fork 换回 pub 官方源,问题立刻消失。经验就是:**纯逻辑库尽量用官方源,不要自作聪明 fork。**除非你确实改了它的源码,否则别让构建系统产生多余联想。

4.3 命令行构建定位问题

有时候在 DevEco 界面里构建报错,日志被 UI 吞掉一半,很难定位。我建议遇到问题直接用命令行构建,例如在工程根目录执行:

flutter build hap --debug

不同版本的分支命令可能略有差异,以你使用的 SDK 分支 README 为准,但思路是一样的:命令行会把 Dart 编译、资源打包、hvigor 构建这几个环节的完整日志打出来,你可以清楚看到问题发生在哪一段。

如果命令行构建能过、DevEco 构建不能过,那基本可以判断是 IDE 的缓存或者同步问题,先flutter clean再重来。如果命令行在 Dart 编译阶段就报 json_bigint 相关的错误,那就是依赖版本和 Dart SDK 兼容性问题,优先查版本约束。

4.4 运行时最容易遇到的两个异常

构建过了只是开始,运行时踩坑才真正磨人。我在真机上遇到过两个非常典型的异常,这里一起说。

第一个是JsonUnsupportedObjectError。场景是你的数据模型里有 BigInt 类型的字段,往 Model 里塞没问题,但当你反过来把整个对象序列化成 JSON 字符串上报时,默认jsonEncode根本不认识 BigInt,直接崩溃。解决方式是用 json_bigint 配套的 encoder 来序列化,或者提前把 BigInt 字段转成字符串再上报。

第二个是运行时报错日志,常见格式是:

E/flutter (31173): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] unhandled exception

很多人看到这种日志就慌了,其实展开异常堆栈发现是type 'BigInt' is not a subtype of type 'int?'。原因很简单:你的 Model 字段声明成了int,但 json_bigint 解析出来的是BigInt,类型对不上。要么把字段类型改成BigInt,要么在解析后显式做一次转换,别指望它自动降级。

5. 数据精度回归测试怎么设计

5.1 边界值用例表

接入 json_bigint 之后,我针对精度问题专门列了一张回归测试表,每次发版前跑一遍。不需要什么复杂框架,就是一个单测文件加一组断言。关键是这些边界值一定要覆盖:

输入值类型说明期望行为
90071992547409912^53 - 1,double 可精确表示的极限正常解析,数值不变
90071992547409932^53 + 1,double 必然丢精度必须保留原始整数,不能变 9007199254740992
9223372036854775807int64 最大值正常解析,数值不变
9223372036854775808int64 最大值 + 1,int 装不下解析为 BigInt,且数值完全不变
-9223372036854775809负数超出 int64 范围解析为 BigInt,符号保留
7326241182001061888020 位订单号纯文本显示时逐位一致
123456789.123正常小数仍按 double 处理,不影响整数逻辑

建议实际断言用字符串对比,也就是把解析结果.toString()之后和原始输入逐字符比较。只比较数字类型或者==可能因为隐式转换掩盖问题。

5.2 统一解析入口,而不是到处写 jsonDecode

有些项目是在几十个文件里直接jsonDecode(raw),接入 json_bigint 以后把每个调用点都替换掉,这种改法一是容易漏,二是后续想调整解析策略还得再扫一遍文件。我建议在项目的core/network或utils里包一个统一入口:

import 'package:json_bigint/json_bigint.dart'; class SafeJson { static final _decoder = BigIntJsonDecoder(); static final _encoder = BigIntJsonEncoder(); static Map<String, dynamic> decodeMap(String source) { final result = _decoder.decode(source); return Map<String, dynamic>.from(result as Map); } static String encode(Object? value) => _encoder.encode(value); }

然后所有网络层和本地存储层统一调SafeJson.decodeMap,而不是直接使用dart:convert。业务代码不必关心底层用的是什么解析库,未来哪怕换掉 json_bigint,也只需要改这一个文件。

5.3 与 json_serializable 生成代码的兼容性

如果你的项目用了json_serializable自动生成fromJson/toJson,要特别注意生成出来的代码里,字段类型是int还是BigInt。默认生成器不认识 BigInt,如果你把 Model 字段声明成BigInt,json_serializable 是可以支持的,因为它在生成时会对BigInt做特殊处理,把值toString()到 JSON 里。

但如果你的大整数字段在 JSON 原始文本里是数字字面量、而不是字符串,生成代码里的json['orderId'] as BigInt仍然可能失败,因为 json_serializable 内部默认用的是jsonDecode而不是你的SafeJson。这种情况我会选择不在 Model 层依赖生成器,而是手动写这个字段的转换逻辑,或者干脆把大整数字段统一在 DTO 层定义成BigInt,解析入口统一走SafeJson。宁可 DTO 层多写几行,也不要在生成代码的边界上赌行为一致。

6. 上线之后:BigInt 数据长期维护的一点点建议

6.1 展示与传输要分离

BigInt 可以直接存在内存里做运算,但一旦要展示到 UI 或者拼进 JSON 上报,就必须显式转换。UI 层一定不要直接Text('$bigIntValue')之外再做隐式类型转换,否则某些组件内部会把它当 double 处理。

上报接口时也建议明确策略:能转字符串的字段转字符串,不能转的就用 json_bigint 的 encoder 序列化。最忌讳的是同一个大整数字段,在 A 接口用字符串上报,在 B 接口用数字上报,日志排查时脑袋都要炸。

6.2 版本升级时的回归清单

json_bigint 和鸿蒙 Flutter SDK 都在快速演进,每次升级要过的检查项我放在这里:

  • flutter pub get后确认 json_bigint 实际解析到的版本;
  • 跑一遍上面那张边界值用例表,重点看 2^53 + 1 和 int64 溢出这两个值;
  • 确认SafeJson统一入口的 decoder / encoder 构造参数没有变化;
  • 确认 Dart SDK 约束仍然满足,不满足就锁旧版本;
  • 在鸿蒙真机上跑一遍大整数字段的列表页、详情页、上报逻辑三个主链路。

这套清单看起来简单,但每次版本升级都能捞出一两个问题,尤其是 encoder 行为的变化,最容易在离线上报链路里冒出来。

6.3 我持续在用的一个封装模板

最后分享一个我现在一直在用的模板,它解决了 BigInt 序列化和反序列化不对称的问题:

class OrderModel { final BigInt orderId; OrderModel({required this.orderId}); factory OrderModel.fromJson(String source) { final map = SafeJson.decodeMap(source); return OrderModel( orderId: map['orderId'] is BigInt ? map['orderId'] as BigInt : BigInt.from(map['orderId'] as int), ); } Map<String, dynamic> toJson() { return { 'orderId': orderId.toString(), }; } }

注意fromJson里做了一个兜底:如果解析结果是 BigInt 就用,如果不是(比如某些老接口已经被转成 int 了)就用BigInt.from包一层,避免类型断言崩溃。toJson里统一转字符串,上报安全,下游也愿意接。这个模板可能不是最优解,但它是目前我在鸿蒙端踩完一圈坑之后最稳的方案。如果你的项目里大整数字段不止一两个,建议把这种转换逻辑收敛到一个父类或者 mixin 里,别每个 Model 都复制一遍。

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

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

立即咨询