Flutter/OpenHarmony 三方库适配实战:sqflite 数据库
一、项目信息
| 项目 | 内容 |
|---|---|
| 仓库名称 | sqflite |
| 仓库地址 | https://atomgit.com/oh-flutter/sqflite |
| 基于版本 | sqflite 2.4.5 |
| 适配 TAG | 2.4.5-ohos.1 |
| OHOS 插件包 | sqflite_ohos |
| OHOS 原生入口 | SqflitePlugin |
| Dart 注册类 | SqfliteOhos |
| 原生执行模型 | BinaryMessenger + ThreadWorker + RDB |
sqflite是 Flutter 生态中最常用的 SQLite 插件之一。业务代码通常直接使用openDatabase()、Database.execute()、Batch和transaction(),并不关心数据库
最终由 Android 的 SQLite、iOS 的 SQLite,还是 OpenHarmony 的 RDB 承载。
因此,本次适配的核心目标不是重写一套数据库 API,而是让原有的sqflite_platform_interface契约在 OpenHarmony 上得到完整实现:
- Dart 侧继续使用
sqflite的公开 API。 - OHOS 侧使用
@ohos.data.relationalStore执行 SQL。 - 数据库路径、打开、关闭、查询、更新、删除、批处理和事务保持原有语义。
- 大 SQL 和批处理不能长时间占用平台主线程。
- 插件销毁时释放 Worker、RDB Store 和待回复消息。
环境搭建与版本
下面是本次适配实际使用的环境。环境搭建过程这里不再展开,直接看 Flutter OH 环境搭建指南。
| 项目 | 版本/说明 |
|---|---|
| Flutter OH | 3.44.9+ohos-0.0.1-canary1 |
| Dart | 3.12.2 |
| DevEco Studio | 26.0.0 |
| HarmonyOS SDK | API 26 |
| 目标仓库 | sqflite |
二、整体适配架构
本次适配分为四层:
业务 Dart 代码 | v sqflite / sqflite_common | v sqflite_platform_interface | v SqfliteOhos(Dart 插件注册) | v BinaryMessenger(未解码 ArrayBuffer) | v SqflitePlugin(主线程仅负责分发) | v SqfliteWorker(解码 SQL、调用 RDB、编码结果) | v @ohos.data.relationalStore这个结构最重要的一点是:MethodChannel的标准处理流程会在平台主线程解码MethodCall。当一次批处理包含大量 SQL 时,消息体可能非常大,主线程解码和执行
很容易触发长时间卡顿。适配版本改用BinaryMessenger.setMessageHandler()获取
未解码的ArrayBuffer,主线程只分配序号并转发消息,实际解码和数据库操作放入
ArkTS Worker。
OHOS 工程目录创建
本次适配前,上游仓库没有ohos/目录。适配分支上按 Flutter OH 的插件工程约束创建平台目录,再把 ArkTS 入口接入pubspec.yaml:
gitswitch-c<your-ohos-adaptation-branch>flutter create--platformsohos.工程生成后至少保留以下边界:
ohos/ ├── index.ets ├── oh-package.json5 └── src/main/ ├── module.json5 └── ets/components/plugin/示例工程还需要生成example/ohos/,并在插件pubspec.yaml的flutter.plugin.platforms.ohos中声明pluginClass;只创建目录、没有注册入口,Dart 方法通道仍然不会工作。
三、让 Flutter 找到 OHOS 实现
根包声明 OHOS 默认实现:
flutter:plugin:platforms:android:default_package:sqflite_androidios:default_package:sqflite_darwinmacos:default_package:sqflite_darwinohos:default_package:sqflite_ohossqflite_ohos/pubspec.yaml声明原生插件类和 Dart 注册类:
name:sqflite_ohosversion:2.4.5flutter:plugin:platforms:ohos:package:io.flutter.plugins.sqflitepluginClass:SqflitePlugindartPluginClass:SqfliteOhosDart 注册类将 OHOS 实现接入统一平台接口:
import'package:sqflite_platform_interface/sqflite_platform_interface.dart';classSqfliteOhosextendsSqflitePlatform{staticvoidregisterWith(){SqflitePlatform.initWithDatabaseFactoryMethodChannel();}}这样业务代码仍然通过同一个DatabaseFactory和Database抽象工作,不需要为
OpenHarmony 单独封装一套数据库调用。
四、主线程只做消息分发
SqflitePlugin在引擎附加时创建 Worker,并维护消息序号与回复对象的映射:
exportdefaultclassSqflitePluginimplementsFlutterPlugin{privatesqfliteWorker:worker.ThreadWorker|null=null;privatepending:Map<number,BinaryReply>=newMap<number,BinaryReply>();privateseq:number=0;onAttachedToEngine(binding:FlutterPluginBinding):void{this.messenger=binding.getBinaryMessenger();this.sqfliteWorker=newworker.ThreadWorker('./SqfliteWorker.ets',{name:'SqfliteWorker',});this.sqfliteWorker.onmessage=(e:MessageEvents):void=>{constdata=e.dataasSqfliteReply;constreply=this.pending.get(data.seq);if(reply!=null){reply.reply(data.result);this.pending.delete(data.seq);}};}}原始消息处理器只做三件事:
constrawHandler:BinaryMessageHandler={onMessage:(message:ArrayBuffer,reply:BinaryReply):void=>{constid:number=++this.seq;this.pending.set(id,reply);this.sqfliteWorker?.postMessage({seq:id,call:message,context:this.context},[message],);},};这里使用seq而不是“最新一条回复”作为关联键,可以避免并发请求互相串号。
未处理的消息不会在平台线程解码,也不会直接调用RdbStore。
引擎分离时必须终止旧 Worker 并清空回复映射,防止热重启后产生重复 Worker:
onDetachedFromEngine(binding:FlutterPluginBinding):void{this.messenger?.setMessageHandler(Constant.SQFLITE_CHANNEL,null);this.sqfliteWorker?.terminate();this.sqfliteWorker=null;this.pending.clear();}五、Worker 中完成 SQL 解码与执行
Worker 收到原始消息后,先用标准方法解码器还原MethodCall,再按方法名分发:
asyncfunctiondispatch(call:MethodCall,result:MethodResult,context:common.Context,):Promise<void>{switch(call.method){caseConstant.METHOD_OPEN_DATABASE:awaitDatabaseHelper.openDatabase(context,call,result);break;caseConstant.METHOD_GET_DATABASES_PATH:handleGetDatabasesPathCall(context,result);break;caseConstant.METHOD_EXECUTE:awaithandleExecuteCall(call,result);break;caseConstant.METHOD_INSERT:awaithandleInsertCall(call,result);break;caseConstant.METHOD_QUERY:awaithandleQueryCall(call,result);break;caseConstant.METHOD_BATCH:awaithandleBatchCall(call,result);break;caseConstant.METHOD_CLOSE_DATABASE:awaithandleCloseDatabaseCall(call,result);break;default:result.notImplemented();}}查询路径先根据路径取得对应RdbStore,再创建MethodCallOperation:
asyncfunctionhandleQueryCall(call:MethodCall,result:MethodResult,):Promise<void>{constrdbStore=DatabaseHelper.getDatabaseOrError(call,result);if(rdbStore==null){return;}constoperation=newMethodCallOperation(call,result);awaitDatabase.query(operation,rdbStore);}Operation、BatchOperation和MethodCallOperation将原库的参数转换成
RDB 的参数数组,并把查询结果转换为 Flutter 可识别的 Map/List。这个转换层是
兼容性的关键,尤其需要注意null、整数、浮点数、Blob 和字符串的类型保持。
六、数据库目录与业务用法
OHOS 实现把应用数据库目录返回给 Dart:
functionhandleGetDatabasesPathCall(context:common.Context,result:MethodResult,):void{constappContext=context.getApplicationContext();result.success(appContext.databaseDir+'/rdb');}业务代码无需改动:
import'package:path/path.dart'asp;import'package:sqflite/sqflite.dart';Future<void>demo()async{finalStringpath=p.join(awaitgetDatabasesPath(),'demo.db');finalDatabasedb=awaitopenDatabase(path,version:1,onCreate:(db,version)async{awaitdb.execute('CREATE TABLE user(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)',);},);finalint id=awaitdb.insert('user',<String,Object?>{'name':'Alice','age':18,});finalList<Map<String,Object?>>rows=awaitdb.query('user',where:'id = ?',whereArgs:<Object?>[id],);print(rows);awaitdb.close();}参数化查询必须继续使用占位符,避免通过字符串拼接生成 SQL:
awaitdb.query('user',where:'age >= ? AND name != ?',whereArgs:<Object?>[18,'Bob'],);事务中应使用回调传入的txn:
awaitdb.transaction((txn)async{awaittxn.insert('user',<String,Object?>{'name':'Tom','age':19,});awaittxn.update('user',<String,Object?>{'age':20},where:'name = ?',whereArgs:<Object?>['Tom'],);});适配过程 FAQ 与解决方案
为什么主线程只做消息分发,SQL 要放到 Worker?
解决方案:ArkTS 主线程不适合长时间执行数据库查询。把 SQL 解码和 RDB 调用放到 Worker,可以避免页面卡顿,也能让并发调用按请求隔离。
数据库打开失败时,如何区分路径和 schema 问题?
解决方案:先打印数据库目录、文件是否存在和 open 返回值,再检查建表 SQL。路径错误通常发生在首次启动,schema 错误多发生在升级或旧文件迁移。
为什么查询结果要重新构造成 sqflite 行格式?
解决方案:RDB 返回的列名、空值和 blob 类型不能直接当作 Dart 的动态 map。适配层要按列重建结果,并保留 rowid、null 和二进制值的语义。
升级 RDB SDK 后如何做回归?
解决方案:至少覆盖首次建表、升级迁移、事务、批量插入、blob/null 和进程重启。只跑一个查询成功,不能证明旧数据库仍然兼容。
七、构建、安装与模拟器验证
本次在以下环境完成验证:
- Flutter:
3.44.9+ohos-0.0.1-canary1 - OHOS SDK:
26 - DevEco Studio:
26.0.0 - 模拟器:
emulator 7.0.0.107(SP8DEVC00E999R4P11)
执行静态分析:
flutter analyze --no-pub结果:未发现问题。
示例应用已构建并安装到 OpenHarmony 模拟器。示例首页包含 Raw tests、Open tests、
Type tests、Batch tests、Todo database example、Exception tests 和 IO tests 等
入口,可以分别验证 SQL、数据库生命周期、值类型和异常处理。
截图中的Sqflite demo页面来自sqflite/example,说明 OHOS 插件已经成功注册,
Dart 示例页面可以正常加载并进入数据库测试入口。
八、需要注意的能力边界
- 当前实现聚焦 OpenHarmony RDB,不等同于桌面端对任意 SQLite 动态库的加载能力。
- 加密数据库、SQLCipher 和外部自定义 SQLite 构建不在本次适配范围内。
- Worker 能避免大 SQL 阻塞平台主线程,但业务仍应控制超大事务和不必要的全表查询。
- 返回的查询结果是跨通道序列化后的新对象,业务不应依赖原生 RDB 对象。
- 真机、多窗口、后台销毁和异常断电场景仍应在业务上线前补充测试。
如果安装 HAP 时提示9568289,应检查应用是否申请了超出当前应用等级的权限。
数据库本身不需要该权限,错误通常来自示例或宿主工程的其他权限声明。
九、代码提交与协作
本次适配已经推送到 AtomGitmain分支,并创建 TAG:
main: e7ad98de849b16038f6f5815f29891e2840a5d0e tag: 2.4.5-ohos.1业务工程可以按 README 中的 TAG 引用适配版本:
dependencies:sqflite:git:url:https://atomgit.com/oh-flutter/sqflite.gitref:2.4.5-ohos.1path:sqflite欢迎加入 CPF-Flutter 鸿蒙社区,参与
Flutter OHOS 插件适配、HAP 构建和模拟器/真机验证经验交流。
- 仓库地址:https://atomgit.com/oh-flutter/sqflite
- 提交 Issue:https://atomgit.com/oh-flutter/sqflite/issues
- 提交 Pull Request:https://atomgit.com/oh-flutter/sqflite/pulls
版本与提交基线
| 项目 | 版本/地址 |
|---|---|
| 上游包 | sqflite |
| 上游最新版本 | 2.4.5 |
| 文章目标基线 | 2.4.5-ohos.1 |
| 当前已发布 TAG | 2.4.5-ohos.1 |
| 适配分支 | main |
| 目标最新 TAG | 2.4.5-ohos.1 |
| 最终提交 | 10f256a |
版本号更新只代表文章和适配分支的基线已对齐;涉及上游 API 变更时,仍需在该 TAG 上重新运行 Dart 测试、ArkTS 编译、HAP 安装和真机回归。
参考资料和仓库地址
- 适配仓库主页
- 适配分支 main
- 当前已发布 TAG 2.4.5-ohos.1
- 目标最新 TAG 2.4.5-ohos.1
- 最终提交 10f256a
- ArkTS 通道实现
- 鸿蒙适配说明
- pub.dev 包页面
- pub.dev 2.4.5 版本页
- 上游仓库
- Flutter 官方插件与平台通道文档
- Flutter OH 鸿蒙 SDK 仓库
欢迎加入 CPF-Flutter 鸿蒙社区。