JSON 转 Dart Model 插件鸿蒙适配手记:废弃 ext_storage 无 OHOS 实现只能 vendor 自补
开发工具: 华为云码道
DevEco Studio:
26.0.0(26.0.0.851)原始仓库: zeeshux7860/auto_json_to_dart_converter(GitHub)
本文配套仓库: oh-flutter/auto_json_to_dart_converter(AtomGit)
auto_json_to_dart_converter 做的事很直白:喂它一段 JSON,还你一组能在项目里直接用的 Dart Model 类——带 fromJson、带 toJson、字段名按 snake_case → camelCase 转换、嵌套对象自动拆成独立的类。它还会顺手把生成结果写进设备的 json_to_dart/ 目录。
本文以 auto_json_to_dart_converter 1.0.0 为例。与前面几篇「纯 Dart 库」不同,这篇文章的适配工作量明显更大,原因有两个:它依赖一个已废弃的存储插件 ext_storage,而这个插件没有鸿蒙实现;它是一份 2020 年的、非空安全(pre-null-safety)代码,在 Dart 3 下根本编译不过。两件事都得在适配时解决。

插件目前支持 ohos 平台,源码位于 AtomGit 配套仓库。文中的代码对应 TAG 1.0.0-ohos-1.0.0(适配主体为提交 2ac0f48)。

一、插件简介与适配目标
1.1 四层结构,四层都碰过
| 层 | 文件 | 职责 | 本次改动 |
|---|---|---|---|
| 入口 | lib/auto_json_to_dart_converter.dart(50 行) | Get / Post 两个「拉远程 JSON 并生成」的类 | +2 / −2,Uri.parse 与 headers 类型 |
| 生成器 | lib/convert/model_generator.dart(161 行) | ModelGenerator,把 JSON 跑成 Dart 源码 | 空安全语法(Hint?、TypeDefinition、!) |
| 语法树 | lib/convert/syntax.dart(328 行) | ClassDefinition / TypeDefinition / Warning | 空安全语法(构造器可空参数、getTypeName) |
| 类型推断 | lib/convert/helpers.dart(284 行) | 类型合并、camelCase、歧义检测 | 空安全语法(Map 泛型、String?) |
四层的改动都只涉及语法,没有一个字动了业务逻辑——这一点可以逐行对比上游客确认。
对外 API 只有两个类,签名一模一样:
class AutoJsonToDartConverterGetMethod {
AutoJsonToDartConverterGetMethod(this.url, this.modelName, this.header);
Future<String> get getmethod async { ... }
}
class AutoJsonToDartConverterPostMethod {
AutoJsonToDartConverterPostMethod(this.url, this.modelName, this.body, this.encoding, this.headers);
Future<String> get postMethod async { ... }
}
用法就是读一个 getter:await AutoJsonToDartConverterGetMethod(url, 'User', {}).getmethod。
1.2 它到底做了什么
getmethod 的完整实现只有 9 行,但每一行都牵出了后面的适配话题:
Future<String> get getmethod async {
var res = await http.get(Uri.parse(url), headers: header?.cast<String, String>());
final classGenerator = new ModelGenerator(modelName);
DartCode dartCode = classGenerator.generateDartClasses(res.body);
print(dartCode.warnings.length);
var path = await ExtStorage.getExternalStorageDirectory();
await Directory(path + '/json_to_dart').create(recursive: true);
File(path + '/json_to_dart/' + modelName.toLowerCase() + '.dart')
.writeAsStringSync(dartCode.result);
return dartCode.result;
}
按顺序看这几行:
http.get直接把res.body交给生成器——没有状态码判断,404 页面、登录跳转 HTML 都会被当成 JSON 解析,然后在decodeJSON里抛异常。这个写法在上游就有,不是适配引入的。generateDartClasses=generateUnsafeDart+dart_style格式化。名字里的unsafe说的是「生成的 Dart 可能语法不合法」,所以后面必须过一遍DartFormatter。ExtStorage.getExternalStorageDirectory()是唯一的平台通道调用,也是鸿蒙适配的核心。- 文件名是
modelName.toLowerCase(),RemoteModel会变成remotemodel.dart——全小写,没有下划线分隔。
1.3 适配前的编译错误
这份代码的 pubspec.yaml 写着 sdk: ">=2.1.0 <3.0.0",依赖是 json_ast ^1.0.5、convert ^2.0.1、dart_style ^1.0.10、http ^0.12.0+4、ext_storage ^1.0.2。在 Flutter 3.47 / Dart 3.13 的环境下,pub get 第一步就会拒绝解析。所以这次适配不是「加个 ohos 目录」,而是先把这份 2020 年的代码搬进空安全时代,再补平台实现。
改造后的依赖表:
dependencies:
json_ast:
path: third_party/json_ast # 本地 vendor
convert: "^3.0.0"
dart_style: "^2.0.0"
http: ^0.13.0
ext_storage:
path: third_party/ext_storage_ohos # 本地 vendor + ohos 实现
两个 path 依赖都不是图省事。json_ast 从 1.0.5 起就没再更新过,它在 test/ 里放了带 : 之类 Windows 非法字符的 fixture 文件,pub get 解压时直接报错;ext_storage 上游根本没有 ohos 目录,也没法在应用里通过 dependency_overrides 指定本地路径之外的方式补平台。两处都只能 vendor 进来。
二、环境准备
环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。
完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:
flutter --version
flutter doctor -v
hdc list targets


本机实际使用的工具链与 SDK 配置如下:
| 项目 | 版本或配置 | 用途 |
|---|---|---|
| Flutter OHOS SDK | 3.47.7-ohos-1.0.0 | Flutter 编译与 OHOS 平台工具链 |
| Flutter 分支 | oh-3.47-dev | 对应开发分支 |
| Dart SDK | 3.13.3 | Dart 语言与包管理环境 |
| DevEco Studio | 26.0.0(26.0.0.851) | 工程 Sync、签名配置与 hvigor 构建 |
| 测试设备 | TLR-AL00(nova 14),OpenHarmony 6.1.1.120 | 真机安装与运行 |
| 设备 API | 24 | const.ohos.apiversion 实测值 |
compatibleSdkVersion | 6.1.1(24) | 工程声明的最低兼容版本 |
targetSdkVersion | 6.1.1(24) | 应用面向的行为版本 |
| 插件版本 | 1.0.0+ohos-1.0.0 | pubspec.yaml 中的包版本 |
| 原生语言 | ArkTS | HarmonyOS 插件实现 |
| 插件产物 | HAR | 被应用 entry 模块依赖 |
2.1 工程里的 SDK 版本为什么写 6.1.1(24)
example/ohos/build-profile.json5 的 product 配置是:
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "6.1.1(24)",
"targetSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS"
}

