Flutter 三方库「json2dart_safe」JSON转Dart的鸿蒙化适配指南
开发工具: 华为云码道
json2dart_safe 将 JSON 与 Dart Model 之间的安全转换封装为 Flutter 库,应用在字段缺失或类型不匹配时得到默认值而不是异常,并可配合 json2dart_db 完成本地数据库的增删改查。本文以 json2dart_safe 1.6.0 为例,介绍源码准备、OHOS 宿主工程配置、FFI 数据库链路和 example 真机运行。
库本体为纯 Dart 实现,可直接在 ohos 平台运行,示例工程已补全 OHOS 宿主,源码位于 GitHub 配套仓库。文中的代码以提交 5a2400e1401e6ba569c851bb4c8a824997dde915 为参考。
| 添加测试用户 | 编辑用户 | 删除用户 |
|---|---|---|
| SnackBar 提示新增成功 | 对话框修改昵称/年龄/爱好/VIP | 确认后记录从列表消失 |
一、插件简介与适配目标
安全转换是 json2dart_safe 的核心能力。应用通过 MapExt 扩展调用 asString、asInt、asBool 等方法解析 JSON 字段,库在类型不匹配或字段缺失时返回默认值而不是抛出异常。业务层不需要自己编写 try/catch 兜底,也不需要针对不同后端的字段风格各写一套解析代码。
例如,后端把 VIP 标志有时返回 true、有时返回 1、有时返回 'true',asBool / asBools 都能正确处理;字段名不固定时(id 或 user_id),asInts 等多字段解析会按顺序选取第一个有值的字段;配合 json2dart_db,同一套 Model 可以直接落到本地 SQLite。
数据解析在 Dart 层同步完成:json2dart_safe 全部由 Dart 实现,没有平台通道和原生依赖,OHOS 平台可直接使用;适配工作的重点是 example 的 ohos 宿主工程和 FFI 数据库链路。
二、环境准备
环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。
完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:
flutter --version
flutter doctor -v
hdc list targets
工程使用的工具链和 SDK 配置如下:
| 项目 | 版本或配置 | 用途 |
|---|---|---|
| Flutter OHOS SDK | 3.44.9+ohos-0.0.1-canary1 | Flutter 编译与 OHOS 平台工具链 |
| Flutter 分支 | user-branch(gitcode.com/CPF-Flutter/flutter_flutter) | CPF-Flutter 对应开发分支 |
| Dart SDK | 3.12.2 | Dart 语言与包管理环境 |
| HarmonyOS 开发套件 | 26.0.0(API 26) | 开发套件版本及对应的 API 级别 |
compileSdkVersion | 工程未显式声明 | 编译时使用本机 API 26 SDK |
targetSdkVersion | 工程未显式声明 | 应用面向的行为版本 |
compatibleSdkVersion | 5.1.0(18) | 当前工程声明的最低兼容版本 |
| 插件版本 | 1.6.0 | pubspec.yaml 中的包版本 |
| 原生语言 | 纯 Dart(宿主 ArkTS) | 库实现语言与宿主工程语言 |
| 示例应用产物 | HAP | 宿主 entry 模块构建产物,库本体无原生产物 |
2.1 开发套件版本与工程中的 SDK 版本配置
26.0.0(API 26) 和 5.1.0(18) 涉及 HarmonyOS 开发套件版本(API 版本)及其底座 OpenHarmony 的版本号体系。在本文工程中,相关版本的含义与配置方式如下:
26.0.0(API 26)表示本机 DevEco Studio 携带的 HarmonyOS 开发套件版本为26.0.0,对应 API 26,构建时使用该 SDK 编译。- 本工程的
example/ohos/build-profile.json5没有显式声明compileSdkVersion和targetSdkVersion,编译行为由本机 SDK 决定。 5.1.0(18)是本文工程中compatibleSdkVersion的属性值,声明最低兼容 API 18。
对应的 product 配置为:
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.1.0(18)",
"runtimeOS": "HarmonyOS"
}
这组配置声明最低兼容 API 18,编译使用本机 API 26 SDK。json2dart_safe 本身是纯 Dart 库,不依赖系统能力;示例的 sqlite3 由随包 libsqlite3.z.so 通过 FFI 提供,也不依赖系统数据库服务,安装后能否运行主要取决于设备是否满足安装版本门槛。
三、从源码仓库开始准备适配工程
3.1 将上游源码同步到仓库
适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。
在代码托管平台的网页新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在同一平台,也可以通过 Fork 获得自己的工作仓库。
本例的上游与适配仓库都是 GitHub 上的 fastcode555/Json2Dart_Null_Safety(BSD 3-Clause),适配提交直接在该仓库的 master 分支完成。需要独立工作仓库时,可将上游导入 AtomGit 或 Fork 后再拉取。
3.2 将代码拉取到宿主机
在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:
git clone https://github.com/fastcode555/Json2Dart_Null_Safety.git
cd Json2Dart_Null_Safety
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD
git clone 会创建 Json2Dart_Null_Safety/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yaml、lib/ 和 example/。Git 仓库名是 Json2Dart_Null_Safety,Dart 包名是 json2dart_safe。
需要使用与本文相同的代码版本时,在没有未提交修改的仓库中切换到以下提交:
git switch --detach 5a2400e1401e6ba569c851bb4c8a824997dde915
适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

