开发工具: 华为云码道

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?)

四层的改动都只涉及语法,没有一个字动了业务逻辑——这一点可以逐行对比上游客确认。

KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页

对外 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;
}

按顺序看这几行:

  1. http.get 直接把 res.body 交给生成器——没有状态码判断,404 页面、登录跳转 HTML 都会被当成 JSON 解析,然后在 decodeJSON 里抛异常。这个写法在上游就有,不是适配引入的。
  2. generateDartClasses = generateUnsafeDart + dart_style 格式化。名字里的 unsafe 说的是「生成的 Dart 可能语法不合法」,所以后面必须过一遍 DartFormatter。
  3. ExtStorage.getExternalStorageDirectory() 是唯一的平台通道调用,也是鸿蒙适配的核心。
  4. 文件名是 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 SDK3.47.7-ohos-1.0.0Flutter 编译与 OHOS 平台工具链
Flutter 分支oh-3.47-dev对应开发分支
Dart SDK3.13.3Dart 语言与包管理环境
DevEco Studio26.0.0(26.0.0.851)工程 Sync、签名配置与 hvigor 构建
测试设备TLR-AL00(nova 14),OpenHarmony 6.1.1.120真机安装与运行
设备 API24const.ohos.apiversion 实测值
compatibleSdkVersion6.1.1(24)工程声明的最低兼容版本
targetSdkVersion6.1.1(24)应用面向的行为版本
插件版本1.0.0+ohos-1.0.0pubspec.yaml 中的包版本
原生语言ArkTSHarmonyOS 插件实现
插件产物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.dartGet / 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 库不同,这个库运行时真的会走平台通道,而且一走就是两条:

ArkTS

Dart

channel: auto_json_to_dart_converter

channel: ext_storage

AutoJsonToDartConverterGetMethod

http.get

ModelGenerator

ExtStorage.getExternalStorageDirectory

File.writeAsStringSync

AutoJsonToDartConverterPlugin

ExtStoragePlugin

网络

应用沙箱 filesDir

通道Dart 侧调用点ArkTS 侧实现运行时是否被调用
auto_json_to_dart_converter无AutoJsonToDartConverterPlugin.ets否
ext_storageExtStorage.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 等),而且面向的是「文件管理器」类应用,不是普通应用的功能目录。

适配采用的映射是:

AndroidOHOS理由
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: headerheaders: header?.cast<String, String>()http 0.13 要求 Map<String, String>?
提示查找Hint _hintForPath(...),orElse: () => nullHint? _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生成的文件名生成的类名
Useruser.dartUser
DemoModeldemomodel.dartDemoModel
RemoteModelremotemodel.dartRemoteModel
UserProfileuserprofile.dartUserProfile

文件名不符合 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.json5bundleName: "zeeshux7860.example.auto_json_to_dart_converter"
entry/src/main/module.json5requestPermissions: [{"name": "ohos.permission.INTERNET"}]
entry/src/main/ets/entryability/EntryAbility.ets继承 FlutterAbility
build-profile.json5compatibleSdkVersion: "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.471.0.0-ohos-1.0.0master

兼容性段落:

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_comparison1413 条在 third_party/json_ast,1 条在 lib/convert/helpers.dart:273空安全迁移后留下的冗余判空
non_nullable_equals_parameter8全在 third_party/json_ast/lib/tokenize.dart== 运算符参数写成可空类型
deprecated_member_use2example/lib/main.dart:204,205Radio.groupValue / onChanged,Flutter 3.32 起推荐 RadioGroup
invalid_null_aware_operator2lib/auto_json_to_dart_converter.dart:15,37header?.cast 在 header 非空时可省略 ?.
invalid_dependency2pubspec.yaml:15,20path 依赖不允许出现在可发布包里

统计口径: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 在设备上逐项操作

  1. 打开应用,确认首屏是「输入」Tab,三个输入源单选、JSON 编辑框、「Generate / 保存到设备」按钮组、状态栏都在;
  2. 保持「本地 JSON(用户)」,点 Generate,确认状态栏变成「生成成功(无警告)」;
  3. 切到「生成结果」Tab,确认 DemoModel 与 Address 两个类都生成了,字段类型正确;
  4. 点「保存到设备」,确认弹出「文件已保存」卡片,路径指向 files/json_to_dart/demo_model.dart;
  5. 回到输入页,选「本地 JSON(嵌套列表)」,点 Generate,确认生成 DemoModel(含 List<Items>)和 Items 类;
  6. 选「远程 GET」,点 Generate,确认状态栏提示「远程生成完成,代码已写入设备 json_to_dart 目录」;
  7. 把 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 住并原样显示,应用不崩溃:

KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页

图 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生成的字段推断类型
usernameusernameString
favouriteIntegerfavouriteIntegerint
favouriteDoublefavouriteDoubledouble
html_urlhtmlUrlString
tagstagsList<String>
randomIntegersrandomIntegersList<int>
personalInfopersonalInfoPersonalInfo(嵌套类)

设备侧的目录清单,两个文件的时间戳与截图里的操作时刻吻合:

-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 + HTMLFormatException

第三行是真正危险的情况。接入方拿到 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 依赖提示

相关链接

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