Flutter/OpenHarmony 三方库适配实战:sqflite 数据库

一、项目信息

项目内容
仓库名称sqflite
仓库地址https://atomgit.com/oh-flutter/sqflite
基于版本sqflite 2.4.2
适配 TAG2.4.2-ohos-1.0.0
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 和待回复消息。

二、整体适配架构

本次适配分为四层:

业务 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。

三、让 Flutter 找到 OHOS 实现

根包声明 OHOS 默认实现:

flutter:
  plugin:
    platforms:
      android:
        default_package: sqflite_android
      ios:
        default_package: sqflite_darwin
      macos:
        default_package: sqflite_darwin
      ohos:
        default_package: sqflite_ohos

sqflite_ohos/pubspec.yaml 声明原生插件类和 Dart 注册类:

name: sqflite_ohos
version: 2.4.2

flutter:
  plugin:
    platforms:
      ohos:
        package: io.flutter.plugins.sqflite
        pluginClass: SqflitePlugin
        dartPluginClass: SqfliteOhos

Dart 注册类将 OHOS 实现接入统一平台接口:

import 'package:sqflite_platform_interface/sqflite_platform_interface.dart';

class SqfliteOhos extends SqflitePlatform {
  static void registerWith() {
    SqflitePlatform.initWithDatabaseFactoryMethodChannel();
  }
}

这样业务代码仍然通过同一个 DatabaseFactory 和 Database 抽象工作,不需要为
OpenHarmony 单独封装一套数据库调用。

四、主线程只做消息分发

SqflitePlugin 在引擎附加时创建 Worker,并维护消息序号与回复对象的映射:

export default class SqflitePlugin implements FlutterPlugin {
  private sqfliteWorker: worker.ThreadWorker | null = null;
  private pending: Map<number, BinaryReply> = new Map<number, BinaryReply>();
  private seq: number = 0;

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.messenger = binding.getBinaryMessenger();
    this.sqfliteWorker = new worker.ThreadWorker('./SqfliteWorker.ets', {
      name: 'SqfliteWorker',
    });

    this.sqfliteWorker.onmessage = (e: MessageEvents): void => {
      const data = e.data as SqfliteReply;
      const reply = this.pending.get(data.seq);
      if (reply != null) {
        reply.reply(data.result);
        this.pending.delete(data.seq);
      }
    };
  }
}

原始消息处理器只做三件事:

const rawHandler: BinaryMessageHandler = {
  onMessage: (message: ArrayBuffer, reply: BinaryReply): void => {
    const id: 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,再按方法名分发:

async function dispatch(
  call: MethodCall,
  result: MethodResult,
  context: common.Context,
): Promise<void> {
  switch (call.method) {
    case Constant.METHOD_OPEN_DATABASE:
      await DatabaseHelper.openDatabase(context, call, result);
      break;
    case Constant.METHOD_GET_DATABASES_PATH:
      handleGetDatabasesPathCall(context, result);
      break;
    case Constant.METHOD_EXECUTE:
      await handleExecuteCall(call, result);
      break;
    case Constant.METHOD_INSERT:
      await handleInsertCall(call, result);
      break;
    case Constant.METHOD_QUERY:
      await handleQueryCall(call, result);
      break;
    case Constant.METHOD_BATCH:
      await handleBatchCall(call, result);
      break;
    case Constant.METHOD_CLOSE_DATABASE:
      await handleCloseDatabaseCall(call, result);
      break;
    default:
      result.notImplemented();
  }
}

查询路径先根据路径取得对应 RdbStore,再创建 MethodCallOperation:

async function handleQueryCall(
  call: MethodCall,
  result: MethodResult,
): Promise<void> {
  const rdbStore = DatabaseHelper.getDatabaseOrError(call, result);
  if (rdbStore == null) {
    return;
  }
  const operation = new MethodCallOperation(call, result);
  await Database.query(operation, rdbStore);
}

Operation、BatchOperation 和 MethodCallOperation 将原库的参数转换成
RDB 的参数数组,并把查询结果转换为 Flutter 可识别的 Map/List。这个转换层是
兼容性的关键,尤其需要注意 null、整数、浮点数、Blob 和字符串的类型保持。

六、数据库目录与业务用法

OHOS 实现把应用数据库目录返回给 Dart:

function handleGetDatabasesPathCall(
  context: common.Context,
  result: MethodResult,
): void {
  const appContext = context.getApplicationContext();
  result.success(appContext.databaseDir + '/rdb');
}

业务代码无需改动:

import 'package:path/path.dart' as p;
import 'package:sqflite/sqflite.dart';

Future<void> demo() async {
  final String path = p.join(await getDatabasesPath(), 'demo.db');
  final Database db = await openDatabase(
    path,
    version: 1,
    onCreate: (db, version) async {
      await db.execute(
        'CREATE TABLE user(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)',
      );
    },
  );

  final int id = await db.insert('user', <String, Object?>{
    'name': 'Alice',
    'age': 18,
  });

  final List<Map<String, Object?>> rows = await db.query(
    'user',
    where: 'id = ?',
    whereArgs: <Object?>[id],
  );
  print(rows);

  await db.close();
}

参数化查询必须继续使用占位符,避免通过字符串拼接生成 SQL:

await db.query(
  'user',
  where: 'age >= ? AND name != ?',
  whereArgs: <Object?>[18, 'Bob'],
);

事务中应使用回调传入的 txn:

await db.transaction((txn) async {
  await txn.insert('user', <String, Object?>{
    'name': 'Tom',
    'age': 19,
  });
  await txn.update(
    'user',
    <String, Object?>{'age': 20},
    where: 'name = ?',
    whereArgs: <Object?>['Tom'],
  );
});

七、构建、安装与验证

执行静态分析:

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 示例页面可以正常加载并进入数据库测试入口。

八、需要注意的能力边界

  1. 当前实现聚焦 OpenHarmony RDB,不等同于桌面端对任意 SQLite 动态库的加载能力。
  2. 加密数据库、SQLCipher 和外部自定义 SQLite 构建不在本次适配范围内。
  3. Worker 能避免大 SQL 阻塞平台主线程,但业务仍应控制超大事务和不必要的全表查询。
  4. 返回的查询结果是跨通道序列化后的新对象,业务不应依赖原生 RDB 对象。
  5. 真机、多窗口、后台销毁和异常断电场景仍应在业务上线前补充测试。

如果安装 HAP 时提示 9568289,应检查应用是否申请了超出当前应用等级的权限。
数据库本身不需要该权限,错误通常来自示例或宿主工程的其他权限声明。

九、代码提交与协作

本次适配已经推送到 AtomGit main 分支,并创建 TAG:

main: e7ad98de849b16038f6f5815f29891e2840a5d0e
tag:  2.4.2-ohos-1.0.0

业务工程可以按 README 中的 TAG 引用适配版本:

dependencies:
  sqflite:
    git:
      url: https://atomgit.com/oh-flutter/sqflite.git
      ref: 2.4.2-ohos-1.0.0
      path: sqflite

欢迎加入 CPF-Flutter 鸿蒙社区,参与
Flutter OHOS 插件适配、HAP 构建和模拟器/真机验证经验交流。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