图 1:在宿主机终端输入 GitHub 仓库拉取命令。
3.3 在仓库根目录创建适配分支
接着在 Json2Dart_Null_Safety/ 根目录创建分支。分支名统一使用 feat/ohos_库名称_版本号,库名称取 pubspec.yaml 的 name,版本号取此次适配的基线版本。本例为:
git switch -c feat/ohos_json2dart_safe_1.6.0
git branch --show-current
如果该分支已存在,使用 git switch feat/ohos_json2dart_safe_1.6.0 切换即可。本例的适配提交直接落在 master 分支(34ffcd4 适配与文档、3472a35 修复真机空白屏幕、5a2400e 更换示例),并用 tag 1.6.0-ohos-1.0.0-beta.1 标记第一个适配提交;沿用分支约定的团队按上面的命令创建分支即可。

图 2:在仓库根目录输入适配分支创建命令。
3.4 补全 OHOS 适配结构
分支创建后,仍在同一个仓库根目录执行结构补全。json2dart_safe 是纯 Dart 库,没有原生代码,不需要为库本身生成 ohos/ 插件模块;需要补全的是 example 的 ohos 宿主工程。以下命令适用于尚无 example/ohos/ 目录的既有 Flutter 应用:
cd example
flutter create --platforms=ohos .
cd ..
git status --short
git diff -- example
--platforms=ohos指定需要补全的平台。.表示在当前示例目录补全工程,不是另建一层目录。
该命令生成 OHOS 宿主工程脚手架。生成后通过 diff 检查 example/ 的变化,保留已有页面、依赖配置和 pubspec.yaml。不同 Flutter OH 版本生成的模板可能略有差异。
如果本机 Xcode 版本过旧,在目标目录内直接生成会触发 xcodebuild 校验失败;可以改用脚手架拷贝法:在 /tmp 用 flutter create --template=app --platforms=ohos <name> 生成应用脚手架,只把其中的 ohos/ 目录拷贝到 example/ 下,并删除 ohosTest、node_modules 等模板产物。本例的 example/ohos/ 即按此方式生成。
配套仓库已经包含 example/ohos/,直接运行示例时可以跳过结构补全。

图 3:在示例目录输入 OHOS 结构补全命令。
3.5 适配后的项目目录
适配后的关键目录如下:
Json2Dart_Null_Safety/
├── lib/
│ ├── json2dart.dart
│ └── src/
│ ├── json2dart.dart
│ ├── json_formatter.dart
│ └── json_parse_utils.dart
├── example/
│ ├── lib/
│ │ ├── main.dart
│ │ ├── models/user_model.dart
│ │ ├── database/
│ │ │ ├── db_manager.dart
│ │ │ └── dao/user_dao.dart
│ │ └── pages/user_page.dart
│ ├── test/widget_test.dart
│ └── ohos/
│ ├── AppScope/app.json5
│ ├── build-profile.json5
│ └── entry/
│ ├── libs/arm64-v8a/libsqlite3.z.so
│ └── src/main/ets/entryability/EntryAbility.ets
├── json2dart_db/
├── test/
├── docs/
└── pubspec.yaml
项目根目录如下,其中包含 example/ohos/,以及 OpenHarmony 中英文说明和变更记录文件:

