【HarmonyOS NEXT】获取通讯录信息(Contacts Kit)详解——联系人选择、权限申请、联系人读取、联系人管理(2026最新版)
适用版本:
- HarmonyOS 5
- HarmonyOS NEXT
- DevEco Studio 6+
- ArkTS
一、前言
通讯录(Contacts)几乎是所有社交类、办公类、企业IM、CRM、OA、企业通讯录等应用都会使用到的一项能力。
例如:
- 微信邀请好友
- QQ添加通讯录好友
- 钉钉企业联系人
- 飞书组织架构
- 企业OA通讯录
- CRM客户导入
- 手机联系人同步
很多刚开始学习HarmonyOS的开发者都会发现:
为什么网上几乎没有鸿蒙通讯录API?
原因很简单:
HarmonyOS NEXT 出于隐私保护考虑,已经不建议应用直接读取整个通讯录,而是推荐开发者通过 Contacts Picker(联系人选择器) 获取用户主动选择的联系人。批量读取通讯录属于受限开放能力,仅适用于备份、同步等特定场景,需要申请相应权限。
因此,目前开发主要有两种方式:
方式一(推荐)
Contacts Picker
用户主动选择联系人
★★★★★ 推荐
普通应用均可使用
-----------------------
方式二(受限)
Contacts Kit
读取整个通讯录
新增联系人
修改联系人
删除联系人
★★★★★
需要申请受限权限
本文将全部讲解。
二、Contacts Kit介绍
HarmonyOS提供了Contacts Kit。
主要负责:
联系人查询
联系人新增
联系人修改
联系人删除
联系人选择器
联系人头像
联系人电话
联系人邮箱
联系人地址
联系人公司
联系人备注
联系人分组
官方能力如下:
Contacts Kit
↓
联系人管理
↓
联系人Picker
↓
联系人数据库
需要注意的是:
HarmonyOS NEXT已经把通讯录能力进行了严格权限划分。
普通应用:
✅ 推荐使用Picker
特殊应用:
才能申请READ_CONTACTS权限。
三、通讯录权限介绍
很多人第一步就写:
ohos.permission.READ_CONTACTS
然后发现:
权限申请失败
为什么?
因为:
HarmonyOS NEXT中:
READ_CONTACTS
属于受限开放权限
官方说明:
READ_CONTACTS
仅支持:
联系人同步
联系人备份
设备克隆
等特殊业务
普通应用一般不能申请。
因此:
官方推荐:
Contacts Picker
由用户主动选择联系人。
四、Contacts Picker介绍
这是HarmonyOS官方推荐方式。
流程如下:
App
↓
打开联系人Picker
↓
系统联系人界面
↓
用户选择联系人
↓
返回联系人信息
↓
App获取结果
整个过程:
无需读取整个通讯录
无需同步全部联系人
更加安全
更加符合隐私规范
也是目前HarmonyOS官方推荐方案。
五、Contacts Picker使用流程
整个流程只有四步:
第一步
导入模块
↓
第二步
创建Picker
↓
第三步
打开联系人列表
↓
第四步
获取联系人结果
整体流程如下:
Button
↓
点击
↓
Contacts Picker
↓
用户选择联系人
↓
返回联系人对象
六、项目结构
entry
│
├──pages
│ Index.ets
│
├──model
│ ContactModel.ets
│
├──utils
│ ContactHelper.ets
│
└──resources
后面我们会把联系人模块封装成一个工具类。
七、导入联系人模块
首先导入联系人能力:
import contact from '@ohos.contact';
随后即可调用联系人相关接口。
八、创建联系人选择器
例如:
import contact from '@ohos.contact';
let picker = new contact.ContactPicker();
然后可以配置:
let options = {
multiple: false
}
如果:
multiple=true
表示:
可以选择多个联系人
否则:
只能选择一个联系人
九、打开联系人选择器
例如:
picker.select(options)
.then((result)=>{
console.info(JSON.stringify(result))
})
.catch((err)=>{
console.error(JSON.stringify(err))
})
运行效果:
点击按钮
↓
系统联系人界面
↓
选择联系人
↓
返回App
↓
获得联系人数据
整个过程无需申请读取全部通讯录权限,更符合 HarmonyOS 的隐私设计。
十、返回的数据结构
返回的数据通常包含:
{
id:"1001",
name:"张三",
phoneNumbers:[
"13800138000"
],
emails:[
"abc@qq.com"
],
organization:"Huawei",
portraitUri:"",
addresses:[
]
}
一般情况下:
开发者最常用的是:
姓名
手机号
邮箱
头像
十一、Contacts Picker 完整实战(推荐方式)
上一章节我们已经能够打开系统联系人选择器,本章节我们来完成一个完整的联系人选择案例,包括:
- 点击按钮打开联系人
- 单选联系人
- 多选联系人
- 获取联系人信息
- 页面数据显示
- 异常处理
这是 HarmonyOS 官方推荐的联系人获取方式,无需读取整个通讯录,更符合隐私保护要求。
1、实现效果
最终效果如下:
┌────────────────────────────┐
│ 通讯录Demo │
├────────────────────────────┤
│ │
│ 请选择联系人 │
│ │
│ 张三 │
│ 13800138000 │
│ │
│ [选择联系人] │
│ │
└────────────────────────────┘
点击按钮后:
Button
↓
打开系统联系人
↓
用户选择联系人
↓
返回联系人对象
↓
刷新UI
整个流程完全由系统负责,开发者无需自行实现联系人列表页面。
2、定义联系人数据模型
为了方便项目维护,建议先定义一个联系人实体类。
ContactModel.ets
export class ContactModel {
id: string = ''
name: string = ''
phone: string = ''
email: string = ''
organization: string = ''
portraitUri: string = ''
}
以后无论是网络接口还是数据库,都可以统一使用该实体。
3、页面状态定义
ArkUI 推荐使用 @State 管理页面数据。
@State contact: ContactModel = new ContactModel()
默认显示:
姓名:
手机号:
邮箱:
选择联系人后自动刷新。
4、页面UI设计
例如:
Column({ space: 20 }) {
Text(this.contact.name)
Text(this.contact.phone)
Text(this.contact.email)
Button("选择联系人")
.onClick(()=>{
this.selectContact()
})
}
.width("100%")
.padding(20)
运行后:
姓名
手机号
邮箱
按钮
布局十分简单。
5、封装选择联系人方法
推荐不要把所有代码写在 Button 中。
例如:
private async selectContact() {
}
以后:
Button
↓
selectContact()
↓
Contacts Picker
代码更加清晰。
6、创建 Picker
例如:
let picker = new contact.ContactPicker()
然后配置:
let options = {
multiple:false
}
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
| multiple | boolean | 是否允许多选 |
例如:
false
↓
只能选择一个联系人
如果:
true
↓
可以选择多个联系人
7、打开联系人页面
例如:
let result = await picker.select(options)
整个调用流程:
App
↓
Contacts Picker
↓
用户点击联系人
↓
返回结果
如果用户取消:
throw Exception
所以建议:
try{
}
catch(){
}
8、解析联系人数据
例如:
this.contact.name =
result.name
this.contact.phone =
result.phoneNumbers[0]
this.contact.email =
result.emails[0]
如果联系人没有邮箱:
emails
↓
[]
↓
不能直接取下标
推荐:
this.contact.email =
result.emails.length>0 ?
result.emails[0] :
""
更加安全。
9、完整代码示例
下面给出完整示例:
import contact from '@ohos.contact'
@Entry
@Component
struct Index {
@State
contactName:string=""
@State
phone:string=""
build(){
Column(){
Text(this.contactName)
Text(this.phone)
Button("选择联系人")
.onClick(async ()=>{
let picker =
new contact.ContactPicker()
try{
let result =
await picker.select({
multiple:false
})
this.contactName =
result.name
if(result.phoneNumbers.length>0){
this.phone =
result.phoneNumbers[0]
}
}catch(err){
console.error(JSON.stringify(err))
}
})
}
}
}
10、运行流程分析
整个运行过程如下:
App启动
↓
点击按钮
↓
创建Picker
↓
跳转系统通讯录
↓
用户选择联系人
↓
返回联系人对象
↓
刷新State
↓
UI自动刷新
ArkUI 响应式更新:
State变化
↓
组件重新构建
↓
Text重新显示
无需手动调用:
notifyDataSetChanged()
setState()
refresh()
11、多选联系人
如果:
multiple:true
那么:
张三
李四
王五
都可以选择。
返回的数据变成:
[
Contact,
Contact,
Contact
]
遍历即可:
for(let item of result){
console.info(item.name)
}
企业通讯录导入通常都会采用这种方式。
12、联系人头像
如果联系人存在头像:
返回:
portraitUri
例如:
console.info(result.portraitUri)
返回:
file://....
或者
media://....
然后即可:
Image(result.portraitUri)
显示联系人头像。
13、联系人电话号码
很多联系人:
手机
家庭
办公
公司
传真
都会存在多个号码。
例如:
phoneNumbers:[
"138****0000",
"0755-88888888"
]
不要默认只有一个。
推荐:
for(let phone of result.phoneNumbers){
}
全部展示。
14、联系人邮箱
同样:
emails
也是数组。
例如:
emails:[
"abc@qq.com",
"123@gmail.com"
]
建议全部读取。
15、联系人公司信息
例如:
organization
返回:
Huawei
Tencent
Alibaba
企业OA中经常使用。
16、联系人地址
返回:
addresses
例如:
广东省深圳市
北京市海淀区
一般用于:
- 快递
- CRM
- 客户管理
17、联系人备注
联系人还可能包含:
Note
Birthday
Nickname
Website
IM
CustomLabel
不同系统版本返回字段可能存在差异,因此建议在使用前做好空值判断,并根据实际返回的数据结构进行适配。
18、异常处理
最常见异常:
用户取消选择
打开联系人
↓
点击返回
↓
Promise Reject
处理:
try{
}catch(err){
}
不要弹出错误提示。
联系人为空
例如:
没有手机号
判断:
if(result.phoneNumbers.length==0){
}
邮箱为空
例如:
if(result.emails.length==0){
}
头像不存在
例如:
portraitUri==""
使用默认头像即可。
19、最佳实践
建议将联系人能力统一封装为一个工具类:
ContactHelper
↓
selectSingle()
↓
selectMultiple()
↓
getPhones()
↓
getEmails()
↓
parseContact()
页面只需要:
let contact =
await ContactHelper.selectSingle()
即可获得统一格式的联系人对象,后续维护成本更低,也更符合企业级项目的分层设计。
十三、HarmonyOS 通讯录权限管理详解(Contacts Permission)
上一章节,我们介绍了 Contacts Picker 的使用,它是 HarmonyOS 官方推荐的联系人获取方式。
很多开发者在实际开发时都会产生疑问:
为什么 Android 可以直接读取整个通讯录,而 HarmonyOS NEXT 却提示没有权限?
本章节就来深入解析 HarmonyOS 的通讯录权限模型、受限权限机制以及企业级开发中的最佳实践。
一、HarmonyOS 权限模型
HarmonyOS NEXT 对用户隐私进行了更加严格的保护。
通讯录属于用户的高敏感数据,因此默认情况下应用不能直接访问。
整体权限模型如下:
应用
│
▼
申请权限
│
▼
系统权限管理
│
▼
用户授权
│
▼
允许访问
但是对于通讯录而言,仅有用户授权还不够。
二、通讯录权限分类
HarmonyOS 中通讯录相关能力主要涉及以下权限:
READ_CONTACTS
读取联系人
------------------------
WRITE_CONTACTS
修改联系人
------------------------
READ_CALL_LOG
读取通话记录
------------------------
WRITE_CALL_LOG
修改通话记录
其中真正开发最常用的是:
READ_CONTACTS
三、为什么申请 READ_CONTACTS 会失败?
很多开发者第一次都会这样写:
{
"requestPermissions": [
{
"name": "ohos.permission.READ_CONTACTS"
}
]
}
然后运行:
权限申请失败
或者:
Authorization failed
甚至:
BusinessError
为什么?
答案就是:
READ_CONTACTS
属于 Restricted Permission(受限权限)
也就是说:
不是所有应用都可以申请。
四、什么是受限权限(Restricted Permission)
HarmonyOS 将权限分成多个等级:
普通权限
↓
危险权限
↓
受限权限
↓
系统权限
其中:
通讯录、
短信、
通话记录、
设备标识、
安装应用列表
都属于高隐私数据。
因此:
HarmonyOS 不允许普通应用自由读取。
五、为什么这样设计?
假设:
某个小游戏安装后:
读取通讯录
↓
上传服务器
↓
分析联系人关系
↓
广告推送
用户根本不知道发生了什么。
因此 HarmonyOS NEXT 提出了:
最小权限原则(Least Privilege)
即:
应用只获取完成当前任务所必须的数据。
例如:
用户只是想邀请一位好友。
那么:
应用根本没有必要读取:
1000
2000
5000
全部联系人
所以官方提供了:
Contacts Picker
用户主动选择联系人。
应用只能获取:
张三
而不是:
整个通讯录
这样更加安全。
六、Contacts Picker 为什么不需要 READ_CONTACTS?
很多人觉得奇怪:
既然通讯录受限,
为什么 Picker 可以?
原因就在于:
联系人数据并不是应用自己读取的。
整个过程如下:
App
↓
调用 Picker
↓
系统联系人应用
↓
用户主动选择
↓
系统返回结果
↓
App 获取数据
注意:
真正读取通讯录的是:
系统联系人
不是你的 App。
因此:
无需 READ_CONTACTS
这也是目前 HarmonyOS 官方推荐方案。
七、哪些应用可以申请 READ_CONTACTS?
官方允许以下类型应用申请:
联系人同步
联系人备份
手机克隆
企业设备管理
系统工具
例如:
华为云同步
手机克隆
企业通讯录
设备迁移
普通:
商城
新闻
外卖
视频
小游戏
通常无法获得该权限。
八、企业应用怎么办?
很多企业 OA 都需要:
读取全部联系人
例如:
员工通讯录
CRM
客户同步
组织架构
这类应用通常需要:
申请受限开放能力
↓
平台审核
↓
权限放开
↓
发布
因此:
不要认为:
READ_CONTACTS
申请不了
就是 API 坏了
实际上:
这是 HarmonyOS 的安全策略。
九、什么时候应该使用 Picker?
官方推荐:
绝大多数 App:
都应该:
Contacts Picker
例如:
邀请好友
↓
选择收货联系人
↓
分享联系人
↓
添加紧急联系人
↓
企业联系人选择
这些场景:
都不需要读取整个通讯录。
十、什么时候必须读取整个通讯录?
例如:
微信好友同步
需要:
全部联系人
例如:
企业CRM
↓
同步客户
例如:
通讯录备份
这些属于:
后台同步
批量读取
持续更新
只能:
READ_CONTACTS
十一、运行时权限申请流程
对于普通危险权限,HarmonyOS 的流程如下:
声明权限
↓
检查权限状态
↓
申请权限
↓
用户授权
↓
执行功能
但对于:
READ_CONTACTS
即使代码流程正确,也可能因为权限类型受限而无法获得授权。
因此,开发时要先确认应用是否具备申请该权限的资格,而不是一味排查代码问题。
十二、如何优雅处理权限失败?
推荐不要这样:
读取失败!
程序异常!
未知错误!
用户完全不知道原因。
建议:
无法读取通讯录
请选择联系人
或者:
当前版本
仅支持联系人选择
体验更好。
十三、企业级权限封装
建议项目中统一封装权限管理类:
PermissionManager
│
├──checkPermission()
├──requestPermission()
├──gotoSetting()
├──showPermissionDialog()
└──openContactPicker()
业务页面:
Page
↓
PermissionManager
↓
Contacts Picker
这样:
以后修改权限逻辑:
所有页面:
无需修改。
十四、权限异常处理
企业项目最常见:
用户拒绝
第一次
↓
拒绝
处理:
弹出说明
↓
再次申请
永久拒绝
勾选
↓
不再询问
处理:
引导进入
设置页面
企业权限未开通
表现:
BusinessError
Permission denied
不要反复重试。
应该:
联系管理员
申请开放权限
模拟器无法测试
部分通讯录能力:
Simulator
×
真机
√
建议:
始终使用真机。
十五、企业项目最佳实践
建议采用如下架构:
UI
↓
ViewModel
↓
ContactService
↓
PermissionManager
↓
Contacts Kit
其中:
PermissionManager
负责:
- 检查权限
- 申请权限
- 权限失败处理
ContactService
负责:
- 打开 Picker
- 查询联系人
- 数据转换
- 返回统一模型
这样可以做到:
业务页面
↓
一行代码
↓
获得联系人
例如:
const contact = await ContactService.selectSingle();
而页面无需关心:
- 权限判断
- 异常捕获
- 数据解析
- 兼容不同系统版本
十六、Contacts Kit 与 Contacts Picker 对比
| 对比项 | Contacts Picker | Contacts Kit |
|---|---|---|
| 是否官方推荐 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 是否需要受限权限 | ❌ 否 | ✅ 是 |
| 用户体验 | 用户主动选择 | 应用直接读取 |
| 是否支持普通应用 | ✅ 支持 | ❌ 大多数不支持 |
| 是否可批量读取 | ❌ | ✅ |
| 是否可后台同步 | ❌ | ✅ |
| 适用场景 | 选择联系人、邀请好友 | 企业同步、备份、迁移 |
十七、面试高频问题
1. HarmonyOS 为什么推荐 Contacts Picker?
答: 因为 Contacts Picker 由系统联系人应用完成数据读取,用户主动选择联系人,应用仅获得用户授权分享的联系人信息,无需访问整个通讯录,更符合最小权限原则和隐私保护要求。
2. 普通应用可以申请 READ_CONTACTS 吗?
答: 一般不可以。READ_CONTACTS 属于受限开放权限(Restricted Permission),通常仅开放给通讯录同步、数据迁移、企业管理等符合条件的应用。
3. 企业级项目如何设计通讯录模块?
答: 建议按 UI → ViewModel → ContactService → PermissionManager → Contacts API 分层设计,将权限申请、联系人选择、数据解析统一封装,业务层只关注联系人结果,提高可维护性和可测试性。
十四、HarmonyOS Contacts Kit 数据模型深度解析(联系人字段详解)
前面我们已经了解了:
- Contacts Picker 的使用
- 通讯录权限体系
- 为什么普通应用推荐使用 Picker
很多开发者接下来会有新的疑问:
联系人对象到底有哪些字段?
手机号为什么是数组?
一个联系人可以有几个邮箱?
为什么不同手机返回的数据不一样?
本章就深入解析 HarmonyOS Contacts Kit 的数据模型,理解联系人对象的组织方式,为后续联系人查询、同步、搜索、导入导出打下基础。
一、联系人(Contact)对象结构
在 HarmonyOS 中,一个联系人(Contact)并不是简单的一条数据,而是由多个属性组合而成。
一个完整的联系人可以表示为:
Contact
│
├── id
├── name
├── phoneNumbers
├── emails
├── organization
├── addresses
├── portraitUri
├── websites
├── nickName
├── note
├── birthday
└── groups
也就是说:
联系人并不是:
姓名
手机号
而是:
一个对象
↓
多个属性
↓
多个集合
二、为什么手机号是数组?
很多新人都会写:
contact.phone
结果发现:
不存在
真正的数据结构更类似于:
phoneNumbers: [
"13800138000",
"0755-88888888",
"400-800-8888"
]
为什么?
因为一个联系人可能拥有:
手机
公司
家庭
备用
传真
其它号码
因此设计成数组。
例如:
张三
↓
手机
13800138000
↓
办公
0755-88888888
↓
家庭
02088888888
企业 CRM 中这种情况十分常见。
三、手机号建议如何封装?
很多业务只需要:
手机号
建议统一封装:
class ContactModel {
name:string=""
phone:string=""
}
解析时:
if(contact.phoneNumbers.length>0){
model.phone=contact.phoneNumbers[0]
}
如果业务需要:
多个号码
建议:
phones:string[]=[]
全部保存。
四、邮箱为什么也是数组?
邮箱的数据结构与手机号类似:
emails:[
"test@qq.com",
"work@company.com",
"abc@gmail.com"
]
原因:
现代联系人通常拥有:
个人邮箱
公司邮箱
备用邮箱
所以:
Email
↓
Array
而不是:
String
五、地址(Addresses)
地址同样可以存在多个:
家庭
公司
学校
其它
例如:
addresses:[
"广东深圳",
"北京市朝阳区"
]
企业管理系统经常使用:
客户地址
↓
物流地址
↓
收货地址
所以建议:
统一按集合处理。
六、组织信息(Organization)
很多开发者认为:
organization
↓
公司名称
实际上:
企业联系人一般还会包含:
公司
部门
职位
工号
例如:
Huawei
研发一部
高级工程师
在 OA、企业通讯录中使用频率非常高。
七、头像(Portrait)
联系人头像通常保存为:
URI
例如:
file://...
media://...
并不是:
Bitmap
也不是:
PixelMap
因此:
UI 显示时一般需要:
Image(contact.portraitUri)
如果:
portraitUri==""
说明:
联系人没有头像。
建议:
显示默认头像。
八、备注(Note)
联系人可以保存备注:
小学同学
大学室友
客户A
供应商
例如:
note:
"十年老客户"
CRM 系统中十分常见。
九、昵称(NickName)
例如:
王总
老李
Tom
Jerry
昵称:
并不是姓名。
很多社交软件:
都会优先显示:
昵称
↓
姓名
十、生日(Birthday)
联系人支持:
生日
例如:
1996-10-01
很多 App:
例如:
日历
生日提醒
会员系统
都会读取。
十一、网站(Website)
联系人可以拥有:
官网
博客
GitHub
个人主页
例如:
https://example.com
企业联系人:
十分常见。
十二、IM 信息
很多联系人:
还有:
微信
QQ
Telegram
Skype
Teams
统一称:
Instant Message
简称:
IM
企业办公软件:
大量使用。
十三、联系人 ID
很多开发者:
喜欢这样:
姓名
↓
唯一
实际上:
错误。
真正唯一的是:
Contact ID
例如:
10001
10002
10003
因此:
数据库更新:
应该:
ID
↓
更新联系人
不要:
姓名
因为:
张三
可能有很多个
十四、联系人对象为什么那么复杂?
因为:
联系人实际上对应:
现实中的一个人。
一个人拥有:
多个电话
多个邮箱
多个地址
多个网站
多个组织
多个IM
所以:
Contacts Kit:
采用:
Contact
↓
多个子对象
↓
多个集合
而不是:
一个大字符串
十五、推荐的数据模型设计
企业项目建议重新封装自己的联系人模型,而不是直接使用系统对象:
export class ContactModel {
id:string=""
name:string=""
avatar:string=""
company:string=""
phones:string[]=[]
emails:string[]=[]
addresses:string[]=[]
note:string=""
}
优点:
统一数据格式
↓
业务解耦
↓
方便缓存
↓
方便网络同步
↓
方便数据库存储
十六、数据转换层(Mapper)
实际开发中,不建议业务代码直接操作系统返回的 Contact 对象。
推荐增加一层转换:
Contacts Kit
│
▼
ContactMapper
│
▼
ContactModel
│
▼
UI
例如:
export class ContactMapper {
static map(contact:any):ContactModel{
let model=new ContactModel()
model.id=contact.id
model.name=contact.name
model.avatar=contact.portraitUri
model.company=contact.organization
model.phones=contact.phoneNumbers
model.emails=contact.emails
model.addresses=contact.addresses
return model
}
}
这样做有几个好处:
- 系统 API 变化时,只需修改 Mapper。
- UI 和业务层无需依赖系统数据结构。
- 便于单元测试和 Mock 数据。
- 后续切换数据来源(本地数据库、网络接口)时改动更小。
十七、大型项目推荐架构
推荐采用如下分层:
UI
↓
ViewModel
↓
Repository
↓
ContactService
↓
ContactMapper
↓
Contacts Kit
各层职责:
| 层级 | 职责 |
|---|---|
| UI | 展示联系人、处理用户交互 |
| ViewModel | 管理页面状态,调用业务接口 |
| Repository | 统一管理联系人数据来源(本地/远程) |
| ContactService | 封装 Contacts Kit 调用 |
| ContactMapper | 系统对象转换为业务对象 |
| Contacts Kit | 与系统通讯录交互 |
这种结构更适合企业级项目,也便于后期维护和扩展。
十八、常见开发误区
误区一:手机号只有一个
错误:
contact.phoneNumbers[0]
直接认为唯一号码。
正确做法:
根据业务需求遍历全部号码,或明确约定取主号码。
误区二:姓名唯一
错误:
根据姓名更新联系人
正确:
根据 Contact ID 更新
误区三:所有字段都存在
错误:
contact.emails[0]
正确:
const email = contact.emails?.length > 0
? contact.emails[0]
: ""
任何可选字段都应做好空值判断。
十五、HarmonyOS Contacts Kit 查询联系人实战(Contacts Query 全解析)
说明:
从本章开始,我们进入 Contacts Kit 的核心 API 讲解。需要再次强调的是:查询整个通讯录属于受限能力,普通应用通常应使用 Contacts Picker;本章内容主要用于了解 Contacts Kit 的能力模型、企业应用开发以及面试学习。
一、Contacts 查询整体流程
很多开发者认为:
查询联系人
↓
得到 List<Contact>
实际上,企业级项目通常会经历多个步骤:
UI
↓
ContactService
↓
ContactsRepository
↓
Contacts Kit
↓
联系人数据库
↓
Cursor(游标)
↓
Contact
↓
Mapper
↓
ContactModel
↓
UI
可以看到:
真正返回给 UI 的一般不是系统对象,而是业务对象。
二、Contacts 查询方式
Contacts Kit 一般支持以下几类查询:
查询全部联系人
↓
根据ID查询
↓
根据姓名查询
↓
根据手机号查询
↓
分页查询
↓
条件过滤
↓
排序查询
不同查询方式适用于不同业务场景。
三、查询全部联系人
企业通讯录同步时:
通常需要:
所有联系人
流程如下:
Contacts Kit
↓
查询数据库
↓
Cursor
↓
读取每一条联系人
↓
转换对象
↓
返回 List<Contact>
注意:
不要一次性把所有联系人直接加载到页面。
例如:
5000 联系人
↓
一次创建5000个对象
↓
内存快速增长
↓
页面卡顿
因此:
推荐:
分页
↓
边读取
↓
边转换
↓
边显示
四、Cursor(游标)是什么?
很多开发者第一次看到:
Cursor
感觉很陌生。
实际上:
Cursor 可以理解成:
数据库结果集
例如:
数据库:
联系人表
↓
10000 条数据
不会:
一次返回10000对象
而是:
Cursor
↓
第一条
↓
第二条
↓
第三条
↓
……
像指针一样:
逐条读取。
这样:
内存占用非常低。
五、Cursor 遍历流程
例如:
Cursor
↓
moveNext()
↓
读取数据
↓
moveNext()
↓
读取数据
↓
结束
整体流程:
Cursor
↓
while()
↓
读取
↓
封装
↓
加入 List
企业项目:
基本都是这样处理。
六、为什么不能一次读取全部?
假设:
一个联系人:
姓名
手机号
头像
邮箱
地址
备注
约:
2KB
如果:
10000 联系人
则:
约20MB
如果:
再加载头像:
几十 MB
甚至
上百 MB
因此:
千万不要:
一次全部加载
七、分页查询思想
推荐:
第一页
50 条
↓
用户滑动
↓
第二页
50 条
↓
第三页
这样:
页面:
始终保持:
几十条联系人
不会出现:
OOM
卡顿
八、联系人分页架构
企业项目:
一般采用:
List
↓
Scroll
↓
滑到底部
↓
加载下一页
↓
追加数据
而不是:
一次全部显示
九、根据姓名查询
很多 App:
都会:
搜索
↓
张三
流程:
输入框
↓
关键词
↓
Contacts Kit
↓
返回联系人
例如:
张
可能返回:
张三
张四
张老师
张经理
因此:
一般:
模糊查询
而不是:
完全匹配
十、根据手机号查询
例如:
CRM:
客户来电
↓
13800138000
↓
查联系人
↓
显示姓名
整体流程:
手机号
↓
Contacts Kit
↓
联系人
↓
详情页
这种:
企业系统:
使用非常多。
十一、根据 ID 查询
联系人:
真正唯一的是:
ID
例如:
10001
查询:
Contact
↓
ID=10001
返回:
张三
更新联系人:
也是:
ID
↓
更新
不要:
姓名
十二、联系人排序
很多通讯录:
都会:
A
B
C
D
按照:
拼音
字母
姓名
排序。
例如:
Apple
↓
Bob
↓
Cindy
↓
David
中文:
通常:
拼音排序
例如:
李四
↓
王五
↓
张三
十三、联系人过滤
例如:
企业:
只显示:
有手机号
过滤:
Contact
↓
phone!=null
或者:
有邮箱
过滤:
email!=null
还可以:
公司
部门
标签
过滤。
十四、搜索优化
错误方式:
输入:
张
↓
查询
↓
输入:
张三
↓
再次查询
每输入一个字:
都查数据库。
推荐:
用户停止输入
300ms
↓
开始查询
即:
Debounce(防抖)
流程:
输入
↓
等待300ms
↓
没有继续输入
↓
查询
这样:
数据库压力:
降低很多。
十五、缓存联系人
企业项目:
建议:
第一次:
Contacts Kit
↓
读取联系人
之后:
缓存:
Memory
或者:
Preferences
Database
搜索:
直接:
缓存
不用:
每次:
读取系统通讯录
效率:
提升很多。
十六、Repository 模式
推荐:
不要:
UI
↓
Contacts Kit
而是:
UI
↓
ViewModel
↓
Repository
↓
Contacts Kit
Repository:
负责:
查询
缓存
分页
搜索
更新
以后:
如果:
联系人:
来自:
网络
只需要:
修改:
Repository。
UI:
不用改。
十七、Service 层封装
例如:
ContactService
↓
queryAll()
↓
queryById()
↓
queryByName()
↓
queryByPhone()
↓
insert()
↓
update()
↓
delete()
页面:
调用:
const list =
await ContactService.queryByName("张");
即可。
十八、Mapper 转换
Contacts Kit:
返回:
System Contact
业务:
使用:
ContactModel
中间:
增加:
Mapper
流程:
System Contact
↓
Mapper
↓
Business Contact
后期:
接口:
变化:
只改:
Mapper。
十九、大数据优化
如果:
联系人:
50000
推荐:
分页
↓
懒加载
↓
缓存
↓
异步解析
↓
增量刷新
不要:
主线程
↓
解析50000对象
否则:
页面:
一定:
卡顿。
二十、异步读取
推荐:
读取联系人
↓
后台线程
↓
转换对象
↓
主线程刷新UI
不要:
UI线程
↓
查询数据库
↓
解析联系人
↓
刷新页面
这样会导致界面掉帧,影响用户体验。
二十一、企业级架构示意图
UI
│
▼
ContactViewModel
│
▼
ContactRepository
┌──────────────┐
│ │
▼ ▼
Contacts Kit Local Cache
│ │
└──────┬───────┘
▼
ContactMapper
▼
ContactModel
这种架构具有以下优点:
- UI 不依赖系统 API。
- 支持缓存与离线数据。
- 方便切换数据源(系统通讯录、企业服务器等)。
- 易于测试和维护。
二十二、常见性能问题
| 问题 | 原因 | 建议 |
|---|---|---|
| 打开通讯录很慢 | 一次查询全部联系人 | 使用分页加载 |
| 搜索卡顿 | 每输入一个字符都查询 | 使用防抖(Debounce) |
| 页面掉帧 | 主线程解析大量数据 | 放到后台线程处理 |
| 内存占用高 | 一次加载全部头像 | 按需加载头像,使用缓存 |
| 查询重复执行 | 每次进入页面都重新读取 | 建立本地缓存并按需刷新 |
二十三、面试高频问题
1. 为什么企业项目推荐使用 Repository?
答:
Repository 可以统一管理数据来源(系统通讯录、本地数据库、网络接口),降低 UI 与底层 API 的耦合,提高代码可维护性和可测试性。
2. 为什么不能一次读取所有联系人?
答:
大量联系人会占用较多内存,并增加对象创建和 UI 渲染开销。分页、懒加载和缓存可以有效降低内存压力,提高页面响应速度。
3. 联系人搜索为什么建议使用防抖?
答:
用户输入时如果每个字符都立即查询,会产生大量无效查询。使用约 300ms 的防抖,可以显著减少查询次数,提高性能和用户体验。
十六、HarmonyOS Contacts Kit 联系人增删改(CRUD)实战
前言
联系人管理不仅仅是"读取联系人"。
企业 OA、CRM、企业通讯录、手机助手、数据迁移等应用,还需要支持:
- 新增联系人
- 修改联系人
- 删除联系人
- 批量导入
- 数据同步
需要注意: Contacts Kit 的写操作通常需要相应权限,普通应用一般无法直接修改系统通讯录。本章主要介绍 Contacts Kit 的数据管理思路、企业级设计方式以及面试重点。
一、联系人 CRUD 流程
整个联系人管理流程如下:
UI
↓
ViewModel
↓
Repository
↓
ContactService
↓
Contacts Kit
↓
系统通讯录数据库
所有的联系人操作:
新增
修改
删除
最终都会落到:
Contacts Kit
完成数据写入。
二、新增联系人流程
新增联系人并不是:
new Contact()
↓
保存
而是:
创建联系人对象
↓
填写联系人信息
↓
参数校验
↓
写入数据库
↓
返回 Contact ID
通常:
成功
↓
生成新的联系人ID
以后:
所有修改:
都依赖:
Contact ID
三、新增联系人数据模型
例如:
export class ContactModel {
id:string=""
name:string=""
phones:string[]=[]
emails:string[]=[]
addresses:string[]=[]
company:string=""
note:string=""
}
新增时:
通常:
ID为空
保存成功:
系统:
自动生成ID
四、联系人校验
企业项目:
不要直接:
保存
建议:
增加:
Validator
例如:
Contact
↓
Validator
↓
合法
↓
保存
检查内容:
姓名不能为空
手机号是否合法
邮箱是否合法
重复联系人检查
公司名称长度
备注长度
例如:
if(model.name==""){
return
}
避免:
空联系人
写入数据库。
五、手机号校验
推荐:
统一工具类:
PhoneUtils
例如:
PhoneUtils.isMobile(phone)
判断:
13800138000
√
12345
×
abcdef
×
避免:
错误号码。
六、邮箱校验
例如:
abc@qq.com
√
hello@gmail.com
√
123@
×
@@@@
×
企业:
建议:
统一:
Regex
校验。
七、重复联系人检测
很多 App:
都会:
导入联系人
例如:
张三
13800138000
已经存在。
此时:
不要:
再次保存。
推荐:
手机号
↓
查数据库
↓
存在
↓
更新
不存在:
新增
八、联系人修改流程
修改:
实际上:
就是:
ID
↓
找到联系人
↓
修改字段
↓
保存
而不是:
姓名
↓
查找
例如:
Contact ID
↓
1001
↓
更新手机号
九、为什么不能根据姓名更新?
例如:
通讯录:
张三
张三
张三
三个:
同名。
如果:
update("张三")
到底:
更新哪个?
所以:
必须:
Contact ID
唯一。
十、修改联系人最佳实践
例如:
错误:
重新创建联系人
↓
覆盖
推荐:
查询联系人
↓
修改字段
↓
提交修改
这样:
不会:
丢失:
头像
生日
备注
网站
IM
等其它字段。
十一、删除联系人
删除流程:
联系人ID
↓
Contacts Kit
↓
删除数据库
↓
成功
例如:
ID=10001
↓
Delete
↓
联系人消失
删除:
建议:
弹窗:
确认删除?
取消
确定
不要:
直接删除。
十二、批量删除
例如:
企业:
通讯录
↓
多选
↓
删除
流程:
选中
↓
ID列表
↓
循环删除
不要:
一个联系人
↓
刷新一次UI
推荐:
全部删除
↓
统一刷新
性能:
更好。
十三、批量导入联系人
例如:
CSV:
姓名
手机号
邮箱
导入:
CSV
↓
解析
↓
ContactModel
↓
Validator
↓
保存
建议:
不要:
解析
↓
立即保存
推荐:
先:
全部校验
避免:
导入:
一半:
失败。
十四、联系人同步
企业:
经常:
服务器
↓
联系人
↓
手机
流程:
Server
↓
JSON
↓
Mapper
↓
ContactModel
↓
Contacts Kit
反向:
手机
↓
联系人
↓
JSON
↓
上传
形成:
双向同步。
十五、冲突处理
例如:
服务器:
手机号:
1380000
手机:
1389999
怎么办?
一般:
采用:
最后修改时间
例如:
Server
2026
↓
Phone
2025
↓
Server覆盖
或者:
提示用户:
保留哪个?
十六、联系人去重
很多:
CRM:
都会:
张三
13800138000
↓
再次导入
↓
重复
建议:
唯一键:
手机号
+
邮箱
不要:
姓名
因为:
姓名:
重复率:
太高。
十七、事务(Transaction)
批量:
新增:
1000 联系人。
错误:
保存一条
提交一次
推荐:
开启事务
↓
保存1000条
↓
统一提交
优点:
速度快
失败可回滚
数据一致
十八、Repository 实现
建议:
ContactRepository
↓
insert()
↓
update()
↓
delete()
↓
query()
页面:
永远:
不要:
直接:
Contacts Kit
例如:
await repository.insert(contact)
即可。
十九、Service 封装
例如:
ContactService
↓
save()
↓
update()
↓
remove()
↓
sync()
↓
import()
↓
export()
以后:
如果:
Contacts Kit:
API:
变化。
修改:
Service
即可。
二十、Undo(撤销)
很多:
系统通讯录:
删除:
并不是:
立即:
物理删除
推荐:
删除
↓
缓存30秒
↓
撤销
↓
恢复
例如:
联系人删除成功
【撤销】
用户体验:
更好。
二十一、批量操作优化
错误:
1000联系人
↓
刷新1000次UI
推荐:
1000联系人
↓
全部处理
↓
刷新一次
ArkUI:
性能:
提升:
非常明显。
二十二、企业项目架构
推荐:
UI
↓
ViewModel
↓
Repository
↓
Service
↓
Validator
↓
Mapper
↓
Contacts Kit
新增:
修改:
删除:
统一:
走:
Repository。
二十三、异常处理
例如:
保存失败:
Permission denied
处理:
提示:
当前应用
没有联系人写入权限
例如:
联系人不存在:
Contact Not Found
处理:
重新同步
或者
刷新联系人
不要:
直接:
崩溃。
二十四、性能优化
联系人:
10000条
新增:
不要:
循环:
insert()
推荐:
批量提交
事务
后台线程
删除:
同样:
批量删除
不要:
每删:
一条:
刷新一次页面。
二十五、面试高频问题
1、为什么修改联系人必须使用 Contact ID?
答:
因为姓名、手机号等字段都可能重复或发生变化,而 Contact ID 是系统生成的唯一标识,能够准确定位联系人,因此更新、删除等操作都应基于 Contact ID,而不是姓名。
2、为什么企业项目要增加 Validator 层?
答:
Validator 用于统一校验联系人数据,例如姓名、手机号、邮箱格式、重复联系人等,避免无效数据进入数据库,提高代码复用性,也便于后期维护。
3、为什么推荐 Repository + Service 双层设计?
答:
Repository 负责数据访问和数据源切换,Service 负责业务逻辑封装。这样可以降低 UI 与 Contacts Kit 的耦合,方便后续扩展到网络同步、本地缓存等场景,也更符合企业级架构设计。
本章总结
本章重点介绍了 Contacts Kit 在联系人 增删改(CRUD) 场景中的设计思路,包括:
- 联系人新增流程
- 修改与删除策略
- 批量导入与同步
- 数据校验(Validator)
- 去重策略
- Repository + Service 分层设计
- 大批量联系人操作性能优化
这些内容不仅适用于通讯录开发,也是企业级 HarmonyOS 项目中常见的架构设计方式。
十七、HarmonyOS 通讯录企业级实战(完整通讯录 Demo 架构设计)
本章开始进入实战阶段。
前面的章节我们学习了:
- Contacts Picker
- 通讯录权限体系
- Contact 数据模型
- 查询联系人
- 联系人 CRUD
- Service / Repository 分层
本章将这些知识组合起来,设计一个完整的企业级通讯录模块。
最终实现:
通讯录 App
├── 联系人列表
├── 搜索联系人
├── A-Z 索引导航
├── 联系人详情
├── 编辑联系人
├── 新增联系人
├── 删除联系人
├── 联系人缓存
└── 数据同步
一、企业通讯录整体架构
一个成熟 HarmonyOS 应用,不推荐:
Page
↓
Contacts Kit
这种写法。
原因:
页面直接依赖系统 API:
导致:
- 难测试
- 难维护
- 难扩展
- API变化影响大
推荐架构:
UI层
│
▼
ContactPage
│
▼
ViewModel层
│
▼
ContactRepository层
│
┌─────────┴─────────┐
▼ ▼
ContactService LocalCache
│ │
▼ ▼
Contacts Kit Preferences/Database
│
▼
System Contacts
二、项目目录设计
推荐结构:
entry
│
├── pages
│ │
│ ├── ContactPage.ets
│ ├── ContactDetailPage.ets
│ └── ContactEditPage.ets
│
├── viewmodel
│ │
│ └── ContactViewModel.ets
│
├── repository
│ │
│ └── ContactRepository.ets
│
├── service
│ │
│ └── ContactService.ets
│
├── model
│ │
│ └── ContactModel.ets
│
├── mapper
│ │
│ └── ContactMapper.ets
│
├── utils
│ │
│ ├── PhoneUtils.ets
│ └── SearchUtils.ets
│
└── cache
│
└── ContactCache.ets
三、联系人业务模型设计
不要直接使用系统 Contact。
定义自己的模型:
export class ContactModel {
id:string = ""
name:string = ""
avatar:string = ""
phones:string[] = []
emails:string[] = []
company:string = ""
department:string = ""
note:string = ""
}
为什么?
因为:
系统对象:
Contact
属于:
系统层
而:
ContactModel
属于:
业务层
二者不要混合。
四、ContactMapper设计
负责:
系统对象转换。
结构:
Contacts Kit
↓
Contact
↓
Mapper
↓
ContactModel
示例:
export class ContactMapper {
static convert(data:any):ContactModel{
let model =
new ContactModel()
model.id =
data.id
model.name =
data.name
model.avatar =
data.portraitUri
model.phones =
data.phoneNumbers
model.emails =
data.emails
return model
}
}
五、ContactService设计
Service负责:
直接操作 Contacts Kit。
例如:
export class ContactService {
async queryAll(){
}
async queryById(id:string){
}
async insert(contact:ContactModel){
}
async update(contact:ContactModel){
}
async delete(id:string){
}
}
业务页面:
不知道:
Contacts Kit。
只知道:
ContactService
六、Repository设计
Repository:
负责:
数据管理。
例如:
export class ContactRepository {
private service =
new ContactService()
async getContacts(){
return await
this.service.queryAll()
}
}
以后:
加入缓存:
只改这里。
例如:
Repository
↓
先查缓存
↓
没有
↓
查系统通讯录
↓
更新缓存
七、ViewModel设计
ViewModel负责:
页面状态。
例如:
export class ContactViewModel {
contacts:ContactModel[]=[]
async load(){
this.contacts =
await repository.getContacts()
}
}
页面:
只负责显示。
八、通讯录列表页面设计
效果:
--------------------------------
🔍 搜索联系人
A
Alice
Andy
B
Bob
C
Charlie
--------------------------------
数据:
@State
contacts:ContactModel[]=[]
List:
List(){
ForEach(
this.contacts,
item=>{
ListItem(){
Text(item.name)
}
})
}
九、联系人头像加载
不要:
直接:
Image(url)
大量联系人:
会导致:
卡顿
推荐:
缓存:
头像URI
↓
ImageCache
↓
Image
例如:
Image(
ContactCache.getAvatar(item.id)
)
十、A-Z 字母索引实现
企业通讯录常见:
右侧:
A
B
C
D
...
Z
实现思路:
联系人:
增加:
firstLetter
例如:
张三
↓
Z
模型:
增加:
firstLetter:string
排序:
A
B
C
...
Z
十一、拼音首字母转换
中文:
需要:
中文
↓
拼音
↓
首字母
例如:
张三
↓
ZHANG SAN
↓
Z
工具类:
class PinYinUtils{
static getFirstLetter(
name:string
){
}
}
十二、联系人搜索
搜索:
不要直接:
每次输入:
查询。
错误:
输入:
张
查询一次
输入:
张三
再次查询
推荐:
防抖:
输入
↓
等待300ms
↓
没有继续输入
↓
执行搜索
十三、SearchUtils
封装:
class SearchUtils{
static match(
keyword:string,
name:string
){
}
}
支持:
张
↓
张三
zs
↓
张三
十四、联系人详情页
点击:
列表:
进入:
ContactDetailPage
显示:
头像
姓名
公司
手机号
邮箱
地址
备注
页面参数:
不要传:
整个对象。
推荐:
传:
id
例如:
/detail?id=10001
然后:
重新查询。
为什么?
因为:
联系人可能变化。
十五、联系人编辑页面
结构:
姓名
手机号
邮箱
公司
备注
保存
保存流程:
点击保存
↓
ViewModel
↓
Validator
↓
Repository
↓
Service
↓
Contacts Kit
↓
刷新列表
十六、状态管理设计
推荐:
页面:
不要自己维护:
多个变量。
例如:
错误:
name
phone
email
list
散落。
推荐:
统一:
ContactState
例如:
class ContactState{
loading:boolean=false
list:ContactModel[]=[]
keyword:string=""
}
十七、加载状态
联系人读取:
可能耗时。
因此:
增加:
Loading
状态。
流程:
进入页面
↓
loading=true
↓
读取联系人
↓
loading=false
↓
显示列表
UI:
加载中...
或者
空状态
十八、错误状态
例如:
权限失败:
显示:
无法读取联系人
请检查权限
网络同步失败:
显示:
同步失败
点击重试
不要:
直接:
Toast错误。
十九、空数据处理
联系人为空:
不要:
白屏。
显示:
暂无联系人
点击添加
二十、万级联系人性能方案
如果:
10000联系人。
必须:
使用:
1. 分页
50条
↓
下一页
2. 懒加载
滑动到底
↓
加载更多
3. 图片缓存
头像:
按需加载。
4. 数据缓存
第一次:
读取:
保存:
Database
之后:
优先:
本地。
二十一、完整数据流
最终:
一次打开通讯录:
流程:
ContactPage
↓
ContactViewModel.load()
↓
Repository.getContacts()
↓
Cache查询
↓
ContactService
↓
Contacts Kit
↓
ContactMapper
↓
ContactModel
↓
State更新
↓
ArkUI刷新
二十二、企业级最佳实践总结
推荐:
✅ 页面不直接调用 Contacts Kit
✅ 使用 Service 封装系统能力
✅ 使用 Repository 管理数据
✅ 使用 Mapper 隔离系统模型
✅ 使用 ViewModel 管理状态
✅ 使用缓存提升性能
✅ 使用分页处理大量联系人
✅ 使用防抖优化搜索
二十三、本章面试重点
Q1:为什么不要在页面直接调用 Contacts Kit?
因为页面直接依赖系统 API,会导致代码耦合严重,不利于维护和测试。应该通过 Service 和 Repository 隔离底层实现。
Q2:为什么详情页推荐传 ID 而不是整个对象?
因为联系人数据可能发生变化,传 ID 后进入详情页重新查询,可以保证数据最新,同时减少页面参数传递。
Q3:万级联系人如何优化?
主要通过:
- 分页加载
- 懒加载
- 头像缓存
- 数据缓存
- 异步读取
- 防抖搜索
降低内存和渲染压力。
十八、HarmonyOS 通讯录 A-Z 索引 + 搜索性能优化实战
本章进入通讯录项目中最常见、也是最容易出现性能问题的部分:
- 联系人列表
- 中文拼音排序
- A-Z 快速定位
- 搜索过滤
- 万级联系人性能优化
微信、企业微信、钉钉、飞书等通讯录类应用,都大量使用类似方案。
一、为什么通讯录需要 A-Z 索引?
普通 List:
张三
李四
王五
赵六
当联系人:
10000+
用户查找:
非常困难。
因此通讯录通常设计:
左侧:
联系人列表
右侧:
A
B
C
D
...
Z
用户点击:
M
立即跳转:
M
↓
马某
梅某
孟某
二、通讯录数据结构设计
原始联系人:
{
name:"张三"
}
不足。
需要增加:
首字母
例如:
{
name:"张三",
firstLetter:"Z"
}
完整模型:
export class ContactModel {
id:string=""
name:string=""
firstLetter:string=""
phones:string[]=[]
avatar:string=""
}
三、中文姓名转拼音
例如:
输入:
张三
转换:
ZHANG SAN
然后:
取:
Z
作为索引。
流程:
中文姓名
↓
拼音转换
↓
大写
↓
首字符
↓
索引
例如:
王五
↓
WANG WU
↓
W
四、为什么首字母需要缓存?
很多开发者会这样:
List显示
↓
每次计算拼音
例如:
10000联系人:
每次刷新:
10000次拼音转换
性能非常差。
正确:
首次加载:
联系人
↓
计算firstLetter
↓
保存
以后:
直接:
contact.firstLetter
五、联系人分组模型
为了快速显示:
推荐:
转换成:
Map
结构:
A
├ Alice
└ Andy
B
├ Bob
C
├ Charlie
代码:
Map<string,ContactModel[]>
例如:
{
A:[
Alice,
Andy
],
B:[
Bob
]
}
六、生成分组数据
工具方法:
function groupContacts(
list:ContactModel[]
){
let map =
new Map<string,ContactModel[]>()
list.forEach(item=>{
let key =
item.firstLetter
if(!map.has(key)){
map.set(
key,
[]
)
}
map.get(key)?.push(item)
})
return map
}
结果:
A
B
C
D
七、List嵌套Section设计
通讯录页面:
通常:
List
↓
Section
↓
ListItem
结构:
List
A
Alice
Andy
B
Bob
ArkUI:
类似:
List(){
ForEach(
sections,
section=>{
Text(section.title)
ForEach(
section.items,
item=>{
})
})
}
八、右侧索引栏设计
UI:
A
B
C
D
E
...
Z
通常:
Column()
实现。
例如:
Column(){
ForEach(
letters,
letter=>{
Text(letter)
})
}
九、点击字母跳转
核心:
点击字母
↓
找到对应Section
↓
List滚动
需要:
保存:
letter
↓
index
映射。
例如:
{
A:0,
B:20,
C:35
}
十、List滚动控制
使用:
ListScroller
保存:
let scroller =
new Scroller()
点击:
B
执行:
scroller.scrollToIndex(20)
效果:
B
↓
立即定位
十一、搜索功能设计
通讯录搜索:
一般支持:
中文搜索
输入:
张
匹配:
张三
张伟
张强
拼音搜索
输入:
zs
匹配:
张三
手机号搜索
输入:
138
匹配:
13800138000
十二、搜索数据结构优化
不要:
每次:
遍历:
10000联系人
推荐建立:
搜索字段:
class SearchContact{
name:string
pinyin:string
phone:string
}
例如:
张三:
name:
张三
pinyin:
zhangsan
phone:
13800138000
搜索:
直接:
三个字段匹配
十三、防抖搜索实现
错误:
输入:
张
查询
张三
查询
张三丰
查询
产生:
大量无效查询。
正确:
等待:
300ms
流程:
输入
↓
启动Timer
↓
300ms
↓
执行搜索
伪代码:
let timer:number=0
function search(keyword:string){
clearTimeout(timer)
timer=setTimeout(()=>{
doSearch(keyword)
},300)
}
十四、Trie 前缀搜索优化
如果:
联系人:
10万
普通:
for循环
已经慢。
可以使用:
Trie树。
结构:
张
|
三
|
丰
搜索:
复杂度:
O(length)
例如:
输入:
zh
快速找到:
张
赵
周
十五、搜索索引建立
初始化:
联系人
↓
建立Index
↓
保存
例如:
Map<string,List<Contact>>
索引:
包括:
姓名
拼音
手机号
例如:
138
↓
张三
zs
↓
张三
十六、大数据 List 优化
万级联系人:
不要:
ForEach
全部渲染。
推荐:
LazyForEach
区别:
ForEach:
全部创建
LazyForEach:
显示多少
创建多少
例如:
10000联系人:
屏幕:
20条。
Lazy:
只创建:
20~40条。
十七、LazyForEach 示例
数据:
class ContactDataSource
implements IDataSource
{
}
页面:
LazyForEach(
dataSource,
item=>{
ListItem(){
Text(item.name)
}
})
优势:
明显降低:
- 内存
- 创建时间
- UI压力
十八、头像优化
通讯录最大性能问题:
不是文字。
而是:
头像。
错误:
10000联系人
↓
加载10000头像
正确:
当前可见区域
↓
加载头像
策略:
懒加载
↓
缓存
↓
淘汰
十九、缓存策略
推荐:
LRU缓存。
结构:
最近使用
↓
保留
↓
不常用删除
例如:
缓存:
100个头像
超过:
删除最旧。
二十、联系人列表刷新策略
不要:
新增联系人:
重新读取全部
正确:
局部更新:
新增
↓
插入List
↓
刷新一个Item
删除:
删除Item
修改:
更新Item
二十一、搜索状态管理
推荐:
状态:
class ContactState{
list:ContactModel[]=[]
searchText:string=""
loading:boolean=false
empty:boolean=false
}
页面:
根据状态显示:
loading
↓
list
↓
empty
二十二、完整通讯录性能方案
最终方案:
联系人数据
↓
一次解析
↓
生成拼音
↓
建立索引
↓
缓存
↓
LazyForEach显示
↓
防抖搜索
↓
头像懒加载
二十三、性能指标参考
优秀通讯录:
应该达到:
| 操作 | 目标 |
|---|---|
| 打开列表 | <500ms |
| 搜索响应 | <100ms |
| A-Z跳转 | 即时 |
| 滑动 | 60FPS |
| 头像加载 | 无明显卡顿 |
二十四、常见面试题
1、为什么通讯录需要保存 firstLetter?
因为中文联系人无法直接按字母排序,通过拼音转换得到首字母,可以快速实现 A-Z 索引定位,提高大量联系人查找效率。
2、为什么使用 LazyForEach?
因为联系人数量可能达到几万条,LazyForEach 可以根据可视区域创建组件,避免一次性创建大量 UI 节点,提高性能。
3、搜索为什么需要建立索引?
如果每次搜索都遍历全部联系人,数据量大时性能下降。提前建立姓名、拼音、手机号索引,可以快速定位匹配联系人。
本章总结
本章完成了企业通讯录最核心的交互能力:
- 中文拼音排序
- A-Z索引
- Section列表
- 快速定位
- 多字段搜索
- 防抖优化
- Trie搜索思想
- LazyForEach性能优化
- 头像缓存策略
这些技术不仅用于通讯录,也适用于:
- 商品列表
- 好友列表
- 城市选择器
- 企业组织架构
- IM好友列表
十九、HarmonyOS 通讯录完整项目实战(联系人详情 + 编辑 + 新增页面)
本章开始实现真正的通讯录业务页面。
前面章节已经完成:
- 联系人数据模型
- Contacts Service
- Repository架构
- 列表展示
- A-Z索引
- 搜索优化
现在继续实现:
- 联系人详情页
- 联系人编辑页
- 新增联系人
- 数据回传
- 页面刷新
一、页面结构设计
完整通讯录通常包含:
通讯录主页
|
|
↓
联系人详情页
|
|
↓
联系人编辑页
页面关系:
ContactPage
|
| 点击联系人
|
↓
ContactDetailPage
|
| 点击编辑
|
↓
ContactEditPage
二、Navigation 路由设计
HarmonyOS NEXT 推荐:
Navigation
+
RouterMap
+
页面级路由
不要:
大量使用:
router.push()
传统跳转。
推荐结构:
Navigation
├── ContactPage
├── ContactDetailPage
└── ContactEditPage
三、联系人主页跳详情
列表:
张三
李四
王五
点击:
张三
携带:
contactId
进入:
DetailPage
为什么传ID?
因为:
联系人数据可能变化。
错误:
params.contact = contact
问题:
进入详情:
数据可能已经过期。
正确:
params.id="10001"
详情页:
重新查询。
四、详情页面结构
效果:
--------------------
头像
张三
手机号
13800138000
邮箱
xxx@qq.com
公司
Huawei
备注
重要客户
编辑
--------------------
五、ContactDetailPage 状态设计
@Component
export struct ContactDetailPage {
@State
contact:
ContactModel =
new ContactModel()
}
进入页面:
执行:
加载联系人
流程:
页面创建
↓
获取id
↓
Repository查询
↓
更新State
↓
UI刷新
六、详情页加载联系人
示例:
async loadData(){
let result =
await repository
.queryById(this.id)
this.contact=result
}
注意:
不要:
页面里面:
直接:
Contacts Kit
保持分层。
七、详情页UI实现
例如:
Column({space:20}){
Image(
this.contact.avatar
)
Text(
this.contact.name
)
Text(
this.contact.phones[0]
)
Button("编辑")
}
ArkUI特点:
修改:
this.contact
自动刷新。
八、编辑页面设计
编辑页面:
类似表单:
姓名
[ 张三 ]
手机号
[13800138000]
邮箱
[xxx@qq.com]
公司
[Huawei]
保存
数据:
@State
name:string=""
@State
phone:string=""
@State
email:string=""
九、为什么编辑页不用直接修改对象?
错误:
contact.name="李四"
问题:
用户点击返回:
数据已经改变。
推荐:
编辑页:
维护:
临时数据
保存:
再提交。
流程:
ContactModel
↓
EditState
↓
用户修改
↓
点击保存
↓
提交Repository
十、表单初始化
进入编辑页:
aboutToAppear(){
this.name =
this.contact.name
this.phone =
this.contact.phones[0]
}
效果:
打开:
显示原数据。
十一、输入框设计
例如:
姓名:
TextInput({
placeholder:"请输入姓名"
})
.onChange(value=>{
this.name=value
})
手机号:
TextInput({
placeholder:"手机号"
})
邮箱:
TextInput({
placeholder:"邮箱"
})
十二、表单校验
保存之前:
必须:
校验
流程:
点击保存
↓
Validator
↓
通过
↓
保存
十三、ContactValidator设计
例如:
export class ContactValidator{
static validate(
model:ContactModel
):boolean{
if(model.name==""){
return false
}
if(model.phones.length==0){
return false
}
return true
}
}
十四、手机号校验
例如:
static validatePhone(
phone:string
){
let reg =
/^1[3-9]\d{9}$/
return reg.test(phone)
}
检查:
13800138000
√
123456
×
十五、邮箱校验
例如:
static validateEmail(
email:string
){
let reg =
/^[A-Za-z0-9]+@[A-Za-z0-9]+\.[A-Za-z]+$/
return reg.test(email)
}
十六、保存联系人
点击:
保存
流程:
EditPage
↓
ViewModel
↓
Validator
↓
Repository
↓
ContactService
↓
Contacts Kit
代码:
async save(){
let model =
new ContactModel()
model.name=this.name
model.phones=[
this.phone
]
await repository.update(model)
}
十七、保存成功后的页面返回
保存:
成功:
需要:
返回详情页
同时:
通知:
列表刷新。
方案:
方案一
重新查询
简单:
返回页面
↓
aboutAppear
↓
重新加载
适合:
普通项目。
方案二
EventHub通知
企业项目:
常用。
流程:
编辑页
↓
发送事件
↓
详情页监听
↓
刷新
十八、EventHub刷新机制
例如:
事件:
contact_changed
编辑:
eventHub.emit(
"contact_changed"
)
列表:
监听:
eventHub.on(
"contact_changed",
()=>{
reload()
}
)
优点:
页面解耦。
十九、新增联系人页面
新增:
与编辑:
区别:
编辑
↓
已有ID
新增
↓
没有ID
模型:
新增:
id=""
保存:
调用:
insert()
二十、新增流程
点击新增
↓
打开AddPage
↓
填写资料
↓
校验
↓
insert
↓
返回列表
↓
刷新
二十一、联系人删除
详情页:
按钮:
删除联系人
流程:
点击删除
↓
确认弹窗
↓
Repository.delete()
↓
返回列表
不要:
直接删除。
二十二、删除确认弹窗
推荐:
确定删除张三?
取消
删除
原因:
联系人数据:
属于重要个人信息。
误删成本高。
二十三、页面间数据通信总结
HarmonyOS 常用方式:
| 方式 | 适用 |
|---|---|
| Navigation参数 | 页面传递数据 |
| EventHub | 页面通知 |
| 状态管理 | 共享状态 |
| Repository | 统一数据源 |
推荐:
详情:
传ID
刷新:
EventHub
数据:
Repository
二十四、完整业务链路
最终:
新增:
AddPage
↓
Validator
↓
Repository
↓
Service
↓
Contacts Kit
↓
EventHub
↓
List刷新
编辑:
Detail
↓
Edit
↓
Update
↓
EventHub
↓
Refresh
删除:
Detail
↓
Confirm
↓
Delete
↓
Back
↓
Refresh
二十五、企业级最佳实践
1. 不直接修改对象
使用:
Draft Model
保存时提交。
2. 所有写操作走Repository
不要:
页面:
调用:
insert()
3. 保存前统一校验
不要:
每个页面:
自己判断。
4. 页面只负责UI
业务:
放:
ViewModel
Service
Repository
二十六、本章面试问题
1、为什么详情页传ID而不是对象?
因为对象可能已经过期,传ID可以保证进入详情页后重新获取最新数据,同时减少参数传递。
2、为什么编辑页面需要临时状态?
避免用户编辑过程中直接修改原始数据。如果用户取消编辑,可以直接丢弃临时状态。
3、EventHub适合什么场景?
适合跨页面、跨组件的事件通知,例如联系人新增、修改、删除后通知其它页面刷新。
本章总结
本章完成了通讯录核心页面设计:
- Navigation页面跳转
- 联系人详情
- 联系人编辑
- 新增联系人
- 删除联系人
- 表单校验
- EventHub刷新机制
- MVVM业务流程
到这里,一个完整通讯录App的核心功能已经基本形成。
第二十章 HarmonyOS 通讯录数据同步与云端架构设计(企业级实战)
前面几章,我们已经完成了通讯录本地 CRUD 和页面开发。
但是在企业项目中,仅仅操作本地通讯录远远不够。
例如:
- 企业OA
- 企业微信
- 飞书
- CRM
- ERP
- 客户管理系统
都需要将本地通讯录与云端通讯录进行同步。
本章重点讲解企业级通讯录同步架构,这是 HarmonyOS 企业开发和面试中的高频知识点。
一、为什么需要数据同步?
很多初学者认为:
通讯录
↓
读取
↓
显示
就结束了。
实际上企业项目的数据流通常如下:
企业后台
│
▼
Contact API Server
↗ ↘
HarmonyOS App Web管理后台
↘ ↗
企业数据库
例如:
员工A修改手机号:
13800138000
↓
13812345678
那么:
- 手机端要更新
- Web后台要更新
- 其它设备也要更新
否则:
数据就不一致。
二、通讯录同步模型
推荐采用:
Server
↓
JSON
↓
Repository
↓
Mapper
↓
ContactModel
↓
Contacts Kit
反向:
Contacts Kit
↓
ContactModel
↓
JSON
↓
Server
这样:
系统模型和服务器模型完全隔离。
三、为什么不要直接同步系统 Contact?
错误:
System Contact
↓
JSON
↓
上传
原因:
系统 Contact:
可能包含:
- 系统字段
- URI
- 本地ID
- 无意义字段
服务器:
根本不需要。
推荐:
Contact
↓
Mapper
↓
DTO
↓
JSON
四、DTO(Data Transfer Object)
企业项目:
一般都会增加:
ContactDTO
例如:
export class ContactDTO{
id:string=""
name:string=""
phone:string=""
email:string=""
}
为什么?
因为:
DTO:
只保存:
服务器需要的数据。
五、本地ID和服务器ID
很多新人:
容易混淆:
Contact ID
实际上:
企业项目:
通常:
两个ID。
例如:
Local ID
10001
↓
Server ID
A8F123456
不要:
混用。
推荐:
class ContactModel{
localId:string=""
serverId:string=""
}
六、同步流程设计
推荐:
如下流程:
本地修改联系人
↓
Repository
↓
加入同步队列
↓
后台同步
↓
服务器返回成功
↓
更新同步状态
而不是:
修改
↓
立即请求服务器
为什么?
网络:
可能:
断开。
七、同步状态设计
联系人:
增加:
syncStatus:number
例如:
0
已同步
-----------------
1
待同步
-----------------
2
同步失败
-----------------
3
同步中
以后:
UI:
即可显示:
☁
同步中...
八、离线模式
企业项目:
一定支持:
无网络
例如:
地铁:
没有信号
用户:
修改联系人:
仍然:
成功。
流程:
修改联系人
↓
本地数据库
↓
syncStatus=待同步
↓
恢复网络
↓
自动同步
用户:
几乎:
感觉不到:
网络问题。
九、同步队列(Sync Queue)
推荐:
建立:
SyncQueue
例如:
新增联系人
↓
Queue
↓
修改联系人
↓
Queue
↓
删除联系人
↓
Queue
后台:
依次:
处理。
模型:
class SyncTask{
type:string
contactId:string
}
十、为什么需要同步队列?
例如:
用户:
一分钟:
连续:
修改:
手机号
↓
姓名
↓
邮箱
如果:
立即:
请求:
服务器。
就会:
产生:
三个HTTP请求
推荐:
队列:
等待
↓
合并
↓
一次上传
性能:
提升:
很多。
十一、增量同步
错误:
同步
↓
10000联系人
每次:
都上传。
推荐:
最后修改时间
↓
变化联系人
↓
上传
例如:
昨天:
10000联系人
今天:
修改2个
↓
上传2个
效率:
极高。
十二、版本号设计
推荐:
联系人:
增加:
version:number
例如:
Version
1
↓
修改
↓
2
↓
修改
↓
3
服务器:
根据:
Version:
判断:
是否冲突。
十三、更新时间设计
模型:
增加:
updateTime:number
例如:
2026-07-10
15:20:18
同步:
依据:
更新时间。
十四、冲突解决
例如:
手机:
修改:
手机号
1380000
Web:
同时:
修改:
1389999
怎么办?
方案一:
最后修改:
优先。
updateTime
大的
覆盖
方案二:
服务器:
提示:
发现冲突
请选择
适合:
CRM。
十五、软删除
不要:
立即:
Delete
推荐:
deleted=true
例如:
class ContactModel{
deleted:boolean=false
}
同步:
服务器:
之后:
真正删除。
十六、数据加密
联系人:
属于:
隐私数据。
推荐:
上传:
HTTPS
敏感字段(如手机号、邮箱)可以根据业务要求进行脱敏展示或在服务端进行加密存储。客户端是否需要额外加密,应结合实际安全方案设计。
十七、本地缓存
推荐:
保存:
Database
不要:
每次:
Contacts Kit
↓
重新查询
流程:
启动App
↓
本地数据库
↓
后台同步
↓
更新数据
用户:
打开:
更快。
十八、同步架构
企业推荐:
UI
│
▼
ContactRepository
│ │
▼ ▼
Local Database SyncManager
│ │
▼ ▼
Contacts Kit REST API
│ │
└────┬─────┘
▼
Server
十九、SyncManager设计
例如:
class SyncManager{
sync()
upload()
download()
resolveConflict()
}
以后:
所有:
同步:
统一:
管理。
二十、自动同步时机
推荐:
App:
以下情况:
同步:
启动
↓
恢复网络
↓
用户登录
↓
下拉刷新
↓
后台定时
不要:
每次:
打开页面:
同步。
二十一、大型企业架构
推荐:
UI
↓
ViewModel
↓
Repository
↓
Local DB
↓
SyncManager
↓
REST API
↓
Server
Contacts Kit:
只是:
其中:
一个:
数据源。
二十二、企业项目最佳实践
✅ 使用 Repository 管理数据
✅ 使用 SyncManager 管理同步
✅ 使用 DTO 与服务器通信
✅ 使用 Mapper 隔离模型
✅ 增量同步,不全量同步
✅ 使用同步队列减少请求
✅ 本地数据库优先
✅ 后台自动同步
二十三、面试高频问题
1、为什么企业项目需要 SyncManager?
答:
SyncManager 将同步逻辑集中管理,负责上传、下载、冲突处理、失败重试、增量同步等功能,避免同步逻辑分散在多个页面或业务模块中,提升系统可维护性。
2、为什么推荐增量同步?
答:
全量同步会带来大量网络流量和数据库操作,而增量同步只处理发生变化的数据,能够显著降低带宽消耗和同步时间,提升系统性能。
3、为什么需要本地数据库,而不是每次读取 Contacts Kit?
答:
直接访问系统通讯录可能存在性能开销,也不利于离线使用。本地数据库可以提供更快的查询速度,支持离线访问,并作为同步过程中的缓存层和数据源。
本章总结
本章完成了企业级通讯录云同步架构设计,包括:
- 通讯录同步模型
- DTO 设计
- SyncManager 架构
- 同步队列
- 增量同步
- 版本号管理
- 冲突处理
- 离线同步
- 本地缓存
- 企业级最佳实践
至此,我们已经覆盖了 HarmonyOS 通讯录开发的大部分核心知识,包括权限体系、Contacts Picker、Contacts Kit、数据模型、CRUD、MVVM 架构、列表优化以及企业级同步方案。
第二十一章 HarmonyOS Contacts Kit 常见异常、踩坑总结与最佳实践(完结篇)
到这里,整个 HarmonyOS 通讯录开发 已经基本完成。
本章将结合大量企业项目经验,总结 Contacts Kit 开发过程中最容易遇到的问题,以及企业级开发规范和面试高频考点。
一、Contacts Kit 开发全流程回顾
整个通讯录模块的数据流如下:
用户操作
│
▼
ContactPage
│
▼
ContactViewModel
│
▼
ContactRepository
┌─────────┴─────────┐
▼ ▼
Local Database ContactService
│
▼
Contacts Kit
│
▼
System Contacts
整个架构遵循:
- UI 与系统 API 解耦
- Repository 统一数据入口
- Service 封装系统能力
- ViewModel 管理页面状态
- Mapper 转换数据模型
- Validator 校验业务数据
二、常见异常一:未申请权限
最常见的问题就是:
读取联系人失败
原因:
没有申请:
ohos.permission.READ_CONTACTS
或者:
用户点击:
拒绝
导致读取失败。
正确流程:
检查权限
↓
没有权限
↓
申请权限
↓
申请成功
↓
读取联系人
不要直接调用 Contacts Kit,否则很容易抛出权限异常。
三、常见异常二:用户永久拒绝权限
例如:
拒绝
↓
不再询问
此时:
再次申请:
不会弹框。
正确处理:
提示用户
↓
打开系统设置
↓
手动开启权限
不要无限循环申请权限。
四、常见异常三:联系人为空
很多开发者:
默认:
一定有联系人。
实际上:
新手机
↓
没有联系人
此时:
返回:
[]
不是:
异常。
页面:
应该:
显示:
暂无联系人
而不是:
程序崩溃。
五、常见异常四:头像为空
很多联系人:
没有头像。
例如:
portraitUri
↓
null
错误:
Image(contact.avatar)
容易导致:
图片加载失败
推荐:
if (contact.avatar == "") {
显示默认头像
}
六、常见异常五:手机号为空
有的人:
只有:
姓名
没有:
手机号
例如:
联系人
↓
张三
↓
phone=[]
不要:
直接:
phones[0]
应该:
先判断:
phones.length > 0
七、常见异常六:邮箱为空
同理:
邮箱:
可能不存在。
推荐:
统一工具:
ContactFormatter
例如:
getFirstPhone()
getFirstEmail()
getAvatar()
统一处理空值。
八、常见异常七:联系人字段不完整
不同联系人:
字段:
完全不同。
例如:
联系人A:
姓名
手机号
联系人B:
姓名
邮箱
公司
地址
不要:
假设:
所有字段:
一定存在。
九、常见异常八:联系人重复
企业项目:
经常:
导入:
Excel:
CSV:
导致:
重复联系人
推荐:
建立:
唯一规则:
手机号
+
邮箱
不要:
只根据:
姓名。
十、常见异常九:大量联系人卡顿
例如:
联系人:
20000+
错误:
ForEach()
一次:
创建:
所有Item。
正确:
LazyForEach
配合:
分页。
十一、常见异常十:搜索卡顿
例如:
输入:
张
每输入:
一个字:
遍历:
10000联系人
推荐:
- 防抖(Debounce)
- 拼音缓存
- Trie 前缀树(超大数据量)
- 建立搜索索引
十二、Contacts Picker 与 Contacts Kit 如何选择?
很多开发者容易混淆:
| 场景 | 推荐 |
|---|---|
| 用户主动选择一个联系人 | Contacts Picker |
| 读取整个通讯录 | Contacts Kit |
| 企业通讯录同步 | Contacts Kit |
| 分享联系人 | Contacts Picker |
| CRM 导入通讯录 | Contacts Kit |
简单来说:
- Picker:用户主动选择
- Kit:程序批量读取和管理
十三、Repository 为什么必不可少?
错误架构:
Page
↓
Contacts Kit
问题:
- 页面无法复用
- 无法缓存
- 无法测试
- API 修改影响所有页面
推荐:
Page
↓
ViewModel
↓
Repository
↓
Service
↓
Contacts Kit
Repository 统一管理:
- 缓存
- 网络
- 数据库
- Contacts Kit
真正做到:
一个入口
多个数据源
十四、Mapper 为什么重要?
系统:
Contact
服务器:
ContactDTO
页面:
ContactModel
三者:
不要混用。
推荐:
Contact
↓
Mapper
↓
Model
↓
DTO
解耦:
系统层:
业务层:
网络层。
十五、性能优化 Checklist
企业项目建议逐项检查:
| 优化项 | 是否建议 |
|---|---|
| Repository 分层 | ✅ |
| ViewModel 管理状态 | ✅ |
| Mapper 转换模型 | ✅ |
| Validator 校验数据 | ✅ |
| LazyForEach | ✅ |
| 防抖搜索 | ✅ |
| 拼音缓存 | ✅ |
| 头像缓存 | ✅ |
| 本地数据库缓存 | ✅ |
| 增量同步 | ✅ |
| SyncManager | ✅ |
| DTO 隔离网络模型 | ✅ |
十六、安全与隐私建议
通讯录属于用户敏感信息,开发时建议遵循以下原则:
1. 最小权限原则
只申请真正需要的联系人权限。
不要因为未来可能用到而提前申请。
2. 数据最小化
例如:
服务器:
只需要:
姓名
手机号
不要上传:
生日
地址
备注
头像
除非业务明确需要。
3. 加密传输
所有联系人同步:
必须:
HTTPS
避免明文传输。
4. 不打印隐私日志
错误:
console.info(contact.phone)
发布版本:
不要输出:
手机号:
邮箱:
身份证:
等敏感信息。
5. 提供隐私说明
首次读取联系人前,应向用户说明:
- 为什么需要通讯录权限
- 将读取哪些数据
- 数据如何使用
- 是否上传服务器
提高用户信任度,也有助于满足应用市场审核要求。
十七、完整项目目录(推荐)
entry
├── pages
│ ├── ContactPage
│ ├── ContactDetailPage
│ ├── ContactEditPage
│ └── ContactAddPage
│
├── model
│ ├── ContactModel
│ ├── ContactDTO
│ └── ContactState
│
├── repository
│ └── ContactRepository
│
├── service
│ ├── ContactService
│ └── SyncManager
│
├── mapper
│ └── ContactMapper
│
├── validator
│ └── ContactValidator
│
├── cache
│ └── ContactCache
│
├── database
│ └── ContactDao
│
├── network
│ └── ContactApi
│
├── utils
│ ├── PhoneUtils
│ ├── SearchUtils
│ ├── PinYinUtils
│ └── PermissionUtils
│
└── common
├── Constant
└── Logger
这种目录结构适合中大型 HarmonyOS 企业项目。
十八、30 道 HarmonyOS Contacts Kit 面试高频题(精选)
基础
- Contacts Kit 和 Contacts Picker 有什么区别?
- 如何申请通讯录权限?
- 如何读取联系人列表?
- 如何获取联系人手机号?
- 如何获取联系人头像?
进阶
- 如何设计 ContactModel?
- 为什么要使用 Repository?
- 为什么需要 Mapper?
- 为什么需要 Validator?
- 如何设计联系人缓存?
性能
- 为什么使用 LazyForEach?
- 如何优化万级联系人列表?
- 如何实现拼音搜索?
- 如何实现 A-Z 索引?
- 如何避免搜索卡顿?
企业
- 如何实现联系人同步?
- 什么是增量同步?
- 为什么需要 SyncManager?
- 如何处理同步冲突?
- 如何设计同步队列?
架构
- MVVM 在通讯录中的作用?
- Repository 为什么优于直接访问 Contacts Kit?
- 为什么要分层?
- 如何进行单元测试?
- 如何设计 DTO?
安全
- 如何保护联系人隐私?
- 为什么不能打印联系人日志?
- 如何进行权限说明?
- 如何设计最小权限原则?
- 如何满足应用市场隐私审核?
十九、全文知识体系总结
整个《HarmonyOS 获取通讯录信息》专题已经完整覆盖:
第一部分:基础能力
- Contacts Picker 使用
- Contacts Kit 基础
- 权限申请
- 权限检查
- 权限异常处理
第二部分:联系人数据
- Contact 数据模型
- 联系人查询
- 联系人详情
- 联系人 CRUD
- 字段解析
第三部分:企业开发
- MVVM
- Repository
- Service
- Mapper
- Validator
- DTO
- 缓存
第四部分:性能优化
- LazyForEach
- 拼音排序
- A-Z 索引
- 搜索优化
- 图片缓存
- 防抖
- 分页加载
第五部分:企业架构
- 本地数据库
- SyncManager
- 增量同步
- 冲突处理
- 离线模式
- 数据安全
- 企业通讯录设计
更多推荐


所有评论(0)