Flutter 社区地址: https://atomgit.com/CPF-Flutter/flutter_flutter
适配后仓库地址:https://atomgit.com/oh-flutter/fast_contacts

本文记录了将开源 Flutter 三方库 fast_contacts 适配到 OpenHarmony / HarmonyOS 平台的完整过程,
包含适配思路、代码改动对照、关键决策和踩坑复盘。


一、背景

1.1 三方库简介

fast_contacts 是一个 Flutter 社区广泛使用的联系人读取插件,提供以下能力:

  • 全量联系人高效读取:可在合理时间内读取整个通讯录(读取 1000 个联系人通常约 200ms 以内,视设备而定)
  • 字段级裁剪:通过 fields 参数只读取需要的字段,字段越少加载越快
  • 批量分页加载:内部按 batchSize 分批拉取,降低 UI 卡顿风险
  • 单联系人查询与头像获取:按 ID 查询单个联系人详情、获取缩略图/全尺寸头像

该三方库最初支持 Android / iOS 两个平台,本次任务将其适配到 OpenHarmony / HarmonyOS 平台。

项目地址https://atomgit.com/oh-flutter/fast_contacts

1.2 适配目标

维度要求
功能一致性5 个原生方法(fetchAllContacts / getAllContactsPage / clearFetchedContacts / getContactImage / getContact)全部打通,返回结构与 Android/iOS 一致
Dart 层零改动Dart API、数据模型(Contact/Phone/Email/StructuredName/Organization)完全不变
性能保留批量分页语义,字段按需裁剪,避免一次性全量序列化
工程规范遵循 CPF-Flutter 插件模板规范:ohos/ HAR 结构、GeneratedPluginRegistrant 自动注册、单次合并提交

二、适配路线图

整个适配分为 4 个阶段:

第 1 阶段:项目初始化 ── 用 flutter create 生成 ohos 平台模板并清理模板噪音文件
第 2 阶段:原生实现   ── 用 @kit.ContactsKit 实现 5 个方法,完成 MethodChannel 注册与字段映射
第 3 阶段:三方库注册 ── 在 pubspec.yaml 添加 ohos 平台配置,接入 GeneratedPluginRegistrant
第 4 阶段:示例验证   ── 生成 example/ohos 宿主工程,补齐运行时权限通道,真机 flutter run 回归验证

三、逐步适配过程

第 1 阶段:项目初始化

使用 Flutter 命令行生成 OHOS 模板:

flutter create . --template=plugin --platforms=ohos

该命令会自动生成 ohos/ 目录的标准模板结构,包含必要的构建配置和入口文件。

目录结构:

ohos/
├── index.ets                              # 模块入口,导出插件类
├── oh-package.json5                       # 包配置
├── build-profile.json5                    # 构建配置
├── src/main/
│   ├── module.json5                       # HAR 模块配置(含 READ_CONTACTS 权限)
│   ├── resources/base/element/string.json # 权限 reason 资源
│   └── ets/components/plugin/
│       └── FastContactsPlugin.ets         # 原生插件实现(核心)

关键配置文件:

index.ets(入口导出文件)

import FastContactsPlugin from './src/main/ets/components/plugin/FastContactsPlugin';
export default FastContactsPlugin;

oh-package.json5(包配置)

{
  "name": "fast_contacts",
  "version": "1.0.0",
  "main": "index.ets",
  "license": "Apache-2.0",
  "dependencies": {}
}

@ohos/flutter_ohos 由 Flutter 引擎在构建时自动链接,无需在 dependencies 中显式声明。

module.json5(HAR 模块配置)

