开发工具: 华为云码道

本文配套仓库: fastcode555/Json2Dart_Null_Safety

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 扩展调用 asStringasIntasBool 等方法解析 JSON 字段,库在类型不匹配或字段缺失时返回默认值而不是抛出异常。业务层不需要自己编写 try/catch 兜底,也不需要针对不同后端的字段风格各写一套解析代码。

例如,后端把 VIP 标志有时返回 true、有时返回 1、有时返回 'true'asBool / asBools 都能正确处理;字段名不固定时(iduser_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 SDK3.44.9+ohos-0.0.1-canary1Flutter 编译与 OHOS 平台工具链
Flutter 分支user-branch(gitcode.com/CPF-Flutter/flutter_flutter)CPF-Flutter 对应开发分支
Dart SDK3.12.2Dart 语言与包管理环境
HarmonyOS 开发套件26.0.0(API 26)开发套件版本及对应的 API 级别
compileSdkVersion工程未显式声明编译时使用本机 API 26 SDK
targetSdkVersion工程未显式声明应用面向的行为版本
compatibleSdkVersion5.1.0(18)当前工程声明的最低兼容版本
插件版本1.6.0pubspec.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 没有显式声明 compileSdkVersiontargetSdkVersion,编译行为由本机 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.yamlLICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在同一平台,也可以通过 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.yamllib/example/。Git 仓库名是 Json2Dart_Null_Safety,Dart 包名是 json2dart_safe

需要使用与本文相同的代码版本时,在没有未提交修改的仓库中切换到以下提交:

git switch --detach 5a2400e1401e6ba569c851bb4c8a824997dde915

适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
图 1:在宿主机终端输入 GitHub 仓库拉取命令。

3.3 在仓库根目录创建适配分支

接着在 Json2Dart_Null_Safety/ 根目录创建分支。分支名统一使用 feat/ohos_库名称_版本号,库名称取 pubspec.yamlname,版本号取此次适配的基线版本。本例为:

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 校验失败;可以改用脚手架拷贝法:在 /tmpflutter create --template=app --platforms=ohos <name> 生成应用脚手架,只把其中的 ohos/ 目录拷贝到 example/ 下,并删除 ohosTestnode_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提供 asStringasBoolasList 等安全转换扩展
lib/src/json2dart.dart声明解析错误回调的单例入口
lib/src/json_formatter.dart将 JSON 输出为带缩进的格式化文本
EntryAbility.ets挂载 Flutter 引擎并注册插件
示例 entry module.json5声明宿主应用 Ability 与设备类型(本例无权限声明)
example/lib/main.dartFFI 数据库初始化与应用入口
example/lib/pages/user_page.dart展示用户列表、添加、编辑和删除

四、Dart 接口与数据流分析

OHOS 适配需要遵循 Dart 层已有的方法和行为约定。先阅读 lib/json2dart.dartlib/src/json_parse_utils.dartlib/src/json_formatter.dartjson2dart_safe 是纯 Dart 库,没有平台通道,也没有需要对应的原生实现。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。

本例的对应关系如下:

Dart 入口或模型实现位置OHOS 表现应保持的行为
asString / asInt / asDouble / asBoollib/src/json_parse_utils.dart纯 Dart 直接运行类型不匹配或缺失时返回默认值,不抛异常
asStrings / asInts / asBools 等多字段解析lib/src/json_parse_utils.dart纯 Dart 直接运行多 key 依次选取,bool 优先取本身就是 bool 的字段
asList / asArray2d / asBeanlib/src/json_parse_utils.dart纯 Dart 直接运行支持 toBean 转换,JSON 字符串自动解码
Json2Dart.instance.addCallbacklib/src/json2dart.dart纯 Dart 直接运行解析失败触发全局回调,便于日志采集
JsonFormatter.formatlib/src/json_formatter.dart纯 Dart 直接运行输出带缩进的格式化文本
json2dart_dbBaseDao / BaseDbModel同仓库 json2dart_db/ 子包FFI 直连随包 sqlite3数据库读写与 Model 转换行为一致

这些方法全部由 Dart 实现,OHOS 平台不需要补对应的原生分支;适配的重点是保证 Dart 层 API 零改动,并打通 example 的运行链路。

4.1 跨端架构与调用时序

库本体不经过平台通道。example 的数据链路是:

  1. json2dart_safe 负责 Model 与 Map 的双向安全转换;
  2. json2dart_db 的 DAO 通过 sqflite 读写数据库;
  3. 在 OHOS 上,sqflite 没有原生实现,由 sqflite_common_ffi 直连随包的 libsqlite3.z.so

Flutter 页面 UserPage

UserDao json2dart_db

DbManager sqflite

sqflite_common_ffi

libsqlite3.z.so 随包 NDK 交叉编译

user_demo.db 应用沙箱

json2dart_safe asXxx / put

UserModel

json2dart_safe 在这条链路中负责 Model 与 Map 的双向转换;数据库读写由 FFI 完成,不经过平台通道。

4.1.1 一次完整新增用户的时序
libsqlite3.z.so sqflite FFI UserDao UserPage libsqlite3.z.so sqflite FFI UserDao UserPage insert(user) user.toJson()(put 跳过 null) insert into tb_user sqlite3 执行 SQL rowid userId queryAll() select tb_user List<Map> UserModel.fromJson(asInt / asString / asBool) List<UserModel> 刷新列表

4.2 数据模型:example/lib/models/user_model.dart

json2dart_dbBaseDaoMap 读写数据,user_model.dartjson2dart_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 / asNum0
asDouble0.0
asBoolfalse
asList / asArray2d / asBeannull

即使后端新增或改变字段类型,现有解析逻辑也不会抛异常,解析失败会走 print 与回调通知。数据库中的 is_vip'true' / 'false' 字符串存储、hobbies 等列表以 JSON 字符串存储,asBoolasList 都能兼容这些形态。

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 单字段版兼容 bool1 / 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。下面列出宿主侧的主要文件,完整工程还包含 AppScoperesources 等应用配置。

宿主入口位于:

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 配置中合并 requestPermissionsusedScene,保留原有 Ability 等配置。

权限声明和运行时授权是两个步骤。接入应用时,还需根据目标 SDK 的权限定义处理授权要求;module.json5 中的声明不会自动完成运行时授权。本例无须权限,可跳过这一步。

5.3 插件注册:纯 Dart 库无须 pluginClass

json2dart_safepubspec.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.mdOHOS 新增能力、适配版本、兼容限制与测试范围
docs/ohos-adaptation-blog.md适配过程记录:必要性评估、路线、关键决策、构建证据与踩坑复盘
LICENSE保留上游许可证(BSD 3-Clause)
example/README.md依赖方式、运行目录、签名、操作步骤与效果图
pubspec.yamlexample/pubspec.yamlexample/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_safepath 配置替换为下面的 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.lockjson2dart_safe 的来源为 git,并核对 urlrefresolved-ref。同时检查没有 dependency_overridespubspec_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 模块配置自动签名:

  1. 用 DevEco Studio 打开 example/ohos,不是仓库根目录;
  2. 等待工程 Sync 成功,确认 Project 视图中存在 entry 模块;
  3. 打开 File > Project Structure > Signing Configs
  4. default product 选择或生成签名;
  5. 确认设备、应用包名、证书和 Profile 匹配;
  6. 再回到终端执行 Flutter 构建或运行。

本例的 bundleNamecom.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 在设备上测试增删改查

  1. 打开应用,确认列表先显示加载圈,随后显示“暂无用户”;
  2. 点击“添加测试用户”,确认 SnackBar 提示新增成功,列表出现一条记录;
  3. 点击编辑图标,修改昵称、年龄、爱好与 VIP 开关,保存后确认列表刷新;
  4. 点击删除图标并确认,记录从列表消失;
  5. 退出应用再进入,确认数据仍在(数据库持久化);
  6. 多次添加、编辑、删除,确认无崩溃、无重复记录。

本例中,签名 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 的版本是否匹配。

处理顺序:

  1. 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
  2. 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
  3. 避免误用 /Applications/DevEco-Studio.app/Contents/sdk 之类的不完整目录;
  4. 确认 SDK 根目录下存在 toolchainsetsjsnativepreviewer
  5. 执行 flutter config --ohos-sdk <正确路径>
  6. 重新执行 flutter doctor -v 和 DevEco Studio Sync。
为什么连接 API 24 手机仍然会报这个错误?

因为 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.json5modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。

9.3 无法手动签名

签名配置依附于可构建的应用模块和 product。工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。

建议先确认:

  • 打开的是 example/ohos
  • SDK 组件完整并且 Sync 成功;
  • entry 的模块类型为 entry
  • default product 和 target 已正确关联;
  • 当前账号、证书和调试设备状态有效;
  • 签名 Profile 与工程 bundleNamecom.example.ohos_example_scaffold)匹配。

9.4 能安装但列表为空或无数据

按以下顺序检查:

  1. 启动日志是否出现 [db] sqflite ffi ready, databases path: ...
  2. example/ohos/entry/libs/arm64-v8a/ 是否包含 libsqlite3.z.so,产物 HAP 中是否随包;
  3. 是否出现 DbManager init failed, skip database(初始化 3 秒超时被跳过,页面仍渲染但无数据);
  4. [db] sqflite ffi init failed 后的异常信息(so 加载失败或数据库目录不可写);
  5. 启动日志中的 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.yamloh-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.lockresolved-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 秒超时,避免初始化卡住阻塞启动。


相关链接

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