
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 MapString, dynamic; print(decoded[orderId]); // 73262411820010618880BigInt 类型 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 检查数据传递链路中有没有被提前转成 doublejson_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大概率会出现依赖找不到的情况。我遇到的报错信息五花八门有的直接提示找不到包有的同步成功但运行时类不存在。后来摸索出的稳定顺序是在工程根目录先执行flutter pub get确认.dart_tool/package_config.json里能看到 json_bigint 的路径确认ohos/.flutter-plugins-dependencies这个文件被重新生成再打开 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 - 1double 可精确表示的极限正常解析数值不变90071992547409932^53 1double 必然丢精度必须保留原始整数不能变 90071992547409929223372036854775807int64 最大值正常解析数值不变9223372036854775808int64 最大值 1int 装不下解析为 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 MapString, dynamic decodeMap(String source) { final result _decoder.decode(source); return MapString, 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 字段声明成BigIntjson_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), ); } MapString, dynamic toJson() { return { orderId: orderId.toString(), }; } }注意fromJson里做了一个兜底如果解析结果是 BigInt 就用如果不是比如某些老接口已经被转成 int 了就用BigInt.from包一层避免类型断言崩溃。toJson里统一转字符串上报安全下游也愿意接。这个模板可能不是最优解但它是目前我在鸿蒙端踩完一圈坑之后最稳的方案。如果你的项目里大整数字段不止一两个建议把这种转换逻辑收敛到一个父类或者 mixin 里别每个 Model 都复制一遍。