{
  "module": {
    "name": "fast_contacts",
    "type": "har",
    "deviceTypes": ["default", "tablet"],
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_CONTACTS",
        "reason": "$string:read_contacts_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

注意flutter create 会顺带生成一批与库原有结构冲突的模板噪音文件(如 lib/ 下新式 platform interface、android/build.gradle.ktsios/Classes 占位实现等),需要清理,避免破坏原有 Android/iOS 工程。


第 2 阶段:原生实现(核心)

这是适配的核心工作。将 Android 平台的 Kotlin 实现逐一翻译为 ArkTS。

2.1 整体架构对比

fast_contacts 属于方法调用型插件(MethodChannel + MethodCallHandler):Dart 主动调用原生方法,原生返回结果。

 Android (Kotlin)                          OHOS (ArkTS)
 ────────────────────                      ────────────────────
 class FastContactsPlugin                  class FastContactsPlugin
   implements FlutterPlugin,                 implements FlutterPlugin,
              MethodCallHandler                            MethodCallHandler,
                                                    AbilityAware
   import io.flutter.embedding.engine.plugins.FlutterPlugin
   import io.flutter.plugin.common.MethodChannel           import { FlutterPlugin,
   import android.provider.ContactsContract                FlutterPluginBinding,
   import androidx.core.content.ContentResolverCompat      MethodCall,
   import java.util.concurrent.*                           MethodCallHandler,
                                                           MethodChannel,
                                                           MethodResult,
                                                           AbilityAware,
                                                           AbilityPluginBinding
                                                         } from '@ohos/flutter_ohos'
   ContentResolver + Cursor                    contact.queryContacts(context)
2.2 通道注册
平台代码
AndroidMethodChannel(flutterPluginBinding.binaryMessenger, "com.github.s0nerik.fast_contacts")
OHOSnew MethodChannel(binding.getBinaryMessenger(), "com.github.s0nerik.fast_contacts")

差异:两侧通道名必须完全一致(com.github.s0nerik.fast_contacts),这是 Dart 与原生之间的通信契约。

2.3 原生方法实现对照

fast_contacts 共有 5 个原生方法,逐一对照如下:

方法Android 实现OHOS 实现返回值
fetchAllContactsContentResolver 多线程并发查询 4 类 Data(姓名/组织/电话/邮箱),CountDownLatch 合并contact.queryContacts(context) Promise 查询全部,插件侧按 fields 过滤{count, timeMillis}
getAllContactsPage从内存 allContacts 切片,按选中字段 asMap从内存 allContacts 切片,contactToMap(contact, selectedFields)List<Map>
clearFetchedContacts清空缓存与字段集清空缓存与字段集null
getContactImage查询 Photo/DisplayPhoto 目录,读取 blob 字节查询 ATTR_PORTRAITfs.openSync 读取 uri 指向文件字节Uint8List?
getContact按 id 过滤查询,合并各 partqueryContacts 后按 id 匹配,contactToMap(c, fields)Map?

fetchAllContacts 核心实现(OHOS ArkTS):

private queryContacts(attrs: contact.ContactAttributes): Promise<Array<contact.Contact>> {
  const context = this.ability?.context;
  if (context == null) {
    return Promise.reject(new Error("Ability is null"));
  }
  // ContactsKit queryContacts 的 attrs 参数在部分系统版本上报 401 参数错误,
  // 因此直接查询全部字段,由插件侧按 selectedFields 做字段过滤。
  return contact.queryContacts(context).then((contacts) => {
    return contacts;
  }).catch((err: Error) => {
    console.error("FastContactsPlugin: queryContacts failed, err=" + JSON.stringify(err));
    throw err;
  });
}

contactToMap 字段过滤(ArkTS):

private contactToMap(contactItem: contact.Contact, fields: Array<string>): Record<string, Object> {
  const map: Record<string, Object> = {};
  map["id"] = String(contactItem.id ?? -1);
  const fieldSet = new Set<string>(fields);
  const wantsName: boolean = fieldSet.has("displayName") || fieldSet.has("namePrefix")
    || fieldSet.has("givenName") || fieldSet.has("middleName")
    || fieldSet.has("familyName") || fieldSet.has("nameSuffix");
  const wantsOrganization: boolean = fieldSet.has("company") || fieldSet.has("department")
    || fieldSet.has("jobDescription");
  const wantsPhones: boolean = fieldSet.has("phoneNumbers") || fieldSet.has("phoneLabels");
  const wantsEmails: boolean = fieldSet.has("emailAddresses") || fieldSet.has("emailLabels");
  // ... 按 wantsXxx 决定是否填充 phones / emails / structuredName / organization
}

关键差异点:Android 用 ContactsContract.Data 按 MIMETYPE 分四类查询并合并;OHOS ContactsKit 一次 queryContacts 返回完整 Contact 对象(含 name / organization / phoneNumbers / emails / portrait),无需并发合并,架构更简单。但 ContactsKit 的 Organization 类没有 department 字段(仅 name 与 title),department 只能返回空字符串。

2.4 实现差异详解

字段过滤策略:Android/iOS 通过原生投影(projection)与 keysToFetch 在原生层裁剪字段;OHOS ContactsKit 的 queryContactsContactAttributes 参数在部分系统版本上会返回 401 参数错误,因此选择在插件侧过滤:

方案优点缺点
插件侧过滤(采用)兼容性最好,所有系统版本可用;Dart 层契约不变原生层仍读取全部字段,超大通讯录性能略低于 Android/iOS
原生层传 ContactAttributes字段级裁剪,性能最优部分系统版本报 401 参数错误,不可靠

第 3 阶段:三方库注册

pubspec.yaml 中添加 OHOS 平台注册:

flutter:
  plugin:
    platforms:
      android:
        package: com.github.s0nerik.fast_contacts
        pluginClass: FastContactsPlugin
      ios:
        pluginClass: FastContactsPlugin
      ohos:                              # ← 新增
        pluginClass: FastContactsPlugin  # ← 对应 index.ets 默认导出

Flutter 的 OHOS 引擎在构建时会读取 pubspec.yaml 中的 ohos 配置,自动加载 ohos/index.ets 中导出的插件类,无需手写注册代码。


第 4 阶段:示例应用创建

example/ 目录下用 Flutter 官方命令生成 OHOS 宿主工程(注意:在 example 目录执行,而非插件根目录):

cd example
flutter create . --platforms=ohos

该命令自动生成 example/ohos/ 目录,包含签名配置、SDK 版本、测试模块与 Flutter 运行时资源:

example/ohos/
├── AppScope/app.json5                     # 应用配置
├── build-profile.json5                   # 项目构建配置(含 signingConfigs、SDK 版本)
├── hvigor/hvigor-config.json5            # 构建工具配置
├── oh-package.json5                      # 顶层包配置
├── hvigorfile.ts                         # 构建入口
├── entry/
│   ├── build-profile.json5
│   ├── oh-package.json5
│   ├── src/main/
│   │   ├── module.json5                  # entry 模块配置
│   │   ├── ets/
│   │   │   ├── entryability/
│   │   │   │   └── EntryAbility.ets      # Ability 生命周期 + 权限通道
│   │   │   ├── pages/
│   │   │   │   └── Index.ets             # UI 页面(Flutter 容器)
│   │   │   └── plugins/
│   │   │       └── GeneratedPluginRegistrant.ets  # 自动注册插件(无需手写)
│   │   └── resources/
│   │       └── rawfile/flutter_assets/   # Flutter 运行时资源(kernel_blob 等)
│   └── src/ohosTest/                     # 测试目录

说明GeneratedPluginRegistrant.ets 由 Flutter 工具根据 pubspec.yamlohos 配置自动生成,会 import FastContactsPlugin from 'fast_contacts'flutterEngine.getPlugins()?.add(new FastContactsPlugin()),无需手写。

补充权限通道:example 的 Dart 端通过 MethodChannel('contacts_permission') 申请运行时权限,Android/iOS 原生端已有实现,但 OHOS 端缺失。因此在本阶段为 EntryAbility.ets 补上基于 abilityAccessCtrl 的权限申请通道:

private async requestContactsPermission(result: MethodResult): Promise<void> {
  const atManager = abilityAccessCtrl.createAtManager();
  const tokenID = this.context.applicationInfo.accessTokenId;
  if (atManager.checkAccessTokenSync(tokenID, READ_CONTACTS_PERMISSION)
    == abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
    result.success(true);
    return;
  }
  const requestResult = await atManager.requestPermissionsFromUser(
    this.context, [READ_CONTACTS_PERMISSION]
  );
  const granted = requestResult.authResults.length > 0 && requestResult.authResults[0] == 0;
  result.success(granted);
}
第 5 阶段:构建验证(HAP)

用 Flutter 工具链构建 HAP,验证原生代码与配置无编译错误:

cd example
flutter build hap --debug

成功产出:example/build/ohos/hap/entry-default-signed.hap


四、完整代码对照

4.1 Android vs OHOS 完整实现对照
维度Android (Kotlin)OHOS (ArkTS)
语言KotlinArkTS (TypeScript 语法)
插件基类FlutterPlugin, MethodCallHandler, LifecycleOwnerFlutterPlugin, MethodCallHandler, AbilityAware
获取 Ability 上下文flutterPluginBinding.applicationContextbinding.getAbility().context(AbilityAware)
联系人查询ContentResolverCompat.query + Cursor 遍历contact.queryContacts(context) 返回 Promise<Array<Contact>>
并发模型每类 Data 一个单线程 Executor + CountDownLatchPromise 链式异步,无手动线程管理
头像读取ContentUris.withAppendedId + Photo 目录portrait.uri + fs.openSync 读取字节
字段过滤Cursor 投影(projection)插件侧 contactToMap(contact, fields) 按字段集过滤
权限声明AndroidManifest.xmlmodule.json5 requestPermissions
4.2 关键 ArkTS 语法差异
Android 语法ArkTS 语法备注
import io.flutter.embedding.engine.plugins.FlutterPluginimport { FlutterPlugin } from '@ohos/flutter_ohos'OHOS 使用模块化导入
MethodChannel(binaryMessenger, name)new MethodChannel(binding.getBinaryMessenger(), name)接口一一对应
result.success(map)result.success(record)对象字面量须显式类型标注(Record<string, Object>),否则报 arkts-no-untyped-obj-literals
ContentResolver + Cursor@kit.ContactsKit + Promise无需手动管理游标/线程
ContactsContract.Data 按 MIMETYPE 分类Contact 对象内聚字段OHOS 单次查询返回完整对象
枚举 ContactField字符串字段名 + Set 判断与 Dart 侧字段名保持一致

五、关键决策说明

决策 1:保持通道名与方法契约不变

Dart 层 MethodChannel('com.github.s0nerik.fast_contacts') 已固定,OHOS 原生侧必须使用完全相同的通道名与方法名。通道名是 Dart 与原生之间的通信契约,任何一侧修改都会导致 MissingPluginException 或调用失败。

维护策略:新增方法时 Dart 与 OHOS 两侧同步实现,保持方法名、参数与返回值结构一致。

决策 2:Dart 层零改动

Dart API(FastContacts.getAllContacts / getContact / getContactImage)与数据模型(Contact/Phone/Email/StructuredName/Organization)完全复用,OHOS 适配只新增原生实现。这保证了同一份代码在 Android/iOS/OHOS 三端行为一致。

维护策略:Dart 层不感知平台差异;平台差异全部收敛到原生侧。

决策 3:字段过滤下沉到插件侧(兼容 401)

ContactsKit 的 queryContactsContactAttributes 参数在部分系统版本上返回 401 参数错误(Mandatory parameters are left unspecified),且 new contact.ContactAttributes() 在运行时抛 Constructor is false。为兼容性,选择 queryContacts(context) 查询全部字段,再由 contactToMap(contact, fields) 按选中字段过滤返回。

维护策略:若未来系统版本修复 attrs 参数问题,可改为原生层字段裁剪以提升性能;当前方案保证所有版本可用。

决策 4:运行时权限申请通道放在 example 侧

ohos.permission.READ_CONTACTS 属于 user_grant 权限,必须运行时弹窗申请。example 的 Dart 端已有 contacts_permission 通道调用约定(Android/iOS 原生侧已实现),OHOS 端在 EntryAbility.ets 中实现同通道名方法,复用 Dart 侧 _requestContactsPermission() 逻辑,Dart 代码零改动。

维护策略:权限申请逻辑集中在 example 宿主工程;插件库本身不负责权限申请,符合 fast_contacts 上游设计(不依赖 permission_handler)。

决策 5:适配完成只提交一次

遵循 CPF-Flutter 插件适配规范,所有平台新增、原生实现、权限配置、文档、修复在适配完成后合并为单次提交(squash),保持上游仓库历史干净。

维护策略:后续迭代按 issue/PR 粒度单独提交。


六、测试与验证

测试环境
项目版本
Flutter3.41.10-ohos-1.0.0(channel: user-branch,基于 CPF-Flutter/flutter_flutter)
Dart3.11.5
HarmonyOS SDK26.0.0(compatibleSdkVersion 5.1.0(18),targetSdkVersion 26.0.0)
IDEDevEco Studio 26.0.0(build DS-261.23567.138.36.2600821)
设备 ROMOpenHarmony 7.0.0.105(API 26)

版本获取方式:

版本项获取方式
Flutter / Dartflutter --version
HarmonyOS SDK读取 example/ohos/build-profile.json5compatibleSdkVersion / targetSdkVersion(或 ~/Library/OpenHarmony/Sdk/<version>/ 目录名)
IDEmacOS:defaults read /Applications/DevEco-Studio.app/Contents/Info.plist CFBundleShortVersionString
设备 ROMhdc shell param get const.product.software.version(先 hdc list targets 确认设备连接)
验证要点
  1. 插件注册GeneratedPluginRegistrant.ets 正确导入并注册 FastContactsPlugin,hilog 显示 Adding plugin: FastContactsPlugin
  2. 权限申请 — 点击 Load contacts 触发 contacts_permission 通道,abilityAccessCtrl.requestPermissionsFromUser 弹出系统授权框,授权成功
  3. 全量联系人读取getAllContacts 返回 3 个联系人,耗时 46–110ms(含查询与分页拉取)
  4. 字段过滤 — 仅选择 phoneNumberscontactToMap 按选中字段裁剪返回,未选字段为空
  5. 分页与清理getAllContactsPage 按 batchSize 分批返回;clearFetchedContacts 清理缓存
  6. 单联系人查询getContact 返回完整 JSON(id/phones/emails/structuredName),Took 65ms
  7. 头像加载getContactImage 调用链正常,portrait.uri 字节读取无异常
  8. 构建验证flutter build hap --debug 成功产出 entry-default-signed.hap

七、运行效果

真机验证中,示例应用加载联系人列表正常:点击 “Load contacts” 后约百毫秒内完成 3 个联系人的读取与展示(联系人名称、电话、邮箱均正确渲染);点击联系人条目进入详情页,单联系人查询在 65ms 内返回完整数据。

image-20260907193638657


八、遗留问题与改进方向

踩坑复盘

适配中遇到的实际问题最有价值,以表格复盘:

踩坑点现象 / 报错根因与解法
模板噪音文件flutter create --template=plugin --platforms=ohos 生成 lib/ 下新式 platform interface、android/build.gradle.ktsios/Classes 占位实现模板生成内容与库原有结构(旧式 plugin + gradle)冲突,且引用未声明的依赖;清理所有噪音文件,仅保留 ohos/example/ohos/
user_grant 权限配置构建报错:hap 模块 requestPermissions 的 READ_CONTACTS 缺少 reason/usedSceneuser_grant 权限必须带 reason$string:xxx 资源引用)与 usedScene;补齐 module.json5 与 string.json 资源
ContactAttributes 构造运行时 Constructor is falseArkTS 中 new contact.ContactAttributes() 运行时不可构造,改用对象字面量 { attributes: [...] }
queryContacts 401contact.queryContacts(context, undefined, attrs) 返回 401 Mandatory parameters are left unspecified部分系统版本不接受显式传 undefined/null holder 或 attrs 参数;改用 queryContacts(context) 查询全部字段,插件侧过滤
ArkTS 对象字面量编译报 arkts-no-untyped-obj-literalsArkTS 禁止无类型对象字面量;所有返回值显式声明 Record<string, Object> / Record<string, string> 类型
ArkTS 隐式 any编译报 arkts-no-any-unknowncall.argument('fields') 返回 Any 需显式断言为 Array<string> | null
权限通道缺失example 点击 Load contacts 报 Failed to get contacts: null(MissingPluginException)example 的 contacts_permission 通道在 OHOS 端无实现;在 EntryAbility.ets 中实现 requestContactsPermission 方法
签名材料入库构建产物 build-profile.json5 含 certpath/keyPassword 等签名材料签名是本地开发配置,不应提交;提交前还原为 signingConfigs: [],本地保留签名配置
构建产物入库rawfile/buildinfo.json5flutter_assets/ 出现在 git status构建产物应忽略;example/ohos/.gitignore 追加 **/src/main/resources/rawfile/
已知问题
  1. organization.department 返回空字符串 — HarmonyOS ContactsKit 的 Organization 类仅提供 name 与 title 字段,没有部门字段,无法获取部门信息,与 Android/iOS 行为存在差异。
  2. fields 参数不在原生层裁剪字段 — 因 queryContacts attrs 参数 401 兼容问题,原生层查询全部字段后由插件侧过滤,超大通讯录场景下全量读取性能略低于 Android/iOS。
未来优化
  • 原生层字段裁剪 — 待 ContactsKit queryContacts 的 attrs 参数在目标系统版本稳定支持后,将字段过滤下沉到原生层,进一步提升大数据量场景性能。
  • 头像读取优化 — 当前通过 fs.openSync 读取 portrait.uri 字节,后续可评估使用 image 解码与缩略图缓存,减少重复读取开销。

九、总结

将一个 Flutter 三方库适配到 OHOS 平台,核心路径可以概括为 三步走

  1. 找对应 ── 找到 OHOS 对每个 Android 原生 API 的等价实现(ContentResolver + Cursor → @kit.ContactsKit + Promise)
  2. 保契约 ── 确保方法通道名、方法名、参数与返回值结构完全一致(com.github.s0nerik.fast_contacts 与 5 个方法逐一对应)
  3. 补缺口 ── 对 OHOS 不提供的 API 用合理方案弥补(无 department 字段返回空串、attrs 401 降级为插件侧过滤)

对于 fast_contacts 三方库,适配涉及 48 个文件的新增与少量修改。Dart 层和其他平台的代码完全不受影响——这正是 Flutter 跨平台三方库生态的魅力所在:同一份 Dart 代码,Android、iOS、OpenHarmony 三端原生实现各显神通,上层 API 契约恒定。


参考文档

Logo

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

更多推荐