Flutter 三方库 fast_contacts 的 OpenHarmony 适配实战
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 平台。
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.kts、ios/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 通道注册
| 平台 | 代码 |
|---|---|
| Android | MethodChannel(flutterPluginBinding.binaryMessenger, "com.github.s0nerik.fast_contacts") |
| OHOS | new MethodChannel(binding.getBinaryMessenger(), "com.github.s0nerik.fast_contacts") |
差异:两侧通道名必须完全一致(
com.github.s0nerik.fast_contacts),这是 Dart 与原生之间的通信契约。
2.3 原生方法实现对照
fast_contacts 共有 5 个原生方法,逐一对照如下:
| 方法 | Android 实现 | OHOS 实现 | 返回值 |
|---|---|---|---|
fetchAllContacts | ContentResolver 多线程并发查询 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_PORTRAIT,fs.openSync 读取 uri 指向文件字节 | Uint8List? |
getContact | 按 id 过滤查询,合并各 part | queryContacts 后按 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 的 queryContacts 传 ContactAttributes 参数在部分系统版本上会返回 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.yaml的ohos配置自动生成,会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) |
|---|---|---|
| 语言 | Kotlin | ArkTS (TypeScript 语法) |
| 插件基类 | FlutterPlugin, MethodCallHandler, LifecycleOwner | FlutterPlugin, MethodCallHandler, AbilityAware |
| 获取 Ability 上下文 | flutterPluginBinding.applicationContext | binding.getAbility().context(AbilityAware) |
| 联系人查询 | ContentResolverCompat.query + Cursor 遍历 | contact.queryContacts(context) 返回 Promise<Array<Contact>> |
| 并发模型 | 每类 Data 一个单线程 Executor + CountDownLatch | Promise 链式异步,无手动线程管理 |
| 头像读取 | ContentUris.withAppendedId + Photo 目录 | portrait.uri + fs.openSync 读取字节 |
| 字段过滤 | Cursor 投影(projection) | 插件侧 contactToMap(contact, fields) 按字段集过滤 |
| 权限声明 | AndroidManifest.xml | module.json5 requestPermissions |
4.2 关键 ArkTS 语法差异
| Android 语法 | ArkTS 语法 | 备注 |
|---|---|---|
import io.flutter.embedding.engine.plugins.FlutterPlugin | import { 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 的 queryContacts 传 ContactAttributes 参数在部分系统版本上返回 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 粒度单独提交。
六、测试与验证
测试环境
| 项目 | 版本 |
|---|---|
| Flutter | 3.41.10-ohos-1.0.0(channel: user-branch,基于 CPF-Flutter/flutter_flutter) |
| Dart | 3.11.5 |
| HarmonyOS SDK | 26.0.0(compatibleSdkVersion 5.1.0(18),targetSdkVersion 26.0.0) |
| IDE | DevEco Studio 26.0.0(build DS-261.23567.138.36.2600821) |
| 设备 ROM | OpenHarmony 7.0.0.105(API 26) |
版本获取方式:
| 版本项 | 获取方式 |
|---|---|
| Flutter / Dart | flutter --version |
| HarmonyOS SDK | 读取 example/ohos/build-profile.json5 的 compatibleSdkVersion / targetSdkVersion(或 ~/Library/OpenHarmony/Sdk/<version>/ 目录名) |
| IDE | macOS:defaults read /Applications/DevEco-Studio.app/Contents/Info.plist CFBundleShortVersionString |
| 设备 ROM | hdc shell param get const.product.software.version(先 hdc list targets 确认设备连接) |
验证要点
- 插件注册 —
GeneratedPluginRegistrant.ets正确导入并注册FastContactsPlugin,hilog 显示Adding plugin: FastContactsPlugin - 权限申请 — 点击 Load contacts 触发
contacts_permission通道,abilityAccessCtrl.requestPermissionsFromUser弹出系统授权框,授权成功 - 全量联系人读取 —
getAllContacts返回 3 个联系人,耗时 46–110ms(含查询与分页拉取) - 字段过滤 — 仅选择
phoneNumbers时contactToMap按选中字段裁剪返回,未选字段为空 - 分页与清理 —
getAllContactsPage按 batchSize 分批返回;clearFetchedContacts清理缓存 - 单联系人查询 —
getContact返回完整 JSON(id/phones/emails/structuredName),Took 65ms - 头像加载 —
getContactImage调用链正常,portrait.uri 字节读取无异常 - 构建验证 —
flutter build hap --debug成功产出entry-default-signed.hap
七、运行效果
真机验证中,示例应用加载联系人列表正常:点击 “Load contacts” 后约百毫秒内完成 3 个联系人的读取与展示(联系人名称、电话、邮箱均正确渲染);点击联系人条目进入详情页,单联系人查询在 65ms 内返回完整数据。

八、遗留问题与改进方向
踩坑复盘
适配中遇到的实际问题最有价值,以表格复盘:
| 踩坑点 | 现象 / 报错 | 根因与解法 |
|---|---|---|
| 模板噪音文件 | flutter create --template=plugin --platforms=ohos 生成 lib/ 下新式 platform interface、android/build.gradle.kts、ios/Classes 占位实现 | 模板生成内容与库原有结构(旧式 plugin + gradle)冲突,且引用未声明的依赖;清理所有噪音文件,仅保留 ohos/ 与 example/ohos/ |
| user_grant 权限配置 | 构建报错:hap 模块 requestPermissions 的 READ_CONTACTS 缺少 reason/usedScene | user_grant 权限必须带 reason($string:xxx 资源引用)与 usedScene;补齐 module.json5 与 string.json 资源 |
| ContactAttributes 构造 | 运行时 Constructor is false | ArkTS 中 new contact.ContactAttributes() 运行时不可构造,改用对象字面量 { attributes: [...] } |
| queryContacts 401 | contact.queryContacts(context, undefined, attrs) 返回 401 Mandatory parameters are left unspecified | 部分系统版本不接受显式传 undefined/null holder 或 attrs 参数;改用 queryContacts(context) 查询全部字段,插件侧过滤 |
| ArkTS 对象字面量 | 编译报 arkts-no-untyped-obj-literals | ArkTS 禁止无类型对象字面量;所有返回值显式声明 Record<string, Object> / Record<string, string> 类型 |
| ArkTS 隐式 any | 编译报 arkts-no-any-unknown | call.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.json5、flutter_assets/ 出现在 git status | 构建产物应忽略;example/ohos/.gitignore 追加 **/src/main/resources/rawfile/ |
已知问题
organization.department返回空字符串 — HarmonyOS ContactsKit 的Organization类仅提供 name 与 title 字段,没有部门字段,无法获取部门信息,与 Android/iOS 行为存在差异。fields参数不在原生层裁剪字段 — 因 queryContacts attrs 参数 401 兼容问题,原生层查询全部字段后由插件侧过滤,超大通讯录场景下全量读取性能略低于 Android/iOS。
未来优化
- 原生层字段裁剪 — 待 ContactsKit
queryContacts的 attrs 参数在目标系统版本稳定支持后,将字段过滤下沉到原生层,进一步提升大数据量场景性能。 - 头像读取优化 — 当前通过
fs.openSync读取 portrait.uri 字节,后续可评估使用image解码与缩略图缓存,减少重复读取开销。
九、总结
将一个 Flutter 三方库适配到 OHOS 平台,核心路径可以概括为 三步走:
- 找对应 ── 找到 OHOS 对每个 Android 原生 API 的等价实现(
ContentResolver+ Cursor →@kit.ContactsKit+ Promise) - 保契约 ── 确保方法通道名、方法名、参数与返回值结构完全一致(
com.github.s0nerik.fast_contacts与 5 个方法逐一对应) - 补缺口 ── 对 OHOS 不提供的 API 用合理方案弥补(无 department 字段返回空串、attrs 401 降级为插件侧过滤)
对于 fast_contacts 三方库,适配涉及 48 个文件的新增与少量修改。Dart 层和其他平台的代码完全不受影响——这正是 Flutter 跨平台三方库生态的魅力所在:同一份 Dart 代码,Android、iOS、OpenHarmony 三端原生实现各显神通,上层 API 契约恒定。
参考文档
更多推荐



所有评论(0)