本机 DevEco Studio 自带的是 API 26 SDK,而测试机是 API 24。按模板默认的 API 26 出包,装到这台设备上报 9568297 older sdk version。把 compatibleSdkVersion 与 targetSdkVersion 一起降到 6.1.1(24) 与真机对齐后,安装正常。
需要注意,降 compatibleSdkVersion 只影响「能不能装」和「按哪个行为版本运行」,编译仍使用本机 DevEco 的 API 26 SDK。这是 hvigor 的默认行为:工程不写 compileSdkVersion 时按当前安装的 SDK 编译。
2.2 这个库额外需要的一件东西
auto_json_to_dart_converter 的 getmethod / postMethod 会发 HTTP 请求,所以宿主应用的 entry/module.json5 必须声明网络权限:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
漏掉这条,远程 GET 会在运行时以 SocketException 失败,而错误信息停在 Dart 层,看不出是权限问题。
三、从源码仓库开始准备适配工程
3.1 将上游源码同步到 AtomGit
适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。auto_json_to_dart_converter 的信息是:
| 项目 | 值 |
|---|---|
| 上游地址 | https://github.com/zeeshux7860/auto_json_to_dart_converter |
| Dart 包名 | auto_json_to_dart_converter |
| 版本 | 1.0.0(2020 年发布,pub.dev 上已长期未更新) |
| 许可证 | MIT |
| 适配起点提交 | df4274a(first commit) |
在 AtomGit 网页的新建仓库流程中,选择从已有仓库导入,填写上游 Git 地址,选择 oh-flutter 组织、设置目标仓库名 auto_json_to_dart_converter 后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。
auto_json_to_dart_converter 现可从 AtomGit 获取。下面使用 AtomGit 地址拉取代码;需要提交修改时,使用自己有写权限的仓库或 Fork。
3.2 将代码拉取到宿主机
在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:
git clone https://atomgit.com/oh-flutter/auto_json_to_dart_converter.git
cd auto_json_to_dart_converter
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD
git clone 会创建 auto_json_to_dart_converter/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yaml、lib/、ohos/、third_party/ 和 example/。这个库的仓库名与 Dart 包名一致,不存在仓库名与包名不同、需要 --project-name 纠正的情况。

图 1:拉取仓库后 ls 的结果,根目录已包含 ohos/、third_party/、example/ 与 OpenHarmony 说明文件。
需要使用与本文相同的代码版本时,在没有未提交修改的仓库中切换到以下提交:
git switch --detach 2ac0f48
3.3 分支与版本标记
仓库当前只有一个分支 master,适配提交直接落在 master 上:

图 2:master 与 origin/master 同步,TAG 为 1.0.0-ohos-1.0.0。
规范化流程建议按 feat/ohos_库名称_版本号 建分支,本例对应的命令是:
git switch -c feat/ohos_auto_json_to_dart_converter_1.0.0
git branch --show-current
本次提交没有走这条路径,是直接在 master 上提交并推送的。两种做法都能用,区别在于评审:走分支可以在 AtomGit 上发起合并请求,评审通过再进 master;直接提交 master 则省掉这一步。接收仓库有分支规范时,以后者为准。
TAG 1.0.0-ohos-1.0.0 打在 master 最新提交上,适配主体是 2ac0f48,README.OpenHarmony_CN.md 的 TAG 表因此可以直接引用它。远端只有一个 origin,没有配置 upstream;要跟踪上游 GitHub,得自己补一条:
git remote add upstream https://github.com/zeeshux7860/auto_json_to_dart_converter.git
git fetch upstream
3.4 自动补全 OHOS 适配结构
在插件根目录执行结构补全。以下命令适用于尚无 ohos/ 目录的既有 Flutter 插件:
flutter create . --template=plugin --platforms=ohos --org zeeshux7860
git status --short
git diff -- pubspec.yaml
--template=plugin指定插件模板;--platforms=ohos只补ohos,不动已有平台;--org zeeshux7860与上游 Android 侧的zeeshux7860.auto_json_to_dart_converter包名保持一致;- 最后的
.表示在当前插件目录补全工程,不是另建一层。

图 3:pubspec.yaml 的版本号、SDK 约束、依赖与插件声明四处同时变化。
图 3 里有三处值得单独说,它们不是 flutter create 生成的,是手工改的:
第一处,版本号从 1.0.0 变成 1.0.0+ohos-1.0.0。 这是 + 后面的 build 后缀,语义上不改变主版本,鸿蒙适配统一用这个格式。
第二处,plugin 段从旧的 androidPackage 写法换成 platforms 写法。 上游用的是 Flutter 1.x 时代的字段:
plugin:
androidPackage: zeeshux7860.auto_json_to_dart_converter
pluginClass: AutoJsonToDartConverterPlugin
这种写法在 Flutter 2.0 之后被废弃,flutter pub get 会直接报 Invalid plugin specification。必须改成:
plugin:
platforms:
android:
package: zeeshux7860.auto_json_to_dart_converter
pluginClass: AutoJsonToDartConverterPlugin
ios:
pluginClass: AutoJsonToDartConverterPlugin
ohos:
pluginClass: AutoJsonToDartConverterPlugin
pluginClass 必须与 ArkTS 侧 getUniqueClassName() 的返回值逐字一致,否则运行时插件注册会静默失败——第八节的日志里能看到这两个名字。
第三处,sdk 约束从 >=2.1.0 <3.0.0 改成 >=2.12.0 <4.0.0,并新增 flutter: ">=1.20.0"。 <3.0.0 这个上界在 Dart 3 下不可用,必须放开到 <4.0.0;下界提到 2.12.0 是因为整份代码已经改成空安全了。
如果生成后 example/ohos/ 仍不存在,进入已有示例应用补全平台:
cd example
flutter create --platforms=ohos .
cd ..
3.5 适配后的项目目录
auto_json_to_dart_converter/
├── lib/
│ ├── auto_json_to_dart_converter.dart # Get/Post 两个入口(50 行)
│ └── convert/
│ ├── model_generator.dart # ModelGenerator(161 行)
│ ├── syntax.dart # 类/字段语法树(328 行)
│ ├── helpers.dart # 类型推断与合并(284 行)
│ ├── warning.dart # 警告类型(13 行)
│ └── json_to_dart.dart # 一行 export
├── ohos/
│ ├── index.ets
│ ├── oh-package.json5
│ ├── build-profile.json5
│ ├── hvigorfile.ts
│ └── src/main/
│ ├── module.json5
│ └── ets/components/plugin/AutoJsonToDartConverterPlugin.ets
├── third_party/
│ ├── json_ast/ # 本地 vendor(剔除 Windows 非法文件名)
│ └── ext_storage_ohos/
│ ├── lib/ext_storage.dart # Dart API 与原插件一致
│ └── ohos/src/main/ets/components/plugin/ExtStoragePlugin.ets
├── example/
│ ├── lib/main.dart # 三种输入源 + 双 Tab(354 行)
│ └── ohos/ # 宿主工程(AppScope + entry)
├── test/auto_json_to_dart_converter_test.dart
├── docs/ohos-device-test-report.md
├── README.OpenHarmony_CN.md
├── README.OpenHarmony.md
├── CHANGELOG.OpenHarmony.md
└── pubspec.yaml