图 4:适配后的 Json2Dart_Null_Safety 项目根目录。
| 文件 | 主要职责 |
|---|---|
lib/json2dart.dart | 统一导出库的公开 API |
lib/src/json_parse_utils.dart | 提供 asString、asBool、asList 等安全转换扩展 |
lib/src/json2dart.dart | 声明解析错误回调的单例入口 |
lib/src/json_formatter.dart | 将 JSON 输出为带缩进的格式化文本 |
EntryAbility.ets | 挂载 Flutter 引擎并注册插件 |
示例 entry module.json5 | 声明宿主应用 Ability 与设备类型(本例无权限声明) |
example/lib/main.dart | FFI 数据库初始化与应用入口 |
example/lib/pages/user_page.dart | 展示用户列表、添加、编辑和删除 |
四、Dart 接口与数据流分析
OHOS 适配需要遵循 Dart 层已有的方法和行为约定。先阅读 lib/json2dart.dart、lib/src/json_parse_utils.dart 和 lib/src/json_formatter.dart。json2dart_safe 是纯 Dart 库,没有平台通道,也没有需要对应的原生实现。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。
本例的对应关系如下:
| Dart 入口或模型 | 实现位置 | OHOS 表现 | 应保持的行为 |
|---|---|---|---|
asString / asInt / asDouble / asBool 等 | lib/src/json_parse_utils.dart | 纯 Dart 直接运行 | 类型不匹配或缺失时返回默认值,不抛异常 |
asStrings / asInts / asBools 等多字段解析 | lib/src/json_parse_utils.dart | 纯 Dart 直接运行 | 多 key 依次选取,bool 优先取本身就是 bool 的字段 |
asList / asArray2d / asBean | lib/src/json_parse_utils.dart | 纯 Dart 直接运行 | 支持 toBean 转换,JSON 字符串自动解码 |
Json2Dart.instance.addCallback | lib/src/json2dart.dart | 纯 Dart 直接运行 | 解析失败触发全局回调,便于日志采集 |
JsonFormatter.format | lib/src/json_formatter.dart | 纯 Dart 直接运行 | 输出带缩进的格式化文本 |
json2dart_db 的 BaseDao / BaseDbModel | 同仓库 json2dart_db/ 子包 | FFI 直连随包 sqlite3 | 数据库读写与 Model 转换行为一致 |
这些方法全部由 Dart 实现,OHOS 平台不需要补对应的原生分支;适配的重点是保证 Dart 层 API 零改动,并打通 example 的运行链路。
4.1 跨端架构与调用时序
库本体不经过平台通道。example 的数据链路是:
json2dart_safe负责 Model 与Map的双向安全转换;json2dart_db的 DAO 通过 sqflite 读写数据库;- 在 OHOS 上,sqflite 没有原生实现,由
sqflite_common_ffi直连随包的libsqlite3.z.so。
json2dart_safe 在这条链路中负责 Model 与 Map 的双向转换;数据库读写由 FFI 完成,不经过平台通道。
4.1.1 一次完整新增用户的时序
4.2 数据模型:example/lib/models/user_model.dart
json2dart_db 的 BaseDao 以 Map 读写数据,user_model.dart 用 json2dart_safe 完成两个方向的转换:
/// JSON 序列化(put 会跳过 null 与空字符串字段)
Map<String, dynamic> toJson() => <String, dynamic>{}
..put('user_id', userId)
..put('username', username)
..put('nickname', nickname)
..put('avatar', avatar)
..put('age', age)
..put('height', height)
..put('is_vip', isVip)
..put('birthday', birthday?.millisecondsSinceEpoch)
..put('hobbies', hobbies)
..put('following_ids', followingIds)
..put('extra', extra);
/// JSON 反序列化(json2dart_safe 的 asXxx 在类型不匹配时返回默认值)
UserModel.fromJson(Map json) {
userId = json.asInt('user_id');
username = json.asString('username');
nickname = json.asString('nickname');
avatar = json.asString('avatar');
age = json.asInt('age');
height = json.asDouble('height');
isVip = json.asBool('is_vip');
final Object? bd = json['birthday'];
birthday = bd is int ? DateTime.fromMillisecondsSinceEpoch(bd) : null;
hobbies = json.asList<String>('hobbies');
followingIds = json.asList<int>('following_ids');
final Object? ex = json['extra'];
extra = ex is Map ? Map<String, dynamic>.from(ex) : null;
}
各解析方法在字段缺失或类型不匹配时的默认返回如下:
| 解析方法 | 默认返回 |
|---|---|
asString | ''(或 defValue) |
asInt / asNum | 0 |
asDouble | 0.0 |
asBool | false |
asList / asArray2d / asBean | null |
即使后端新增或改变字段类型,现有解析逻辑也不会抛异常,解析失败会走 print 与回调通知。数据库中的 is_vip 以 'true' / 'false' 字符串存储、hobbies 等列表以 JSON 字符串存储,asBool 与 asList 都能兼容这些形态。
4.3 公开 API 与错误回调
Json2Dart 是全局单例,解析失败的回调在全局生效:
class Json2Dart {
static Json2Dart? _instance;
factory Json2Dart() => _getInstance();
static Json2Dart get instance => _getInstance();
static Json2Dart _getInstance() => _instance ??= Json2Dart._();
Json2Dart._();
Function(String)? callBack;
Function(String method, String key, Map? map)? detailCallBack;
///添加报错回调
void addCallback(Function(String) callBack) {
this.callBack = callBack;
}
///添加报错回调,详细的解析方式跟map
void addDetailCallback(Function(String method, String key, Map? map) callBack) {
this.detailCallBack = callBack;
}
}
各回调的触发时机如下:
callBack:任何解析方法兜底时调用,可执行多次;detailCallBack:额外携带解析方法名、key 与原始 Map,便于定位脏数据。
业务通过 Json2Dart.instance.addCallback(...) 注册回调,即可统一采集解析失败日志。
4.4 安全解析的关键实现
4.4.1 多字段解析:优先取本身就是 bool 的字段
bool asBools(List<String> keys, [bool? defValue]) {
var keyHasValues = <String>[];
//优先使用返回值就是bool的作为返回值
for (var key in keys) {
Object? value = this![key];
if (value == null) continue;
if (value is bool) return value;
keyHasValues.add(key);
}
//找不到bool值,兼容其1,或者0,或者字符串作为返回值
for (var key in keyHasValues) {
Object? value = this![key];
if (value == null) continue;
if (value is int && (value == 1 || value == 0)) {
return asBool(key, defValue);
}
if (value is String && (value == 'true' || value == 'false' || value == '1' || value == '0')) {
return asBool(key, defValue);
}
}
return defValue ?? false;
}
asBool 单字段版兼容 bool、1 / 0 与 'true' / 'false' / '1' / '0' 字符串,其余值兜底为 defValue ?? false 并触发回调。
4.4.2 列表与嵌套 Model 解析
List<T>? asList<T>(String key, [Function(Map json)? toBean]) {
if (this == null) return null;
try {
Object? obj = this![key];
if (toBean != null && obj != null) {
if (obj is List) {
if (obj.isEmpty) return [];
//二维数组的处理
if (obj.is2dArray) {
return obj.map((ele) => ele.map((v) => toBean(v)).toList()).toList().cast<T>();
}
return obj.map((v) => toBean(v)).toList().cast<T>();
} else if (obj is String) {
List _list = jsonDecode(obj);
return _list.map((v) => toBean(v)).toList().cast<T>();
}
} else if (obj != null) {
if (obj is List) {
return _listFrom<T>(obj, key);
} else if (obj is String) {
return _listFrom<T>(jsonDecode(obj), key);
}
}
} catch (e) {
print(e);
_print('json parse failed,exception value::\"$key\":${this![key]}');
_printDetail('asList', key, this);
}
return null;
}
传入 toBean 时支持嵌套 Model 与二维数组;值为 JSON 字符串时自动 jsonDecode 后再转换。解析失败打印异常并兜底返回 null。
4.4.3 序列化时的空值处理
///key and value的功能
Map put(String key, Object? value) {
if (value != null && value is String && value.isNotEmpty) {
this![key] = value;
} else if (value != null && value is! String) {
this![key] = value;
}
return this!;
}
///移除掉空的
void removeNull() {
if (this == null || this!.isEmpty) return;
var keys = List.from(this!.keys);
for (Object key in keys) {
if (this![key] == null) this?.remove(key);
}
keys.clear();
}
put 链式写入并跳过 null 与空字符串,removeNull / removeNullOrEmpty 用于清理已生成的 Map。toJson 全部通过 put 完成,写库时不会产生空值脏数据。
五、补全 OHOS 宿主实现与工程配置
5.1 在 EntryAbility.ets 中挂载 Flutter 引擎
以示例应用启动为例:业务仍调用 json2dart_safe 的 Dart API,Dart 层不需要任何改动。需要补全的是 example/ohos 宿主工程:EntryAbility 继承 FlutterAbility 挂载 Flutter 引擎,并随包提供 libsqlite3.z.so 供 FFI 直连。这样业务页面沿用原有代码即可在 OHOS 设备上运行。
EntryAbility 继承 FlutterAbility。下面列出宿主侧的主要文件,完整工程还包含 AppScope、resources 等应用配置。
宿主入口位于:
example/ohos/entry/src/main/ets/entryability/EntryAbility.ets
5.1.1 引入 Flutter 引擎模块
import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';
其中:
FlutterAbility提供 Flutter 页面承载与引擎生命周期;FlutterEngine是引擎实例,注册插件时作为参数传入;GeneratedPluginRegistrant由 Flutter 工具生成,负责把依赖的原生插件注册进引擎。
5.1.2 连接 Flutter Engine
export default class EntryAbility extends FlutterAbility {
configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
}
}
configureFlutterEngine 在引擎就绪后调用,registerWith 完成插件注册。json2dart_safe 是纯 Dart 库,本身没有需要注册的原生实现;示例依赖中也没有带 OHOS 原生实现的插件,因此注册函数当前为空。
5.1.3 准备随包 sqlite3(NDK 交叉编译)
OHOS 上没有 sqflite 的原生实现,示例改用 FFI 直连 sqlite3。用 NDK 交叉编译 sqlite3 得到动态库,放入:
example/ohos/entry/libs/arm64-v8a/libsqlite3.z.so
该文件会随 HAP 打包进应用,Dart 侧通过 DynamicLibrary.open('libsqlite3.z.so') 加载,无需系统数据库服务。
5.1.4 Dart 侧初始化 FFI 数据库
example/lib/main.dart 在启动时完成初始化:
/// OpenHarmony 等无 sqflite 原生实现的平台,改用 FFI 直连随包 sqlite3。
Future<void> _initDatabase() async {
final String os = Platform.operatingSystem;
if (os != 'android' && os != 'ios' && os != 'macos' && os != 'windows') {
try {
// 加载随 HAP 打包的 sqlite3(NDK 交叉编译的 libsqlite3.z.so)
sqlite3_open.open
.overrideForAll(() => DynamicLibrary.open('libsqlite3.z.so'));
sqfliteFfiInit();
// 必须使用无 isolate 工厂:sqlite3.open 的覆盖只作用于本 isolate
databaseFactory = databaseFactoryFfiNoIsolate;
final String dbDir = _pickDatabasesDir();
// setDatabasesPathOrNull 仅在具体实现类上(未导出),此处以 dynamic 调用
(databaseFactoryFfiNoIsolate as dynamic).setDatabasesPathOrNull(dbDir);
print('[db] sqflite ffi ready, databases path: $dbDir');
} catch (e) {
print('[db] sqflite ffi init failed: $e');
}
}
try {
await DbManager.instance.init().timeout(const Duration(seconds: 3));
} catch (e) {
debugPrint('DbManager init failed, skip database: $e');
}
}
平台判断保证只在无 sqflite 原生实现的平台启用 FFI。必须使用无 isolate 工厂,因为 sqlite3.open 的覆盖只作用于当前 isolate。初始化被 catch 包住并设置 3 秒超时:失败时跳过数据库,页面仍能渲染,不会出现空白屏幕。
5.1.5 选择可写的数据库目录
默认数据库路径在应用沙箱内不可写,需要显式指定:
/// 找一个应用可写的目录存放数据库文件(默认路径 .dart_tool 与 HOME 均不可写)
String _pickDatabasesDir() {
final List<String> candidates = <String>[
Directory.systemTemp.path,
Platform.environment['HOME'] ?? '',
];
// 从可执行文件路径推导应用沙箱目录:.../haps/entry/... -> .../haps/entry/files
final String exe = Platform.resolvedExecutable;
final int idx = exe.indexOf('/haps/entry');
if (idx > 0) {
candidates.add('${exe.substring(0, idx)}/haps/entry/files');
}
for (final String base in candidates) {
if (base.isEmpty) continue;
try {
final Directory dir = Directory('$base/databases')
..createSync(recursive: true);
return dir.path;
} catch (e) {
print('[db] candidate path failed: $base -> $e');
}
}
throw StateError('no writable databases path found');
}
候选目录按临时目录、HOME、从可执行文件推导的 .../haps/entry/files 依次尝试,第一个能成功创建 databases 子目录的路径生效。
5.1.6 引擎销毁与资源释放
宿主工程由 FlutterAbility 管理引擎生命周期,EntryAbility 无需手工解绑通道。数据库连接由 DbManager 单例持有,应用退出时随进程释放;示例没有提供显式关闭数据库的页面入口,接入业务如需提前释放可自行补充 close 调用。
5.2 声明宿主权限(本例无须额外权限)
示例应用不访问网络,不使用系统能力,数据库文件位于应用沙箱内。example/ohos/entry/src/main/module.json5 未声明任何 requestPermissions:
{
"module": {
"name": "entry",
"type": "entry",
"mainElement": "EntryAbility",
"deviceTypes": ["phone"],
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"exported": true
}
]
}
}
5.2.1 库本体无须权限声明
json2dart_safe 是纯 Dart 库,没有 ohos/ 模块,也不需要在任何 module.json5 中为它声明权限。
5.2.2 应用 entry 的权限场景
最终安装的是宿主应用。如果接入方在业务中引入了需要权限的能力,再在 example/ohos/entry/src/main/module.json5 的现有 module 配置中合并 requestPermissions 与 usedScene,保留原有 Ability 等配置。
权限声明和运行时授权是两个步骤。接入应用时,还需根据目标 SDK 的权限定义处理授权要求;module.json5 中的声明不会自动完成运行时授权。本例无须权限,可跳过这一步。
5.3 插件注册:纯 Dart 库无须 pluginClass
json2dart_safe 的 pubspec.yaml 没有 flutter.plugin.platforms 段,库不注册原生插件。Flutter 工具为 example 生成的 example/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets 中,注册函数当前为空实现:
export class GeneratedPluginRegistrant {
static registerWith(flutterEngine: FlutterEngine) {
try {
} catch (e) {
Log.e(
TAG,
"Tried to register plugins with FlutterEngine ("
+ flutterEngine
+ ") failed."
);
Log.e(TAG, "Received exception while registering", e);
}
}
}
执行 flutter pub get 和构建后,Flutter 工具会根据依赖自动更新该文件。通常不应手工编辑 GeneratedPluginRegistrant.ets,因为下次构建可能覆盖它。
如果业务应用同时引入了带 OHOS 原生实现的插件,注册异常的排查步骤见第九节 MissingPluginException。
5.4 检查 example 的 OHOS 应用结构
本例的 example/ohos/build-profile.json5 应在 products 中设置版本。下面是需核对的配置片段,请合并到现有工程;其中 signingConfig: "default" 需要与本机配置的签名名称一致,签名材料保留在本地。
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.1.0(18)",
"runtimeOS": "HarmonyOS"
}
],
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": ["default"]
}
]
}
]
}
}
配置后,在 DevEco Studio 中执行一次 Sync Project。如果 entry 模块正常识别,Project 视图中会出现 entry,并能打开 entry 模块的签名配置。
六、补全交付文件并提交适配分支
6.1 除代码外还要补全哪些文件
代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:
| 文件 | 应写清楚的内容 |
|---|---|
README.md / README_CN.md | 原项目说明,保留上游信息 |
README.OpenHarmony_CN.md | 简介、安装方式(pub.dev 与 git TAG)、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题 |
README.OpenHarmony.md | 与中文说明对应的英文文档 |
CHANGELOG.OpenHarmony.md | OHOS 新增能力、适配版本、兼容限制与测试范围 |
docs/ohos-adaptation-blog.md | 适配过程记录:必要性评估、路线、关键决策、构建证据与踩坑复盘 |
LICENSE | 保留上游许可证(BSD 3-Clause) |
example/README.md | 依赖方式、运行目录、签名、操作步骤与效果图 |
pubspec.yaml、example/pubspec.yaml、example/ohos/oh-package.json5 | 核对包名、版本、依赖与覆盖配置 |
.gitignore | 忽略构建缓存及本机签名材料,不漏提交必要源码和配置 |
库本身的来源与版本写入 OpenHarmony 说明文档。本例的包名为 json2dart_safe,版本为 1.6.0,采用 BSD 3-Clause 许可证;Flutter 和 HarmonyOS SDK 版本写入环境说明。
6.2 提交前检查
提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:
git branch --show-current
git diff --check
git status --short
git diff --stat
git diff
检查 diff 中的依赖和工程配置变化,移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置(含证书绝对路径与加密口令),需要在提交前从暂存内容中移除。
6.3 提交并推送到 GitHub
文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:
git add example docs .gitignore
git add README.OpenHarmony_CN.md README.OpenHarmony.md
git add CHANGELOG.OpenHarmony.md
git diff --cached --check
git diff --cached --stat
git diff --cached
git commit -m "feat(ohos): add OpenHarmony support for json2dart_safe 1.6.0"
git remote -v
git branch --show-current
git push -u origin master
git tag 1.6.0-ohos-1.0.0-beta.1
git push origin 1.6.0-ohos-1.0.0-beta.1
推送时,origin 应指向自己有写权限的仓库,本例为 GitHub 上的 fastcode555/Json2Dart_Null_Safety,工作分支为 master,tag 1.6.0-ohos-1.0.0-beta.1 标记适配基线。
推送后在 GitHub 发起 Pull Request,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。
七、使用根目录 example 演示接入
仓库自带 example/,可以直接用来调试插件和体验安全解析与数据库增删改查。
7.1 本地适配时使用路径依赖
当前 example/pubspec.yaml 的依赖是:
dependencies:
flutter:
sdk: flutter
json2dart_safe:
path: ../
json2dart_db:
path: ../json2dart_db/
sqflite: any
sqflite_common_ffi: ^2.3.6
sqlite3: ^2.4.7
dependency_overrides:
json2dart_safe:
path: ../
../ 相对于 example/pubspec.yaml 指向插件根目录,../json2dart_db/ 指向同仓库的数据库扩展子包,修改根目录插件后可直接联调。json2dart_db 依赖 hosted 版的 json2dart_safe,这里用 dependency_overrides 统一强制走本地源码,保证示例验证的就是本仓库的库代码。
7.2 通过 Git 引入插件
业务应用通过 Git 引入时,将 json2dart_safe 的 path 配置替换为下面的 Git 依赖。这里固定到本文使用的提交:
dependencies:
json2dart_safe:
git:
url: https://github.com/fastcode555/Json2Dart_Null_Safety.git
ref: 5a2400e1401e6ba569c851bb4c8a824997dde915
仓库同时提供 tag 1.6.0-ohos-1.0.0-beta.1(指向第一个适配提交)。使用自己的适配版本时,先推送分支,再将 url 改为对应仓库,ref 改为自己的 tag 或提交号。正式发布后可固定到 tag 或 commit。
从插件根目录执行:
cd example
flutter pub get
flutter pub deps
检查 example/pubspec.lock 中 json2dart_safe 的来源为 git,并核对 url、ref 和 resolved-ref。同时检查没有 dependency_overrides 或 pubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。
7.3 调用接口实现安全解析
下面的页面展示一次完整解析,可用于 example/lib/main.dart。仓库中的完整 Demo 是用户管理 CRUD,还提供数据库增删改查。
import 'package:flutter/material.dart';
import 'package:json2dart_safe/json2dart.dart';
void main() {
runApp(const MaterialApp(home: ParsePage()));
}
class ParsePage extends StatefulWidget {
const ParsePage({Key? key}) : super(key: key);
_ParsePageState createState() => _ParsePageState();
}
class _ParsePageState extends State<ParsePage> {
String _result = '';
void _parse() {
final Map<String, Object?> json = <String, Object?>{
'name': 'Flutter',
'version': '3.44',
'isStable': '1',
'tags': ['ui', 'ohos'],
};
// 类型不匹配或缺失时返回默认值,不会抛异常
final String name = json.asString('name'); // Flutter
final double version = json.asDouble('version'); // 3.44(字符串自动转 double)
final bool isStable = json.asBool('isStable'); // true(兼容 '1' 字符串)
final List<String> tags = json.asList<String>('tags') ?? <String>[];
setState(() {
_result = 'name=$name\nversion=$version\nisStable=$isStable\ntags=$tags\n\n'
'${JsonFormatter.format(json)}';
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('json2dart_safe demo')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FilledButton(
onPressed: _parse,
child: const Text('解析示例 JSON'),
),
const SizedBox(height: 16),
Text(_result),
],
),
),
);
}
}
7.4 页面退出时的资源处理
异步回调先检查 mounted,避免页面销毁后继续调用 setState。示例的数据库由 DbManager 单例持有,页面 dispose 时不关闭数据库,应用退出时随进程释放。
多个页面都需要数据库时,继续复用 DbManager.instance 单例,各页面只通过 DAO 读写,不要各自初始化连接。
八、验证、构建与鸿蒙设备运行效果
8.1 分别验证插件与 example
从插件仓库根目录执行:
flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test
接口测试应覆盖多 key 解析、二维数组、类型不匹配兜底和错误回调。仓库根目录的 test/ 包含多 key 与二维数组等解析用例;example 的 Widget 测试在宿主环境(macOS)用 FFI 提供 sqlite,先清空表再验证编辑对话框的昵称、年龄、爱好与 VIP 字段。
Dart 测试覆盖解析逻辑和页面交互;FFI 加载、随包 sqlite3 与数据库行为还需要在鸿蒙设备上验证。
8.2 确认设备连接
hdc list targets
flutter devices
设备首次连接电脑时,需要在手机端确认调试授权。列表为空时,检查 USB 连接、调试模式和电脑授权。本例使用的验证设备为 4UQ9K25508013016(OpenHarmony 6.1.1.120,API 24)。
8.3 配置签名
真机安装的 HAP 通常需要有效签名。推荐使用 DevEco Studio 为 entry 模块配置自动签名:
- 用 DevEco Studio 打开
example/ohos,不是仓库根目录; - 等待工程 Sync 成功,确认 Project 视图中存在
entry模块; - 打开 File > Project Structure > Signing Configs;
- 为
defaultproduct 选择或生成签名; - 确认设备、应用包名、证书和 Profile 匹配;
- 再回到终端执行 Flutter 构建或运行。
本例的 bundleName 为 com.example.ohos_example_scaffold,签名 Profile 与 bundleName 绑定,不匹配时 hvigor 会在签名阶段报错。签名材料保存在本机,公开仓库中只保留构建所需的通用配置。
8.4 运行示例
以下命令在 example/ 目录执行,将 <device-id> 替换为设备列表中的实际 ID:
flutter run -d <device-id>
也可以先构建 HAP(本例实际执行的命令,默认 release 模式):
flutter build hap
构建通过时输出:
✓ Built build/ohos/hap/entry-default-signed.hap (24.4MB).
典型产物位于:
example/build/ohos/hap/entry-default-signed.hap
本例产物为已签名 HAP(约 24.4 MB),可直接用于真机安装。
8.5 在设备上测试增删改查
- 打开应用,确认列表先显示加载圈,随后显示“暂无用户”;
- 点击“添加测试用户”,确认 SnackBar 提示新增成功,列表出现一条记录;
- 点击编辑图标,修改昵称、年龄、爱好与 VIP 开关,保存后确认列表刷新;
- 点击删除图标并确认,记录从列表消失;
- 退出应用再进入,确认数据仍在(数据库持久化);
- 多次添加、编辑、删除,确认无崩溃、无重复记录。
本例中,签名 HAP 构建、真机安装启动、数据库初始化与建表已通过验证;以上交互步骤可作为验收清单逐项执行。
8.6 鸿蒙设备运行效果
完成适配后,示例应用能够在鸿蒙设备上通过 FFI 直连随包 sqlite3,完成用户数据的 新增、编辑、删除 和 列表查询 四类操作。
| 添加测试用户 | 编辑用户 | 删除用户 |
|---|---|---|
| SnackBar 提示新增成功 | 对话框修改昵称/年龄/爱好/VIP | 确认后记录从列表消失 |
示例的 sqlite3 由随包 libsqlite3.z.so 提供,不依赖系统数据库服务。启动日志中的 method not implemented 警告来自 OHOS 引擎未实现的系统通道,属良性日志,不影响示例功能。不同设备的沙箱路径可能存在差异,数据库目录选择逻辑(5.1.5)需在目标设备上验证。
九、FAQ:适配过程与使用问题
9.1 Missing SDK components
典型错误如下:
Missing SDK components. SDK path: ...,
missing components: toolchains,ets,js,native,previewer.
这个错误发生在 Hvigor 同步阶段。通常需要检查构建工具使用的 SDK 路径、组件是否完整,以及 Hvigor 与 SDK 的版本是否匹配。
处理顺序:
- 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
- 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
- 避免误用
/Applications/DevEco-Studio.app/Contents/sdk之类的不完整目录; - 确认 SDK 根目录下存在
toolchains、ets、js、native、previewer; - 执行
flutter config --ohos-sdk <正确路径>; - 重新执行
flutter doctor -v和 DevEco Studio Sync。
因为 Sync 和 Compile 首先读取 Mac 本地 SDK。手机 API 版本只在部署、安装和运行时参与兼容判断。即使完全不连接手机,本地 SDK 不完整时也会得到相同错误。
当前工程的 compatibleSdkVersion 是 API 18,因此 API 24 在安装版本门槛上是满足的;json2dart_safe 本身是纯 Dart 库,无系统能力依赖,示例的数据库能力由随包 libsqlite3.z.so 提供。
9.2 DevEco Studio 中看不到 entry 模块
本仓库没有插件的 ohos/ HAR 模块,可安装应用的 entry 模块位于 example/ohos/entry。
请直接使用 DevEco Studio 打开:
Json2Dart_Null_Safety/example/ohos
如果仍看不到 entry,先解决 SDK Sync 错误,再检查 example/ohos/build-profile.json5 的 modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。
9.3 无法手动签名
签名配置依附于可构建的应用模块和 product。工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。
建议先确认:
- 打开的是
example/ohos; - SDK 组件完整并且 Sync 成功;
entry的模块类型为entry;defaultproduct 和 target 已正确关联;- 当前账号、证书和调试设备状态有效;
- 签名 Profile 与工程
bundleName(com.example.ohos_example_scaffold)匹配。
9.4 能安装但列表为空或无数据
按以下顺序检查:
- 启动日志是否出现
[db] sqflite ffi ready, databases path: ...; example/ohos/entry/libs/arm64-v8a/是否包含libsqlite3.z.so,产物 HAP 中是否随包;- 是否出现
DbManager init failed, skip database(初始化 3 秒超时被跳过,页面仍渲染但无数据); [db] sqflite ffi init failed后的异常信息(so 加载失败或数据库目录不可写);- 启动日志中的
method not implemented警告为良性日志,可先排除。
9.5 MissingPluginException
这通常表示 Dart 通道找不到已注册的原生插件。json2dart_safe 是纯 Dart 库,本身不会触发此异常;如果业务同时引入了带原生实现的其他插件,新增原生插件后需要重新构建应用。从仓库根目录执行:
cd example
flutter clean
flutter pub get
flutter run -d <device-id>
如果仍然出现,检查自动生成的插件注册文件 GeneratedPluginRegistrant.ets 中是否包含对应插件,同时核对插件的 pubspec.yaml 与 oh-package.json5。
9.6 列表出现重复数据
重点检查两处:
- “添加测试用户”每次点击都会插入一条新记录,确认没有在未完成时重复点击;
DbManager是单例(factory+_instance ??=),确认没有绕过单例多次init建立多个连接。
清理数据可调用 DAO 的 clear(example 的 Widget 测试即用 clear 保证用例可重复执行)。
9.7 编译成功但安装失败
常见原因包括:
- HAP 未签名或使用了错误的 Profile;
- 设备未加入调试设备列表;
- 包名与签名 Profile 不匹配;
- 安装包的
compatibleSdkVersion高于设备 API; - 手机上已经安装了使用不同证书签名的同包名应用。
根据安装错误码区分签名、版本和包名冲突,再处理对应配置。本工程最低兼容 API 18,验证设备 API 24 满足安装门槛。
9.8 flutter create 不认识 ohos,或包名不合法
先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。包名报错时,确认当前目录包含目标工程的 pubspec.yaml,并显式传入 --project-name;仓库名 Json2Dart_Null_Safety 不能直接作为 Dart 包名。本机 Xcode 版本过旧时,在目标目录内直接生成会触发 xcodebuild 校验失败,可改用 /tmp 脚手架加目录拷贝(见 3.4)。生成后检查 diff。
9.9 Git 依赖提示找不到分支或无权限
先检查 URL 是否指向已同步的目标仓库,再确认 ref 使用的 tag 1.6.0-ohos-1.0.0-beta.1 或提交号已推送。分支或 tag 尚未推送时,可以先使用第七节的提交号。私有仓库还需在本机配置 Git 认证。
9.10 改了本地代码,Demo 为什么没变化
先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地插件改动。本地联调切回 path: ../;测试远程版本则先提交推送,再更新依赖并核对 pubspec.lock 的 resolved-ref。example 中还有 dependency_overrides 强制 json2dart_safe 走本地源码,检查是否被覆盖。改动了随包 so 或宿主 ArkTS 后,停止应用并重新构建运行,不能只做 Dart 热重载。
9.11 首次启动后列表一直为空
main 里的 _initDatabase 会捕获数据库初始化异常并跳过数据库(日志 DbManager init failed, skip database),页面照常渲染但始终没有数据。按 9.4 的顺序检查 [db] 日志、随包 so 与可写目录;修复后完全重启应用再验证。示例为 DbManager.init 设置了 3 秒超时,避免初始化卡住阻塞启动。
相关链接
更多推荐





所有评论(0)