1. 从三方库到鸿蒙原生:为什么要做这件事
先交代一下背景。团队在做某跨平台系统时,需要一套能在HarmonyOS NEXT上稳定运行的社交名片与通讯录管理能力。之前Android端和iOS端都复用Flutter生态里的成熟方案——输入关键词“oho chat address”定位到的那个本地通讯录解析库,覆盖面广、接口简洁、社区活跃度也不错。但当项目转向鸿蒙端,准备直接把原有Flutter插件迁过去时,问题立刻冒出来了:鸿蒙的运行时不是Linux内核那一套,Flutter标准插件机制里大量通过MethodChannel访问原生能力的方式,在纯血鸿蒙下需要换成ARKTS侧对应的实现通道。
这个“地址”还真不是普通地址——它管理的是联系人数据,包括姓名、电话号码、邮箱、组织信息、头像等结构化条目,对应到鸿蒙侧类似@ohos.contact这个系统能力。简单说:Android上有ContactsContract,iOS上有CNContactStore,而鸿蒙NEXT上对应的就是contactManager。Flutter SDK层面,目前官方还没有一个稳定的、覆盖联系人读写删改的联邦插件,社区里能用的库良莠不齐,很多直接卡在权限申请或者类型映射上。
所以这次鸿蒙化适配,说白了就是把这套Flutter依赖的三方库,在鸿蒙原生侧重新做一套实现,让上层Dart代码基本不用改、改也只是改一点点平台判断,就能在鸿蒙端获得和Android/iOS对等的体验。
我拿实际项目“某跨平台系统的联系人模块”作为载体,把这个适配过程完整还原一遍。这个过程适合谁参考?正在做鸿蒙化改造的Flutter团队、准备把既有插件生态迁移到鸿蒙的开发者、以及接入了oho chat address这类三方库但不想推倒重来的同学。
提示:这里提到的“鸿蒙化”指的是面向HarmonyOS NEXT(API 12+)的适配,不涉及旧版兼容框架。全文所有代码基于ArkTS和鸿蒙SDK公开接口编写。
2. 核心思路拆解:不是“翻译代码”,而是“架构对齐”
第一次接手这个任务的时候,最容易犯的错是想把Android端Java实现“逐行翻译”成ArkTS。这个思路在遇到系统API差异、异步模型差异、数据格式差异之后,基本会翻车。我建议的路径是先在Dart层做抽象,把与系统相关的部分全部收口到统一接口后面,再针对鸿蒙去实现这套接口。
2.1 需求边界和鸿蒙侧能力调研
先列一下原始库的核心能力范围,以下会作为适配基准反复提到:
- 获取联系人列表(支持分页和关键字过滤)
- 读取联系人详情(含电话号码、邮箱、头像、组织、备注)
- 新增联系人(写入本地通讯录)
- 修改联系人
- 删除联系人
- 读取通话记录(某些版本支持)
- 通讯录权限状态检测与引导申请
然后对应鸿蒙侧,我在API 12的官方文档里确认了以下事实:
@ohos.contact是系统提供的联系人操作能力,支持查询、插入、删除、更新。- 权限声明变了:鸿蒙NEXT不再像Android那样在
AndroidManifest.xml里写READ_CONTACTS,而是在module.json5里声明ohos.permission.READ_CONTACTS和ohos.permission.WRITE_CONTACTS。 - 授权弹窗是系统级的,权限的判定结果是
PermissionRequestResult,API返回0代表允许,-1代表拒绝。 - 联系人构建方式:新增联系人时需构造
contact.Contact对象,并把各种属性通过contact.PlainText等类型附到联系人上,这一点和Android的ContentValues思路类似,但API形状不同。
这一步调研的价值在于:后续所有代码设计都是围绕上面四个事实展开的,架构对齐而不是代码翻译的思路,可以少走至少三天弯路。
2.2 统一接口设计与目录结构调整
不管原始库内部怎么实现,从Dart侧使用者的角度看,它提供给业务方的接口基本是以下几类:
ContactsPlugin ├── Future<bool> requestPermission() ├── Future<List<ContactModel>> getContacts({String? query}) ├── Future<ContactModel?> getContactById(String id) ├── Future<String> addContact(ContactModel contact) ├── Future<bool> updateContact(ContactModel contact) ├── Future<bool> deleteContact(String id)这里最关键的设计决策是不直接改这些接口的签名。因为业务层已经调用得很稳定了,改签名意味着所有调用方都要跟着改,工作量成倍增长。鸿蒙化处理的是接口实现侧:在getContacts内部,走一条新的分支去调用鸿蒙原生能力。
目录结构推荐这样拆:
lib/ ├── ohochat_address.dart # 对外统一入口,保留原始API ├── models/ │ ├── contact_model.dart # 联系人实体(跨端通用) │ └── contact_field.dart # 字段类型枚举 ├── platform/ │ ├── contacts_platform.dart # 抽象接口 │ ├── contacts_android.dart # Android实现(原逻辑保留) │ ├── contacts_ios.dart # iOS实现(原逻辑保留) │ └── contacts_harmony.dart # 鸿蒙实现(本次新增) └── harmony/ ├── src/main/ets/ │ ├── ContactsHarmonyModule.ets │ ├── ContactsModelMapper.ets │ └── ContactsPermissionHelper.ets └── src/main/module.json5 # 权限和配置2.3 为什么选择在原生侧做映射,而不是用纯Dart解析
有同学可能会问:鸿蒙的联系人数据库能不能直接通过某种方式在Flutter层读取?理论上如果联系人数据存在一个可访问的数据库中,纯Dart解析是可行的,但实际中:
- 鸿蒙联系人模块通过
contactManager提供封装,没有直接暴露数据库文件。 - 联系人数据涉及大量系统级绑定(如头像文件访问、组织单位联动、通话记录的关联),直接解析文件无法保证一致性。
- 合规上,系统级API强制走权限申请流程,绕过系统API自己解析会存在合规风险。
所以原生侧做映射是唯一合理路径。
3. 核心适配细节与实操要点
3.1 权限申请和初始化流程
鸿蒙的权限模型和Android有本质区别,尤其第一次适配很容易踩坑。先看权限声明,在module.json5里加上:
{ "module": { "requestPermissions": [ { "name": "ohos.permission.READ_CONTACTS", "reason": "用于展示和同步联系人信息", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }, { "name": "ohos.permission.WRITE_CONTACTS", "reason": "用于新增和编辑联系人信息", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } } ] } }这里有个细节要特别提醒:reason字段是必填的,如果不填,应用市场审核时会被驳回,甚至在调试阶段就可能遇到系统弹窗异常。
然后在ArkTS侧写一个权限申请的工具类:
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit'; export class ContactsPermissionHelper { static async requestPermission(context: common.UIAbilityContext): Promise<boolean> { const permissions: Array<Permissions> = [ 'ohos.permission.READ_CONTACTS', 'ohos.permission.WRITE_CONTACTS' ]; const atManager = abilityAccessCtrl.createAtManager(); const result = await atManager.requestPermissionsFromUser(context, permissions); if (result.authResults.length > 0) { // 全部允许,authResults对应的返回值为0 return result.authResults.every(res => res === 0); } return false; } static async checkPermission(): Promise<boolean> { const atManager = abilityAccessCtrl.createAtManager(); const result = await atManager.checkAccessTokenSync(0, 'ohos.permission.READ_CONTACTS'); return result === 0; } }实操中我发现一个很重要的点:鸿蒙的权限checkAccessTokenSync在某些模拟器上可能返回异常,但真机上表现正常。所以我建议写适配层的时候,加一个“模拟器兼容策略”——如果检测到是模拟器环境,直接跳过check,改为主动发起请求,让系统弹窗来决定。
Flutter侧的调用逻辑:
if (Platform.isHarmonyOS) { final harmonyImpl = ContactsHarmonyImpl(); final ok = await harmonyImpl.requestPermission(); if (!ok) { throw PlatformException(code: 'PERMISSION_DENIED', message: '联系人权限未授予'); } }注意Flutter SDK在鸿蒙上判断平台的写法,不能用Platform.isAndroid,因为鸿蒙运行时不认为是Android。有些版本里Platform.operatingSystem可能返回字符串,建议做一层封装统一判断。
我习惯这样封装一个平台判断:
bool get isHarmonyOS { return Platform.operatingSystem.toLowerCase().contains('harmony') || Platform.environment['OHOS'] != null; }3.2 联系人数据模型的双向映射
鸿蒙侧的联系人实体结构如下(简化后):
import { contact } from '@kit.ContactsKit'; export class ContactsModelMapper { static toDartJson(contact: contact.Contact): Record<string, Object> { let id = ''; let name = ''; let phones: Array<string> = []; let emails: Array<string> = []; let avatarUri = ''; if (contact.id) id = `${contact.id}`; if (contact.name) name = contact.name; if (contact.phoneNumbers && contact.phoneNumbers.length > 0) { phones = contact.phoneNumbers.map(item => item.phoneNumber || ''); } if (contact.emails && contact.emails.length > 0) { emails = contact.emails.map(item => item.email || ''); } if (contact.avatar && contact.avatar.uri) { avatarUri = contact.avatar.uri; } return { 'id': id, 'name': name, 'phones': phones, 'emails': emails, 'avatarUri': avatarUri, }; } static fromDartJson(json: Record<string, Object>): contact.Contact { let c = new contact.Contact(); const name = json['name'] as string | undefined; if (name !== undefined) { c.name = name; } const phones = json['phones'] as Array<string> | undefined; if (phones !== undefined) { c.phoneNumbers = phones.map(p => { let item = new contact.PhoneNumber(); item.phoneNumber = p; return item; }); } const emails = json['emails'] as Array<string> | undefined; if (emails !== undefined) { c.emails = emails.map(e => { let item = new contact.Email(); item.email = e; return item; }); } return c; } }这里遇到的第一个坑:鸿蒙的contact.Contact在构造时,有些字段不允许直接赋值undefined。所以在取字段时一定要做undefined判断,否则运行时会报类型错误。
第二个坑是电话号类型:鸿蒙的PhoneNumber有一个label属性,表示号码类型(手机、工作、家庭等),如果不设置,默认显示可能不对。建议从Dart层接收phoneLabel字段,显式映射到label:
item.label = phoneLabel ?? contact.PhoneLabel.CUSTOM;3.3 联系人查询与分页实现
鸿蒙的contactManager.queryContacts有两种模式:一种是全量查询,一种是按条件查询。全量查询在大通讯录下(超过1000条)可能耗时较久,所以我建议分页取。
import { contact } from '@kit.ContactsKit'; import { BusinessError } from '@kit.BasicServicesKit'; const PER_PAGE = 100; export async function queryContactsByPage(page: number, query?: string): Promise<Array<Record<string, Object>>> { const holder = new contact.ContactHolder(); holder.pageNumber = page; holder.pageSize = PER_PAGE; holder.holderType = contact.HolderType.PAGE; let filter: contact.ContactFilter = new contact.ContactFilter(); filter.name = query ?? ''; try { let result = await contact.queryContacts(holder, filter); return result.map(c => ContactsModelMapper.toDartJson(c)); } catch (err) { let e = err as BusinessError; throw new Error(`query contacts failed: code=${e.code}, message=${e.message}`); } }ContactFilter里如果传空字符串,查询时不会作为条件过滤,这点和Android端ContentResolver的行为一致,实测下来完全没有问题。
还有一个精准过滤的技巧:鸿蒙的ContactFilter支持按phoneNumber维度精确匹配,但对于“模糊搜索”的需求(比如输入“张”字,匹配“张三”“张伟”),原生API支持有限。我的方案是:全量拉取然后内存过滤,每条记录只有几十字节,哪怕5000条联系人也就几百KB内存,完全扛得住。
如果联系人特别多(企业通讯录几千甚至上万条),还想做模糊过滤,可以考虑在原生侧用关键字 + 姓名索引的方式,做两次查询合并,但实际项目中暂时没遇到这个量级,先不展开。
3.4 新增与修改联系人
新增联系人的完整实现:
export async function addContact(contactModel: Record<string, Object>): Promise<string> { let c = ContactsModelMapper.fromDartJson(contactModel); try { let id = await contact.addContact(c); return `${id}`; } catch (err) { let e = err as BusinessError; throw new Error(`add contact failed: code=${e.code}, message=${e.message}`); } }修改联系人略有不同:鸿蒙的contact.updateContact需要一个已经存在完整实例的Contact对象,如果某些字段没设置,更新后可能把这些字段清空。
export async function updateContact(contactModel: Record<string, Object>): Promise<boolean> { let c = ContactsModelMapper.fromDartJson(contactModel); const id = contactModel['id']; if (id !== undefined) { c.id = parseInt(`${id}`); } try { await contact.updateContact(c); return true; } catch (err) { let e = err as BusinessError; throw new Error(`update contact failed: code=${e.code}, message=${e.message}`); } }实操中发现的坑:如果你要更新的联系人有头像,但传入的新Contact对象没有设置avatar字段,更新后原头像会被清除。这个行为Android和iOS都有类似的坑,鸿蒙也不例外。所以更新操作前,建议先按ID查询一遍原联系人,把原数据merge到新对象中再update。
3.5 头像与扩展字段处理
联系人头像在鸿蒙的实体里是contact.Avatar,它有个uri字段,指向一个文件路径或者内容提供者的路径。我遇到的情况是:直接设置本地文件路径,保存后头像可能不显示。
后来排查发现,问题出在文件权限上。鸿蒙对应用沙盒文件的访问做了隔离,联系人模块可能无法直接读取应用私有目录下的图片文件。解决方案有两种:
- 先把图片拷贝到公共图片目录(如
/storage/Users/currentUser/下),然后把这个公共路径设置给avatar.uri。 - 使用
fileUri转换,通过fileUri.getUriFromPath()把本地路径转换成file://开头的URI,再赋给avatar.uri。
我选了第二种,代码大致如下:
import { fileUri } from '@kit.CoreFileKit'; const uri = fileUri.getUriFromPath('/data/storage/el2/base/files/avatar_123.jpg'); let avatar = new contact.Avatar(); avatar.uri = uri; c.avatar = avatar;只有这样才能保证在通讯录列表里正常显示头像。
4. 实操过程:从编译到跑通全流程记录
4.1 鸿蒙侧工程配置与编译
先在Flutter工程的ohos目录下,给鸿蒙侧加一个模块。整体流程:
- 在
.flutter/ohos工程里,找到entry/src/main/ets/pages下的入口,添加自定义模块代码。 - 在
module.json5补充权限声明。 - 编译ArkTS侧模块,生成
.har包或者直接源码依赖。
如果想把鸿蒙侧做成独立Har包,方便团队内其他项目复用,可以这样操作:
hvigorw clean --no-daemon hvigorw assembleHap --mode module -p product=default --no-daemon然后到build/default/outputs/har目录下找产物。需要注意:Har包在引用时,module.json5里的requestPermissions不会自动合入宿主应用的配置,必须由使用方在宿主配置里手动声明。这是鸿蒙Har包的已知行为,和Android的AAR库权限自动合并完全不同。
4.2 Flutter侧插件通道打通
在Flutter侧新建ContactsHarmonyImpl,内部通过MethodChannel和ArkTS侧通信:
class ContactsHarmonyImpl extends ContactsPlatform { static const MethodChannel _channel = MethodChannel('ohochat_address/methods'); @override Future<bool> requestPermission() async { final result = await _channel.invokeMethod<bool>('requestPermission'); return result ?? false; } @override Future<List<ContactModel>> getContacts({String? query}) async { final list = await _channel.invokeListMethod<Map<Object?, Object?>>( 'getContacts', {'query': query ?? ''}, ); return list?.map(ContactModel.fromMap).toList() ?? []; } // 其余方法类似,不一一列出 }在ArkTS侧注册通道:
import { MethodChannel } from '@kit.IPCKit'; export function registerContactsChannel() { const channel = new MethodChannel('ohochat_address/methods'); channel.setMethodHandler((method, params, callback) => { switch (method) { case 'requestPermission': ContactsPermissionHelper.requestPermission(getContext()) .then(ok => callback({ result: ok })) .catch(e => callback({ error: { code: 'ERROR', message: `${e}` } })); break; case 'getContacts': queryContactsByPage(1, params['query'] as string) .then(data => callback({ result: data })) .catch(e => callback({ error: { code: 'ERROR', message: `${e}` } })); break; // 其他case } }); }注意一点:鸿蒙侧MethodChannel的callback不能多次调用,同一方法的回调只允许回调一次,多次调用会告警。所以异步操作里,要保证每个分支只走一次回调。建议在代码里加一个hasCallback的布尔保护,避免异常分支重复回调。
4.3 数据模型的Dart侧兼容
为了让上层业务代码不用感知平台差异,ContactModel需要支持从Map构造。这里的Map来源可能是Android的HashMap、iOS的NSDictionary,也可能是鸿蒙的Record,只要键名统一,就能无缝转换。
class ContactModel { final String id; final String name; final List<String> phones; final List<String> emails; final String avatarUri; ContactModel({ required this.id, required this.name, required this.phones, required this.emails, required this.avatarUri, }); factory ContactModel.fromMap(Map<Object?, Object?> map) { return ContactModel( id: map['id'] as String? ?? '', name: map['name'] as String? ?? '', phones: (map['phones'] as List<Object?>? ?? []).cast<String>(), emails: (map['emails'] as List<Object?>? ?? []).cast<String>(), avatarUri: map['avatarUri'] as String? ?? '', ); } Map<String, Object?> toMap() { return { 'id': id, 'name': name, 'phones': phones, 'emails': emails, 'avatarUri': avatarUri, }; } }提一个很多初学者会踩的坑:invokeListMethod返回的泛型是List<dynamic>,但当元素是Map时,具体类型可能是Map<Object?, Object?>而不是Map<String, dynamic>。直接map['key'] as String在某些情况下会抛cast异常,所以建议统一用Map<Object?, Object?>再取值。
4.4 真机调试与日志分析方法
鸿蒙调试和Android非常相似,我用的是DevEco Studio连接的HarmonyOS真机。命令行下也支持日志过滤:
hdc shell hilog | grep -i "oho"核心经验有三条:
- 权限弹窗的日志会有
ContactsPermissionHelper的tag,排查权限问题可以直接搜这个关键字。 - MethodChannel的调用链路在鸿蒙侧没有统一样式日志,需要自己在通道处理函数里加log,记录method名、参数、回调状态。
- 联系人模块的操作日志级别比较低,默认可能被过滤掉,需要调整日志等级才看得到。
我的做法是在registerContactsChannel里对每个method都加一行hilog.info,记录方法名和关键参数,后续所有问题排查都基于这套日志。
5. 常见问题与排查技巧实录
5.1 权限申请正常但读取为空
这个问题的典型场景是:requestPermission返回true,但getContacts返回空列表。我排查下来,大概率原因如下:
- 联系人模块本身没有数据源(比如新手机上还没同步过任何联系人)。
- 查询条件传入了非法字符导致结果被过滤。
- 联系人存储服务尚未就绪,在系统启动后立刻查询会偶发空结果。
验证方法很简单:先在系统通讯录App里手动添加一条联系人,再用Demo查询。如果还是空的,进一步检查ContactFilter里有没有把query参数误传成了单引号或特殊符号。
5.2 新增联系人后列表不刷新
这个问题很多人会忽略。鸿蒙的联系人查询没有强一致性的缓存策略,理论上插入后立即查询应该能看到,但遇到看不到的情况时,我用的办法是:
await channel.invokeMethod('syncContacts');在鸿蒙侧实现一个清理缓存的方法:
export async function syncContacts(): Promise<void> { // 强制重建联系人索引 await contact.queryContacts(new contact.ContactHolder(), new contact.ContactFilter()); }这个方法本质上是触发一次全量查询,让系统内部索引刷新。实测有效。
5.3 头像字段偶发写入失败
前面提到了fileUri的转换,但还有一个坑是路径存在但文件被删除。contact.addContact在保存头像时会读取该URI对应的文件,如果文件不存在会静默失败,而且不返回异常。排查方式很笨但有效:先确认file://路径在相册或文件管理器里能打开,再写入联系人。
5.4 分页查询偶发重复数据
鸿蒙的queryContacts分页机制在通讯录发生写入时,可能出现页间重复或丢失。这个属于数据快照的边界行为,官方文档没有明确说明。我的方案是拉取结果后在Dart侧做一次身份证号去重,用id做key,用Map去重,结合业务需求选择保留最新一条。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 权限申请弹窗不出现 | module.json5权限声明缺失或reason为空 | 检查requestPermissions配置 |
| 权限返回false但无报错 | 用户点了禁止且勾选“不再询问” | 引导去系统设置开启 |
| 查询联系人结果为空 | 通讯录无数据或查询条件有误 | 先验证系统通讯录是否有数据 |
| 新增联系人返回ID但列表看不到 | 系统索引未刷新 | 调用一次全量查询 |
| 头像保存后不显示 | URI指向私有目录文件 | 使用fileUri.getUriFromPath转换 |
| MethodChannel调用崩溃 | callback被多次调用或参数类型不匹配 | 检查回调保护与Map类型转换 |
| 真机调试时数组相关报错 | List泛型类型不一致 | 统一用Map<Object?, Object?>接收 |
5.6 独家避坑经验
这里分享几个完全靠踩坑换来的经验:
第一,鸿蒙的联系人查询API有“首次调用延迟”。应用冷启动后第一次调用queryContacts,可能耗时300到800毫秒,甚至更多。如果业务侧有一个快速进入通讯录页面的逻辑,建议在启动阶段提前预热一次查询,把结果缓存到一个临时变量里。
第二,不要过度依赖原生侧做搜索过滤。鸿蒙的ContactFilter在模糊搜索上不如Android的LIKE查询方便。与其在原生侧反复调API,不如全量拉到Dart层,自己用正则或子串匹配,灵活性大得多。
第三,Har包权限合入问题必须写进团队文档。每次有新人接入,踩的最多的就是权限问题。因为Har包不会自动合并权限,所以让新人在宿主module.json5里手动加上权限写法,能够节省大量沟通时间。
6. 适配产出的性能与稳定性验证
6.1 性能对比数据
我在一台HarmonyOS NEXT真机上跑了一组基准测试,数据如下:
| 操作 | 平均耗时(毫秒) | 备注 |
|---|---|---|
| 冷启动权限申请 | 120 | 系统弹窗交互时间不计 |
| 全量查询(500条联系人) | 230 | 首次调用延迟较高 |
| 分页查询(每页100条) | 85 | 连续翻页稳定 |
| 新增联系人 | 45 | 不含头像写入 |
| 头像写入(本地文件) | 320 | 文件越大耗时越高 |
| 修改联系人 | 110 | 必须携带主键ID |
实测下来,鸿蒙侧性能基本对标Android原生,在查询场景还略有优势。
6.2 稳定性表现
连续跑了两小时的自动化脚本,包含新增、删除、修改、查询的循环操作,没有出现一次崩溃或ANR。唯一需要注意的是:鸿蒙联系人模块在高频写入时(比如连续新增100条),系统可能会触发一次GC或数据库落盘等待,导致单次操作偶发延迟。这个不是适配层的问题,属于系统行为,业务侧可以通过异步队列来缓冲。
7. 安全与合规校验
鸿蒙应用上架时,涉及联系人权限的应用审查比较严格。我在适配过程中特别注意了以下几点:
- 权限用途声明:
reason字段和弹窗文案必须一致,不能写“用于展示联系人信息”却在上架审核时被看出实际用途是采集用户数据。 - 最小化采集:不要在联系人数据里读取和业务无关的字段。比如业务只需要姓名和电话,就不要去拿邮箱、地址、生日。
- 数据本地化:联系人数据如果不上传服务器,在隐私政策里要明确写“仅本机处理,不会上传”。
- 批量操作限制:不建议在应用启动时后台批量读取全部联系人。
READ_CONTACTS权限在NEXT系统中会记录调用频率,高频读取可能被标记为风险行为。
如果你的应用不涉及通讯录数据上传,在隐私弹窗里最好直接写明“本地处理”,这样审核通过率会高很多。
8. 后续扩展思路
这套适配方案做完之后,我把鸿蒙侧的能力又做了一层标准化封装,沉淀成了一个内部自用的Har包。后续如果有团队也需要做联系人模块的鸿蒙化改造,可以直接复用。
扩展方向上,我觉得可以做的事情还不少:
- 同步策略增强:目前是本地全量读取,后续可以结合分布式数据管理能力,做多设备之间的联系人增量同步。
- 卡片展示:鸿蒙支持服务卡片,联系人卡片可以做成桌面小组件,直接拨号或查看详情。
- 来电识别场景:把联系人库接到来电识别模块里,在HarmonyOS NEXT上实现来电号码匹配和名片展示。
- 联系人分组与标签管理:在本地联系人数据结构之上,增加自定义分组和标签,弥补系统通讯录在管理维度上的不足。
我在实际优化过程中发现一个可以明显提升体验的点:将查询结果缓存在Dart侧,并监听联系人变更回调(鸿蒙支持contact.on('contactChange')),当系统通讯录发生变化时主动刷新缓存,这样业务侧在用户从系统设置页修改联系人之后回到App时,列表能立刻保持一致。这是一个值得投入时间做的方向。
最后从性能、稳定性、可维护性来看,这次鸿蒙化适配整体达到了预期目标。核心业务代码零侵入迁移,上层调用方式和原有Android/iOS保持一致,新增的鸿蒙分支逻辑清晰,后续维护成本也算可控。
踩过一次坑之后,最大的体会是:跨端适配的第一步不是写代码,而是把两边的系统API差异摸透,尤其是数据模型、权限模型和生命周期模型。这三个模型对齐了,后面代码写起来会非常顺。
如果你也在做类似的Flutter三方库鸿蒙化适配,欢迎交流。通讯录只是第一步,后续还有文件系统、多媒体、推送通知等更多系统能力需要做类似的鸿蒙化封装,思路是相通的。