图 4:适配后的关键目录。third_party/ 下是两个本地 vendor 的依赖,各自带一个 ohos 实现或裁剪。
| 文件 | 主要职责 | 本次是否改动 |
|---|---|---|
lib/auto_json_to_dart_converter.dart | Get / Post 两个入口 | 改,空安全 + http 0.13 + 类型标注 |
lib/convert/*.dart | 生成器与类型推断 | 改,空安全语法(TypeDefinition?、Hint?、! 等) |
ohos/.../AutoJsonToDartConverterPlugin.ets | 注册 auto_json_to_dart_converter 通道 | 新增,46 行 |
third_party/ext_storage_ohos/ | ext_storage 的鸿蒙实现 | 新增,Dart 侧 1 个文件 + ArkTS 侧 60 行 |
third_party/json_ast/ | 本地 vendor | 新增,剔除 test/fixtures |
example/lib/main.dart | 演示界面 | 由 48 行扩写为 354 行 |
test/auto_json_to_dart_converter_test.dart | 生成器单元测试 | 新增,20 行 2 个用例(上游 test/ 为空目录) |
example/ohos/ | 宿主应用工程 | 新增 |
docs/ohos-device-test-report.md | 适配方的真机测试报告 | 新增 |
适配提交 2ac0f48 的统计为 88 files changed, 3483 insertions(+), 634 deletions(-)。88 个文件里超过一半是 third_party/json_ast 的源码拷贝,真正手写的代码不到 800 行。
四、Dart 接口与通道分析
4.1 两条通道,一条业务一条存储
和前面几个纯 Dart 库不同,这个库运行时真的会走平台通道,而且一走就是两条:
| 通道 | Dart 侧调用点 | ArkTS 侧实现 | 运行时是否被调用 |
|---|---|---|---|
auto_json_to_dart_converter | 无 | AutoJsonToDartConverterPlugin.ets | 否 |
ext_storage | ExtStorage.getExternalStorageDirectory() | ExtStoragePlugin.ets | 是 |
第一条通道是个「有注册、无流量」的壳。Dart 侧从头到尾没有 MethodChannel('auto_json_to_dart_converter'),也没有任何 invokeMethod。它存在的唯一理由是 pubspec.yaml 声明了 ohos 平台插件类,引擎启动时会实例化并注册它。
第二条才是真正的业务通道。ext_storage 这个包在 pub.dev 上已被标记为废弃,也没有 ohos 实现,所以适配时必须自己补。
4.2 为什么 ext_storage 要映射到 filesDir
上游 ext_storage 在 Android 上返回的是 Environment.getExternalStorageDirectory(),即 /storage/emulated/0——一个所有应用都能读写的公共根目录。鸿蒙上没有语义等价的目录:/storage/media/100/local/files 之类的公共路径需要受限权限(ohos.permission.READ_WRITE_DOWNLOAD_DIRECTORY 等),而且面向的是「文件管理器」类应用,不是普通应用的功能目录。
适配采用的映射是:
| Android | OHOS | 理由 |
|---|---|---|
Environment.getExternalStorageDirectory() | context.filesDir | 应用沙箱内唯一必然可写、且不需要权限的目录 |
Environment.getExternalStoragePublicDirectory(type) | context.filesDir | 各 type(Music / Pictures / Download…)在 OHOS 上没有公共对应物,统一落到同一目录 |
filesDir 在设备上的实际路径是:
/data/app/el2/100/base/<bundleName>/files
对应用而言这就是「自己的存储根目录」,卸载即清空。适配后的 Dart 侧 API 与上游完全一致,业务代码不用改:
class ExtStorage {
static const MethodChannel _channel = MethodChannel('ext_storage');
static Future<String> getExternalStorageDirectory() async {
final String? path = await _channel.invokeMethod('getStorageDirectory');
return path ?? '';
}
static Future<String> getExternalStoragePublicDirectory(String type) async {
final String? path =
await _channel.invokeMethod('getStoragePublicDirectory', {'type': type});
return path ?? '';
}
}
这个映射有一个必须说清楚的后果:Android/iOS 上生成的文件能在系统文件管理器里看到,鸿蒙上不能。文件在应用沙箱里,只有本应用(以及
hdc file recv)能访问。如果业务需要「用户能找到这个文件」,得另外走@ohos.file.picker让用户主动选择保存位置,或者在生成后调起分享。这是平台差异,不是适配缺陷。
4.3 getmethod 里的空安全改造
入口文件相对上游只有 2 行变动,但都值得说:
- var res = await http.get(url,headers: header);
+ var res = await http.get(Uri.parse(url),headers: header?.cast<String,String>());
...
- var res = await http.post(url,body: body,encoding: encoding,headers: headers);
+ var res = await http.post(Uri.parse(url),body: body,encoding: encoding,headers: headers?.cast<String,String>());
Uri.parse(url) 是这次唯一带行为影响的改动。 http 0.12 的 get / post 接受 String,0.13 起只接受 Uri,所以必须显式转换。副作用是非法 URL 的报错提前了:以前字符串直接传进去,直到真正发请求才在 URL 解析阶段失败;现在 Uri.parse 当场抛 FormatException,被示例的 try/catch 捕获后显示成「生成失败: Invalid argument(s): Unsupported scheme …」——第八节的图 13 就是这个现象。对业务方来说这是改进:错误从「网络异常」变成「参数错误」,更容易定位。
其余空安全改动:
| 位置 | 原代码 | 改后 | 为什么 |
|---|---|---|---|
| 请求头 | headers: header | headers: header?.cast<String, String>() | http 0.13 要求 Map<String, String>? |
| 提示查找 | Hint _hintForPath(...),orElse: () => null | Hint? _hintForPath(...) | 找不到时返回 null,返回类型必须可空 |
第二处是典型的「原代码逻辑对、类型标注错」:上游写的是
Hint _hintForPath(String path) {
return this.hints.firstWhere((h) => h.path == path, orElse: () => null);
}
firstWhere 的 orElse 返回 null,但返回类型却写成非空的 Hint。这在非空安全时代只是「类型标注不严谨」,空安全下是硬错误——返回类型必须能容纳 null。
header?.cast<String, String>() 这一行还顺手修了一个类型问题:http 0.13 的 headers 参数类型是 Map<String, String>?,而上游传的是裸 Map。cast 在运行期做惰性转换,如果调用方传了非字符串的值,报错会推迟到真正写 header 的时候——这是上游的既有行为,适配没有改。
4.4 modelName 到文件名的转换
File(path + '/json_to_dart/' + modelName.toLowerCase() + '.dart')
只有 toLowerCase(),没有任何分隔符处理。实测结果:
modelName | 生成的文件名 | 生成的类名 |
|---|---|---|
User | user.dart | User |
DemoModel | demomodel.dart | DemoModel |
RemoteModel | remotemodel.dart | RemoteModel |
UserProfile | userprofile.dart | UserProfile |
文件名不符合 Dart 的 snake_case 文件命名规范(analysis_options.yaml 若开了 file_names lint 会报错),但类名是对的,不影响编译。要符合规范得自己把生成结果重命名,或者给上游提 PR。
五、补全 OHOS 原生实现与工程配置
5.1 AutoJsonToDartConverterPlugin.ets
这是 pubspec.yaml 里声明的那条通道,Dart 侧不调用,但必须实现完整,否则 notImplemented 会让某些探测流程拿不到结果:
import {
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
} from '@ohos/flutter_ohos';
import deviceInfo from '@ohos.deviceInfo';
export default class AutoJsonToDartConverterPlugin implements FlutterPlugin, MethodCallHandler {
private channel: MethodChannel | null = null;
constructor() {
}
getUniqueClassName(): string {
return "AutoJsonToDartConverterPlugin"
}
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), "auto_json_to_dart_converter");
this.channel.setMethodCallHandler(this)
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null)
this.channel = null
}
}
onMethodCall(call: MethodCall, result: MethodResult): void {
try {
if (call.method == "getPlatformVersion") {
// 与 Android("Android x.y.z")/iOS("iOS x.y") 语义一致:返回 "OpenHarmony <系统全名>"
result.success("OpenHarmony " + deviceInfo.osFullName)
} else {
result.notImplemented()
}
} catch (e) {
result.error("PLUGIN_ERROR", String(e), null)
}
}
}
三个细节:
getUniqueClassName()必须与pubspec.yaml的pluginClass完全一致。引擎用这个字符串做插件去重和日志输出,不一致时插件不会注册,Dart 侧调用会一直等到超时。getPlatformVersion返回"OpenHarmony " + deviceInfo.osFullName,格式与 Android 侧的"Android 12"、iOS 侧的"iOS 17.1"对齐。osFullName是@ohos.deviceInfo的只读属性,设备上实测返回OpenHarmony-6.1.1.120。try/catch包住整个分支,异常通过result.error回给 Dart。这是flutter_ohos插件的推荐写法——ArkTS 侧抛出的未捕获异常不会自动传给 Dart,会让 Future 永远挂住。
5.2 ExtStoragePlugin.ets
真正会被调用的那条通道:
import {
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
} from '@ohos/flutter_ohos';
export default class ExtStoragePlugin implements FlutterPlugin, MethodCallHandler {
private channel: MethodChannel | null = null;
private filesDir: string | null = null;
constructor() {
}
getUniqueClassName(): string {
return "ExtStoragePlugin"
}
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.filesDir = binding.getApplicationContext().filesDir;
this.channel = new MethodChannel(binding.getBinaryMessenger(), "ext_storage");
this.channel.setMethodCallHandler(this)
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null)
this.channel = null
}
}
onMethodCall(call: MethodCall, result: MethodResult): void {
try {
if (call.method == "getStorageDirectory") {
result.success(this.filesDir)
} else if (call.method == "getStoragePublicDirectory") {
// The type argument has no OHOS public-directory equivalent; all
// types resolve to the application files directory.
result.success(this.filesDir)
} else {
result.notImplemented()
}
} catch (e) {
result.error("EXT_STORAGE_ERROR", String(e), null)
}
}
}
filesDir 在 onAttachedToEngine 里取一次并缓存,而不是每次 onMethodCall 都调 binding.getApplicationContext()。前者是插件挂载时机的标准做法,后者虽然也能拿到值,但在插件 detach 之后调用 getApplicationContext() 行为未定义。
getStoragePublicDirectory 收了 {'type': 'Pictures'} 这类参数却不读,全部返回同一个目录。这是刻意的:假装把 Pictures 映射到某个路径,业务方以为文件会出现在相册里,实际不会——不如统一返回沙箱目录,让行为可预测。
5.3 权限
ext_storage 映射到 filesDir,属于应用沙箱内的自有目录,不需要任何权限。适配后的插件 module.json5 里 requestPermissions 是空数组。
但宿主应用的 entry 模块需要 ohos.permission.INTERNET,因为 getmethod / postMethod 会发请求。这条容易漏——它不属于插件,属于应用:
{
"module": {
"name": "entry",
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
}
}
5.4 注册并导出插件
ohos/index.ets 负责把两个插件都登记给引擎:
export { default as AutoJsonToDartConverterPlugin } from './src/main/ets/components/plugin/AutoJsonToDartConverterPlugin';
export { default as ExtStoragePlugin } from './src/main/ets/components/plugin/ExtStoragePlugin';
注意这里导出的是两个插件。ext_storage 来自 third_party/ext_storage_ohos,它有自己的 ohos/ 目录和 index.ets;因为是通过 path 依赖引入的,Flutter 生成注册代码时会把它当成独立插件一并注册。第八节的 hilog 里能看到两条独立的 Adding plugin: 记录。
ohos/build-profile.json5 里模块类型是 har,oh-package.json5 的依赖只有 @ohos/flutter_ohos。插件不引入任何第三方 HAR。
5.5 example 的 OHOS 应用结构
example/ohos/ 是标准的宿主工程,几个关键点:
| 文件 | 配置 |
|---|---|
AppScope/app.json5 | bundleName: "zeeshux7860.example.auto_json_to_dart_converter" |
entry/src/main/module.json5 | requestPermissions: [{"name": "ohos.permission.INTERNET"}] |
entry/src/main/ets/entryability/EntryAbility.ets | 继承 FlutterAbility |
build-profile.json5 | compatibleSdkVersion: "6.1.1(24)" |
包名必须是三段式。zeeshux7860.auto_json_to_dart_converter 只有两段,DevEco 的签名工具会报 bundleName 不符合域名格式。加一层 example 变成 zeeshux7860.example.auto_json_to_dart_converter 即可。
六、补全交付文件并提交适配分支
6.1 除代码外还要补全哪些文件
OpenHarmony 生态的插件仓库习惯带四份说明文件,缺了会被评审打回:
| 文件 | 内容 |
|---|---|
README.OpenHarmony_CN.md | 中文说明:安装、TAG 表、兼容性、权限、使用示例 |
README.OpenHarmony.md | 英文版,内容对应 |
CHANGELOG.OpenHarmony.md | 鸿蒙适配的变更记录 |
README.OpenSource | 上游地址、许可证、开源声明 |
CHANGELOG.OpenHarmony.md 里写的是:
## 1.0.0-ohos-1.0.0
* Adapted auto_json_to_dart_converter for the OpenHarmony platform: added the ohos plugin implementation (ArkTS), vendored an OpenHarmony fork of ext_storage, upgraded the Dart layer to null safety (public API unchanged), and provided a full OHOS example app.
README.OpenHarmony_CN.md 的 TAG 表:
| Flutter 框架版本 | TAG 名称 | 分支名 |
|---|---|---|
| 3.47 | 1.0.0-ohos-1.0.0 | master |
兼容性段落:
1. Flutter: 3.47.7-ohos-1.0.0, DevEco Studio: 26.0.0(26.0.0.851), SDK: 6.1.1(24), ROM: 6.1.1.120;
初次提交时这里写的 DevEco 版本是
26.0.0(26.0.0.630),与本次实际使用的26.0.0(26.0.0.851)不符,仓库地址也还写着CPF-Flutter组织。提交92172a0一并修正为oh-flutter与851。这类「文档与实测不一致」的问题在适配提交里很常见,交付前值得逐条核对。
6.2 提交前检查
flutter pub get
flutter analyze
git status --short
git diff --stat
flutter analyze 的结果要如实记录。本次跑出来是 28 issues found,按规则分类:
| 规则 | 数量 | 位置 | 说明 |
|---|---|---|---|
unnecessary_null_comparison | 14 | 13 条在 third_party/json_ast,1 条在 lib/convert/helpers.dart:273 | 空安全迁移后留下的冗余判空 |
non_nullable_equals_parameter | 8 | 全在 third_party/json_ast/lib/tokenize.dart | == 运算符参数写成可空类型 |
deprecated_member_use | 2 | example/lib/main.dart:204,205 | Radio.groupValue / onChanged,Flutter 3.32 起推荐 RadioGroup |
invalid_null_aware_operator | 2 | lib/auto_json_to_dart_converter.dart:15,37 | header?.cast 在 header 非空时可省略 ?. |
invalid_dependency | 2 | pubspec.yaml:15,20 | path 依赖不允许出现在可发布包里 |
统计口径:21 条来自 vendor 的 json_ast(未改动,属于上游遗留),7 条来自自有代码。
这 28 条都是 warning 或 info,没有 error,也就是说代码能编译、能跑。要不要清干净,取决于接收仓库的 lint 策略。path 依赖那两条如果想消掉,加一行 publish_to: none 即可,代价是这个包不能再发 pub.dev——但带 third_party 的包本来也发不了。
6.3 提交并推送
git add -A
git commit -m "feat: adapt auto_json_to_dart_converter for OpenHarmony"
git push origin master
git tag -a 1.0.0-ohos-1.0.0 -m "1.0.0-ohos-1.0.0"
git push origin 1.0.0-ohos-1.0.0
提交信息里值得把改动分类写清楚,尤其是「为什么 vendor」这种评审一定会问的问题:
- Add ohos platform plugin (ArkTS): register channel auto_json_to_dart_converter
with getPlatformVersion aligned to Android/iOS semantics
- Vendor OpenHarmony fork of ext_storage (third_party/ext_storage_ohos):
Dart API unchanged, getExternalStorageDirectory maps to context.filesDir
- Vendor json_ast locally (strip Windows-illegal test fixtures)
- Upgrade Dart layer to null safety with public API and behavior unchanged
- Add ohos platform config in pubspec.yaml
七、使用根目录 example 演示接入
7.1 本地适配时使用路径依赖
在 example/pubspec.yaml 里:
dependencies:
flutter:
sdk: flutter
auto_json_to_dart_converter:
path: ../
7.2 通过 AtomGit 引入插件
在自己的项目里:
dependencies:
auto_json_to_dart_converter:
git:
url: https://atomgit.com/oh-flutter/auto_json_to_dart_converter
ref: 1.0.0-ohos-1.0.0
7.3 两种调用方式
方式一:只生成代码,自己决定写哪。 这条路径完全不碰 ext_storage,也就没有任何平台差异:
import 'package:auto_json_to_dart_converter/convert/model_generator.dart';
final generator = ModelGenerator('User');
final dartCode = generator.generateDartClasses(jsonString);
if (dartCode.warnings.isNotEmpty) {
for (final w in dartCode.warnings) {
debugPrint('${w.warning} @ ${w.path}');
}
}
print(dartCode.code);
方式二:拉远程 JSON + 自动落盘。 走 ext_storage 通道:
import 'package:auto_json_to_dart_converter/auto_json_to_dart_converter.dart';
final code = await AutoJsonToDartConverterGetMethod(
'https://raw.githubusercontent.com/javiercbk/json_to_dart/master/example/sample.json',
'RemoteModel',
const <String, String>{},
).getmethod;
返回值和写到磁盘的内容是同一份。文件落在 filesDir/json_to_dart/remotemodel.dart。
7.4 接入时的四个注意点
第一,字段名转换是单向的。 snake_case 会转成 camelCase,但 fromJson 里仍然按原 key 取值:
RemoteModel.fromJson(Map<String, dynamic> json) {
htmlUrl = json['html_url']; // 字段名 htmlUrl,key 仍是 html_url
}
所以后端改字段名时,fromJson 里的字符串也得跟着改,Dart 编译器不会帮你发现。
第二,toJson 生成的 key 是转换后的名字吗?不是。 看生成的代码:
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['is_admin'] = this.isAdmin; // 写回原始 key
return data;
}
toJson 写回的也是原始 key,往返一致。这一点是对的,比某些生成器做得规范。
第三,warnings 不要忽略。 list is empty 意味着这个字段的列表类型推不出来(List<dynamic>),list is ambiguous / type is ambiguous 意味着同一个列表里出现了不同类型,生成器只取了第一个。忽略警告的后果是运行时 type 'String' is not a subtype of type 'int'。
第四,生成的代码带 new 关键字。 上游的模板字符串里写死了 new List<Items>()、new Map<String, dynamic>()。Dart 3 下 new 仍然合法(只是冗余),所以能编译,但 flutter analyze 会给出 unnecessary_new 提示。想去掉得改 syntax.dart 里的模板。
八、验证、构建与鸿蒙设备运行效果
8.1 静态检查与测试
从插件仓库根目录:
flutter pub get
flutter analyze
flutter test
flutter analyze 的结果见 6.2 节(28 条,无 error)。
flutter test 的情况要说清楚:它在这台机器上跑不起来,原因与前面几篇相同:
Shell: [ERROR:flutter/shell/common/shell.cc(120)] Dart Error: Can't load Kernel binary: Invalid SDK hash.
Shell: [ERROR:flutter/runtime/dart_isolate.cc(146)] Could not prepare isolate.
Shell: [ERROR:flutter/runtime/runtime_controller.cc(576)] Could not create root isolate.
Failed to load ".../test/auto_json_to_dart_converter_test.dart": Connection closed before test suite loaded.
flutter_tester 无法创建 isolate,Invalid SDK hash 说明它与当前 SDK 不匹配,这是本机工具链的问题,与本库无关。所以本文不把 flutter test 记为通过。
不过测试要验证的逻辑是纯 Dart 的,不依赖渲染。用 dart run 起一个临时脚本就能覆盖同样的用例,实测输出如下:
--- 1 nested ok, warnings=0
class DemoModel { | int page; | List<Items> items;
--- 2 empty list warnings=[list is empty @ /tags]
--- 3 ambiguous warnings=[list is ambiguous @ /v]
--- 4 non-json throws: FormatException: FormatException: Unexpected character (at character 1)
--- 5 top array: class User {
--- 6 camel: class RemoteModel { | String htmlUrl; | bool isAdmin;
这 6 条与 test/auto_json_to_dart_converter_test.dart 里的 2 条断言方向一致(类名生成、空列表警告),另外补了 4 条边界。
仓库里目前有两处测试:
| 文件 | 来源 | 断言 |
|---|---|---|
test/auto_json_to_dart_converter_test.dart | 新增 20 行 | ModelGenerator('User') 生成出 class User 与 class Address;{"tags": []} 产生警告 |
example/test/widget_test.dart | 改写 | 首屏能找到 Generate、保存到设备、本地 JSON(用户) 三个控件 |
example/test/widget_test.dart 原来是 flutter create 的模板测试,断言页面里有以 Running on: 开头的 Text。改写后的 Demo 没有这个文案,模板断言必然失败,所以把它换成了对实际控件的查找。这两处测试都未能实际执行(原因同上),只做了静态审读。
8.2 边界情况实测
同一批脚本里顺手探了 5 个上游没测过的输入,结果值得单独列一张表——这些是 FAQ 的来源:
| 输入 | 结果 | 判断 |
|---|---|---|
[](顶层空数组) | 抛 RangeError:Invalid value: Valid value range is empty: 0 | 缺陷。jsonRawDynamicData[0] 没有前置长度检查 |
{"a": null} | 生成 Null a; | 缺陷。Null 不是合法字段类型,生成代码无法编译 |
{"list":[{"a":1},{"a":2,"b":"x"}]} | 生成 List<List> list; | 缺陷。对象数组被误判成二维列表 |
{"class": 1} | 抛 FormatterException | 可理解为已知限制。字段名不转义 Dart 关键字 |
[{"a":1},{"a":2}](顶层非空数组) | 正常生成 class User | 符合预期,取第一个元素推字段 |
{"list":[{...}]} 这个失败案例最值得说。正常的对象数组应该走 mergeObjectList 把多个对象合并成 {a: 1, b: "x"} 再生成一个类,实际却生成了 List<List>。原因在 mergeObjectList 的合并逻辑里:当 a 在两个对象里类型相同(都是 int)时,合并后 obj['a'] 是一个 int;但代码随后把结果 List.filled(1, mergedList.result) 塞回去,再被 TypeDefinition.fromDynamic 看到时就变成了「列表里装着列表」。
这四条缺陷都在上游代码里,不是适配引入的。是否修取决于是否可以偏离上游——本文的选择是如实记录、不修,因为修了会让行为与 pub.dev 版本分叉,接入方从 pub 切到鸿蒙分支时可能踩到不同结果。
8.3 确认设备连接
hdc list targets
flutter devices
本机输出:
4UQ9K25508013016
设备首次连接电脑时需要在手机端确认调试授权。列表为空时检查 USB 连接、调试模式和电脑授权。
8.4 构建
本机使用 DevEco Studio 自带的 hvigor 构建。Windows 上需要把 JBR 同时加进 JAVA_HOME 和 PATH,否则打包阶段会因为找不到 java 失败:
cd example/ohos
export JAVA_HOME="/d/Program Files/Huawei/DevEco Studio/jbr"
export PATH="/d/Program Files/Huawei/DevEco Studio/jbr/bin:$PATH"
"/d/Program Files/Huawei/DevEco Studio/tools/hvigor/bin/hvigorw" \
assembleHap -p product=default -p buildMode=debug --no-daemon
产物位于:
example/ohos/entry/build/default/outputs/default/entry-default-signed.hap
debug 包约 160 MB,体积大头是 libs/arm64-v8a/libflutter.so 与 ets/modules.abc。包内没有独立的 auto_json_to_dart_converter.har 或 ext_storage.har——两个插件都是静态 HAR,ArkTS 源码在编译期合进 modules.abc。
也可以用 flutter build hap --debug。本机在部分工程上遇到过 ohpm.bat 递归调用中断,直接用 hvigorw 更稳。
8.5 安装与启动
cd example/ohos/entry/build/default/outputs/default
hdc install -r entry-default-signed.hap
hdc shell aa start -a EntryAbility -b zeeshux7860.example.auto_json_to_dart_converter
输出:
[Info]App install path:...\entry-default-signed.hap msg:install bundle successfully.
AppMod finish
start ability successfully.
Windows 上
hdc容易把/data/...当成路径转换,命令前面加MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*'更保险。多设备连接时用hdc -t <序列号>指定目标。
8.6 在设备上逐项操作
- 打开应用,确认首屏是「输入」Tab,三个输入源单选、JSON 编辑框、「Generate / 保存到设备」按钮组、状态栏都在;
- 保持「本地 JSON(用户)」,点 Generate,确认状态栏变成「生成成功(无警告)」;
- 切到「生成结果」Tab,确认
DemoModel与Address两个类都生成了,字段类型正确; - 点「保存到设备」,确认弹出「文件已保存」卡片,路径指向
files/json_to_dart/demo_model.dart; - 回到输入页,选「本地 JSON(嵌套列表)」,点 Generate,确认生成
DemoModel(含List<Items>)和Items类; - 选「远程 GET」,点 Generate,确认状态栏提示「远程生成完成,代码已写入设备 json_to_dart 目录」;
- 把 URL 改成非法值再点 Generate,确认状态栏显示「生成失败: Invalid argument(s): Unsupported scheme …」且应用不崩溃。
第 5 步和第 6 步是这次适配最有价值的验证:第 5 步说明嵌套列表的对象合并逻辑在鸿蒙上跑得通,第 6 步说明「网络 + 解析 + 落盘」三条链路端到端串起来了。
8.7 鸿蒙设备运行效果
首屏。「输入」Tab 下三个输入源 + JSON 编辑区 + 按钮组,状态栏提示当前该做什么:
图 5:首屏。默认选中「本地 JSON(用户)」,编辑区里是一份包含 int / String / bool / double / List / 嵌套对象的样例,故意覆盖全部类型分支。
选中「本地 JSON(嵌套列表)」后,编辑区换成带对象数组的样例:
图 6:{"page": 1, "items": [{"id": 1, "title": "first", "price": 9.9}, ...]},items 是两个对象的数组,会被合并成一个 Items 类。
点 Generate,状态栏变为「生成成功(无警告)」,「清空结果」按钮同时由灰变亮:
图 7:「生成成功(无警告)」说明 warnings 为空——空列表和歧义列表都没出现,类型推断完全确定。
切到「生成结果」Tab,DemoModel 的完整代码。注意 List<Items> items; 和 items.add(new Items.fromJson(v)) 两处:
图 8:DemoModel.fromJson 里对 items 先判 != null 再 forEach,toJson 里用 map((v) => v.toJson()).toList() 转回去。
往下翻能看到 Items 类的三个字段 id / title / price,类型分别是 int / String / double:
图 9:Items 由两个对象的合并结果推出——第一个对象有 id/title/price,第二个也有,类型一致,所以直接合并。
点「保存到设备」,底部弹出绿色对勾卡片,路径完整显示:
图 10:/data/storage/el2/base/files/json_to_dart/demo_model.dart。这是应用侧看到的路径,设备侧实际落在 /data/app/el2/100/base/<bundleName>/files/json_to_dart/。
切到「远程 GET」,URL 输入框自动填好 GitHub 的 sample.json 地址:
图 11:URL 是上游 README 里的示例地址。选中远程源时,JSON 编辑区会被隐藏,只留 URL 输入框。
点 Generate,状态栏提示远程生成完成:
图 12:「远程生成完成,代码已写入设备 json_to_dart 目录」——一句话覆盖了 HTTP 请求、JSON 解析、代码生成、文件写入四步。
URL 不合法时(本例是被误拼接成 httpshttps://... 的地址),错误被 catch 住并原样显示,应用不崩溃:
图 13:生成失败: Invalid argument(s): Unsupported scheme 'httpshttps' in URI httpshttps://example.com//examplhttp...。异常来自 Uri.parse,在 HTTP 请求发出之前就抛了。
8.8 落盘结果核对
hdc file recv 把两个生成的文件拉回宿主机,与截图里的代码逐字对照。demo_model.dart 共 1354 字节:
class DemoModel {
int id;
String name;
String email;
bool isAdmin;
double score;
List<String> tags;
Address address;
DemoModel.fromJson(Map<String, dynamic> json) {
id = json['id'];
name = json['name'];
email = json['email'];
isAdmin = json['is_admin']; // snake_case key -> camelCase field
score = json['score'];
tags = json['tags'].cast<String>();
address =
json['address'] != null ? new Address.fromJson(json['address']) : null;
}
}
remotemodel.dart 共 2879 字节,与上游 sample.json 的字段一一对应:
| sample.json 的 key | 生成的字段 | 推断类型 |
|---|---|---|
username | username | String |
favouriteInteger | favouriteInteger | int |
favouriteDouble | favouriteDouble | double |
html_url | htmlUrl | String |
tags | tags | List<String> |
randomIntegers | randomIntegers | List<int> |
personalInfo | personalInfo | PersonalInfo(嵌套类) |
设备侧的目录清单,两个文件的时间戳与截图里的操作时刻吻合:
-rw-rw-rw- 1 20020248 20020248 1354 2026-10-23 12:02 demo_model.dart
-rw-rw-rw- 1 20020248 20020248 2879 2026-10-23 11:20 remotemodel.dart
8.9 运行时日志
插件注册阶段(PID 19577):
10-23 12:01:52.794 19577 19577 W .../Flutter: FlutterEngineCxnRegistry --> Adding plugin: AutoJsonToDartConverterPlugin
10-23 12:01:52.794 19577 19577 D .../Flutter: DartMessenger --> Setting handler for channel 'auto_json_to_dart_converter'
10-23 12:01:52.794 19577 19577 W .../Flutter: FlutterEngineCxnRegistry --> Adding plugin: ExtStoragePlugin
10-23 12:01:52.794 19577 19577 D .../Flutter: DartMessenger --> Setting handler for channel 'ext_storage'
两条 Adding plugin: 对应 ohos/index.ets 里导出的两个插件,两条 Setting handler 对应两个通道名。同一次启动共注册了 17 个 channel handler,其中 15 个属于 flutter/*(flutter/platform、flutter/textinput、flutter/navigation 等),2 个是我们的。完整清单:
auto_json_to_dart_converter ext_storage
flutter/accessibility flutter/displaymetrics
flutter/isolate flutter/keyboard
flutter/localization flutter/mousecursor
flutter/nativevsync flutter/navigation
flutter/platform flutter/platform_views
flutter/platform_views_2 flutter/restoration
flutter/sensitivecontent flutter/test
flutter/textinput
点「保存到设备」时,ext_storage 通道被真正调用了:
10-23 12:02:07.127 19577 19577 D .../Flutter: DartMessenger --> Received message from Dart over channel 'ext_storage'
而 auto_json_to_dart_converter 通道在整个交互过程中没有任何 Received message 记录——印证了 4.1 的结论:这条通道只注册,不走流量。
九、FAQ:适配过程与使用问题
9.1 flutter pub get 报 json_ast 解压失败
现象:解析依赖时抛出类似 PathNotFoundException 或 Filename too long,停在 json_ast。
根因:json_ast 1.0.5 的 test/fixtures/ 下有文件名带冒号(invalid:name.json 之类)的样例文件,Windows 文件系统不允许 : 出现在文件名中,解压直接失败。
处置:把 json_ast 的 lib/ 与 pubspec.yaml 拷到 third_party/json_ast,删掉 test/,pubspec.yaml 里改成 path 依赖。
json_ast:
path: third_party/json_ast
9.2 ext_storage 在 pub.dev 上已经废弃了,能换掉吗
可以,但要改 Dart 侧代码。ext_storage 的全部用处就是拿一个可写目录,换成 path_provider 的 getApplicationDocumentsDirectory() 即可:
final dir = await getApplicationDocumentsDirectory();
await Directory('${dir.path}/json_to_dart').create(recursive: true);
代价是 path_provider 本身也要有 ohos 实现(官方 path_provider_ohos 已提供)。本次适配选择保留 ext_storage 的 API 形状,理由是不偏离上游——接入方从 pub.dev 版本切到鸿蒙分支时,业务代码一行都不用改。
9.3 生成的文件在系统文件管理器里找不到
见 4.2。鸿蒙上映射到 filesDir,是应用沙箱目录,文件管理器看不到。要在设备上取出来:
hdc file recv /data/app/el2/100/base/<bundleName>/files/json_to_dart/demo_model.dart ./
需要用户能看到文件时,另行接 @ohos.file.picker 让用户选保存位置,或生成后调起分享面板。
9.4 顶层是空数组 [] 时抛 RangeError
上游缺陷。_generateClassDefinition 里对 List 分支直接取 jsonRawDynamicData[0],没有长度检查:
if (jsonRawDynamicData is List) {
final node = navigateNode(astNode, '0');
_generateClassDefinition(className, jsonRawDynamicData[0], path, node); // ← 空数组崩在这
}
空数组本身没有字段信息,生成不出有意义的类,所以「抛异常」在语义上说得过去,但抛的是 RangeError 而不是带说明的异常,调用方不好判断。规避办法是在调 generateDartClasses 前自己判空。
9.5 {"a": null} 生成 Null a;,编译不过
上游缺陷。TypeDefinition.fromDynamic(null, node) 得到类型名 Null,ClassDefinition 把它当成合法字段类型直接写出去了。
规避:JSON 里不要有值为 null 的字段。如果有,要么在服务端过滤,要么拿到生成结果后手工把 Null 改成 dynamic 或可空类型。这个修起来不难(在 helpers.dart 的 getTypeName 里把 null 归为 dynamic),但改了会与 pub.dev 版本行为不一致,本次没有动。
9.6 对象数组被生成成 List<List>
见 8.2。{"list":[{"a":1},{"a":2,"b":"x"}]} 期望生成 List<Foo>,实际生成 List<List>。
规避:如果数组元素字段完全一致(没有「这个对象多一个字段」的情况),生成结果是对的;只有字段不完全一致、需要走 mergeObjectList 合并时才出问题。遇到这种情况可以先把 JSON 里的对象对齐成相同字段集再生成。
9.7 字段名是 Dart 关键字时报 FormatterException
{"class": 1} 会生成 int class;,dart_style 格式化时直接抛 FormatterException。
上游没有做关键字转义。规避:服务端改字段名,或者在调生成器之前先把 JSON 里的关键字 key 重命名(class → class_),生成后再手工改回来。
9.8 远程 GET 报 SocketException
先检查 entry/module.json5 有没有 ohos.permission.INTERNET(见 5.3)。其次确认 URL 用的是 https——鸿蒙对明文 HTTP 默认拦截,需要额外配 network_security_config 才放行。
9.9 远程 GET 返回的不是 JSON 怎么办
getmethod 不看状态码,直接把 res.body 交给 decodeJSON。所以:
| 响应 | 结果 |
|---|---|
| 200 + 合法 JSON | 正常生成 |
| 200 + HTML(登录页、错误页) | FormatException: Unexpected character (at character 1) |
| 404 + JSON 错误体 | 会正常生成,把错误体当成数据模型 |
| 500 + HTML | FormatException |
第三行是真正危险的情况。接入方拿到 code: 404, message: "not found" 生成出来的 RemoteModel 类,字段全是 int code; String message;,编译能过、运行能跑,直到某个页面莫名其妙显示「not found」才被发现。生产代码建议不要用 getmethod,改用 7.3 的方式一,自己发请求、自己判状态码、只把 body 交给 ModelGenerator。
9.10 文件名全小写,不符合 snake_case 规范
见 4.4。RemoteModel → remotemodel.dart。上游只做 toLowerCase()。
规避:生成后手工重命名,或者在业务侧先调用 generateDartClasses 拿字符串,自己决定文件名与路径。
9.11 生成的代码里到处是 new,analyze 会提示吗
上游模板字符串里写死了 new(new List<Items>()、new Map<String, dynamic>())。Dart 3 下合法,只是冗余。
flutter analyze 不会提示,因为本仓库既没有 analysis_options.yaml,也没有 flutter_lints 依赖,走的是 Dart 默认规则(unnecessary_new 不在其中)——这也是本次 28 条里看不到它的原因。如果你把自己的项目接入这些生成代码,且项目里引了 flutter_lints,那就会出现一大批 unnecessary_new。
两种处置:项目级 analysis_options.yaml 里加
linter:
rules:
unnecessary_new: false
或者改 lib/convert/syntax.dart 的模板把 new 去掉。
9.12 遗留问题清单
| 项 | 状态 | 说明 |
|---|---|---|
flutter test 未通过 | 未执行 | flutter_tester 报 Invalid SDK hash,本机工具链问题;测试逻辑已用 dart run 覆盖 |
| release 模式真机运行 | 未验证 | 本机工具链缺 Windows 交叉 gen_snapshot,仅完成 debug 模式验证 |
顶层空数组 RangeError | 已知缺陷 | 上游遗留,未修(保持与 pub.dev 行为一致) |
null 字段生成 Null 类型 | 已知缺陷 | 上游遗留,未修 |
对象数组误判 List<List> | 已知缺陷 | 上游遗留,未修 |
关键字字段名 FormatterException | 已知限制 | 上游遗留,未修 |
文件名不合 snake_case | 已知限制 | 上游遗留,未修 |
flutter analyze 28 条 | 保留 | 21 条来自 vendor 的 json_ast,7 条为自有代码的冗余判空 / 废弃 API / path 依赖提示 |
相关链接
- 原始仓库:zeeshux7860/auto_json_to_dart_converter
- 本文配套仓库:oh-flutter/auto_json_to_dart_converter
- Flutter OH 开发环境搭建:flutter_samples / docs / ohos / getting-started
- Flutter OH 社区:CPF-Flutter
- 本文示例 JSON 上游:javiercbk/json_to_dart
更多推荐



所有评论(0)