HarmonyOS6 - 鸿蒙读取系统联系人实战案例
开发环境为:
开发工具:DevEco Studio 6.0.1 Release
API版本是:API21
本文所有代码都已使用模拟器测试成功!
1. 需求
首先我们设定如下需求:
- 点击添加,就弹框显示手机联系人列表
- 勾选联系人,勾选完毕后,点击确认,勾选的联系人数据,就显示到APP中这个页面中
- 这个页面中,可以多选联系人,点击删除,就删除了联系人数据
- 联系人数据存储在APP本地数据库中
- 应用重启后联系人数据依然需要存在
2. 分析
针对上面需求,我的分析思路如下:
- 页面结构设计
设计联系人列表页面的整体布局结构,包括顶部导航栏、联系人列表区域和底部操作栏。
- 数据模型定义
定义联系人数据接口(FriendsItem)和电话信息接口(PhoneItem),用于规范数据类型。
- 数据库操作实现
- 封装RDB数据库工具类(RdbTools)
- 实现数据增删改查功能
- 添加防重复逻辑,通过手机号判断联系人是否已存在
- 核心功能开发
(1)联系人展示
- 页面加载时从数据库查询并显示所有联系人
- 使用List组件渲染联系人列表
- 添加数据加载完成提示
(2)联系人添加
- 集成系统联系人选择组件
- 批量添加联系人功能
- 实时反馈添加结果(新增成功/重复提醒)
(3)多选删除功能
- 长按联系人进入多选模式
- 显示勾选框供用户选择
- 实现全选/取消全选功能
- 批量删除选中的联系人
- 用户体验优化
- 添加多选模式下的视觉反馈(选中背景色变化)
- 操作结果提示(弹窗消息)
- 底部按钮动态切换(添加/取消、选择/删除)
- 手势交互(长按触发多选)
- 异步处理优化
- 解决异步操作顺序问题,确保日志正确显示
- 使用async/await确保数据处理的顺序性
- 优化Promise链式调用,避免状态混乱
- 状态管理
- 使用@State管理组件状态
- 维护多选模式状态
- 管理选中联系人ID集合
- 确保UI状态与数据同步
3. 编码
根据以上思路,我们开始编写代码
首先新建页面【ContactsDetail.ets】,代码如下:
js
import { router } from "@kit.ArkUI";
import { contact } from "@kit.ContactsKit";
import { myTools } from "../common/MyTools";
import { ObjectModel } from "../common/rdb/ObjectModel";
import RdbTools from "../common/rdb/RdbTools";
export interface FriendsItem {
id?: number;
name?: string;
phone?: string;
}
export interface PhoneItem {
labelId?: number;
labelName?: string;
phoneNumber?: string;
}
const TAG = 'ContactsDetail'
/**
* 联系人列表(可添加系统联系人)
*/
interface PhoneItemInterface {
isNew: boolean;
phone: string;
}
@Entry
@Component
struct ContactsDetail {
// 联系人数据集合
@State friendsList: FriendsItem[] = [];
// 选中的联系人ID集合
@State selectedIds: Set<number> = new Set<number>();
// 是否处于多选模式
@State isMultiSelectMode: boolean = false;
//新增联系人信息到RDB数据库
async addDataToRDB(friend: FriendsItem): Promise<PhoneItemInterface> {
return new Promise(async (resolve) => {
//新增之前,先查询数据库中是否已经存在该手机号
await RdbTools.queryData(friend.phone).then(async res => {
console.info(TAG, '查询手机号[' + friend.phone + ']结果=' + JSON.stringify(res))
if (res.length > 0) {
console.info(TAG, '查询手机号[' + friend.phone + ']数据库已存在,无需新增!')
resolve({ isNew: false, phone: friend.phone as string }); // 重复添加
} else {
console.info(TAG, '查询手机号[' + friend.phone + ']数据库不存在,需要新增!')
//数据保存到RDB数据库
await RdbTools.insertData({
id: friend.id,
name: friend.name,
phone: friend.phone,
} as ObjectModel)
//加到列表集合中
this.friendsList.push(friend);
resolve({ isNew: true, phone: friend.phone as string }); // 新添加
}
})
});
}
//查询RDB数据库中的所有联系人数据
async queryAllDB() {
await RdbTools.queryData('').then(res => {
console.info(TAG, '查询数据库所有联系人信息=' + JSON.stringify(res))
if (res) {
this.friendsList = res
console.info(TAG, 'this.friendsList=' + JSON.stringify(this.friendsList))
myTools.alertMsg('数据加载完毕!')
}
})
}
aboutToAppear(): void {
//需要等待RDB数据库初始化完毕后才能查询数据库,所以这里进入页面后,1秒之后才查询数据库
setTimeout(() => {
this.queryAllDB();
}, 1000)
}
// 选择联系人
async selectContacts() {
// 选择联系人时的筛选条件 (是否多选)
let contactSelectionOptions: contact.ContactSelectionOptions = { isMultiSelect: true };
// 调用通讯录选择组件,让用户选择需要传入APP的通讯录联系人
let promise = contact.selectContacts(contactSelectionOptions);
await promise.then(async (data) => {
// 用户选择确认之后,会在此处收到回调
console.info(TAG, `selectContacts success: data->${JSON.stringify(data)}`);
if (data && data.length > 0) {
// 用于收集结果的数组
const chongfuPhone: string[] = [];
const newPhone: string[] = [];
// 使用for循环按顺序处理每个联系人,并等待每个处理完成
for (const contactItem of data) {
let phoneItemArray: Array<PhoneItem> = JSON.parse(JSON.stringify(contactItem.phoneNumbers));
let friend: FriendsItem = {
id: contactItem.id,
name: contactItem.name?.fullName,
phone: phoneItemArray[0].phoneNumber
};
// 等待每个联系人处理完成
const result = await this.addDataToRDB(friend);
// 根据结果分类
if (result.isNew) {
newPhone.push(result.phone);
} else {
chongfuPhone.push(result.phone);
}
}
console.info(TAG, `处理完成后的this.friendsList=${JSON.stringify(this.friendsList)}`);
console.info(TAG, `处理完成后的newPhone.length=${newPhone.length}`);
console.info(TAG, `处理完成后的chongfuPhone.length=${chongfuPhone.length}`);
//弹框提示新增结果
if (newPhone.length > 0) {
if (chongfuPhone.length > 0) {
myTools.alertMsg('新增' + newPhone.length + '个联系人成功!重复手机号有:' +
chongfuPhone.toString(), 5000);
} else {
myTools.alertMsg('新增' + newPhone.length + '个联系人成功!', 5000);
}
} else {
if (chongfuPhone.length > 0) {
myTools.alertMsg('新增0个联系人成功!重复手机号有:' +
chongfuPhone.toString(), 5000);
}
}
}
}).catch((err: BusinessError) => {
console.error(TAG, `selectContacts fail: err->${JSON.stringify(err)}`);
});
}
//删除RDB数据库中的联系人
async deletePhone(item: FriendsItem) {
await RdbTools.deleteData(item.id).then(() => {
this.queryAllDB();
});
}
// 批量删除选中的联系人
async deleteSelectedContacts() {
if (this.selectedIds.size === 0) {
myTools.alertMsg('请先选择要删除的联系人!');
return;
}
myTools.alertDialogRollback('请确认:', `确定删除选中的${this.selectedIds.size}个联系人吗?`, async () => {
try {
// 批量删除选中的联系人
const deletePromises = Array.from(this.selectedIds).map(async id => {
await RdbTools.deleteData(id).then(() => {
});
}
);
await Promise.all(deletePromises);
// 删除成功后更新列表
await this.queryAllDB();
// 清空选中集合
this.selectedIds.clear();
// 退出多选模式
this.isMultiSelectMode = false;
myTools.alertMsg('删除成功!');
} catch (error) {
console.error(TAG, '批量删除失败:', error);
myTools.alertMsg('删除失败,请重试!');
}
}, () => {
// 取消操作
});
}
// 切换多选模式
toggleMultiSelectMode() {
this.isMultiSelectMode = !this.isMultiSelectMode;
if (!this.isMultiSelectMode) {
// 退出多选模式时清空选中状态
this.selectedIds.clear();
}
}
// 取消多选模式
cancelMultiSelectMode() {
this.isMultiSelectMode = false;
this.selectedIds.clear();
}
// 选中/取消选中联系人
toggleSelectContact(item: FriendsItem) {
if (!item.id) {
return;
}
if (this.selectedIds.has(item.id)) {
this.selectedIds.delete(item.id);
} else {
this.selectedIds.add(item.id);
}
// 如果所有项都取消选中,退出多选模式
if (this.selectedIds.size === 0) {
this.isMultiSelectMode = false;
}
}
// 全选/取消全选
toggleSelectAll() {
if (this.selectedIds.size === this.friendsList.length) {
// 如果已经全选,则取消全选
this.selectedIds.clear();
} else {
// 全选
this.selectedIds = new Set(this.friendsList.filter(item => item.id).map(item => item.id!));
}
}
// 勾选框组件
@Builder
checkboxBuilder(item: FriendsItem) {
Checkbox()
.select(this.selectedIds.has(item.id!))
.selectedColor('#007DFF')
.width(24)
.height(24)
.margin({ right: 12 })
.onChange((checked: boolean) => {
if (checked) {
this.selectedIds.add(item.id!);
} else {
this.selectedIds.delete(item.id!);
}
})
}
build() {
Column() {
// 顶部导航栏
Row() {
Image($r('app.media.back'))
.width(24)
.height(24)
.margin({ left: 16 })
.onClick(() => {
if (this.isMultiSelectMode) {
// 如果处于多选模式,点击返回退出多选模式
this.cancelMultiSelectMode();
} else {
router.back();
}
})
Text(this.isMultiSelectMode ? `已选中${this.selectedIds.size}项` : '常用联系人')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#000000')
.layoutWeight(1)
.textAlign(TextAlign.Center)
// 多选模式下的操作按钮
if (this.isMultiSelectMode) {
Text(this.selectedIds.size === this.friendsList.length ? '取消全选' : '全选')
.fontSize(16)
.fontColor('#007DFF')
.margin({ right: 16 })
.onClick(() => {
this.toggleSelectAll();
})
} else {
// 非多选模式下的占位
Row()
.width('50%')
.height(24)
.margin({ right: 16 })
}
}
.width('100%')
.height(56)
.backgroundColor('#FFFFFF')
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.Start)
// 内容区域
List() {
ForEach(this.friendsList, (item: FriendsItem, index: number) => {
ListItem() {
Row() {
// 多选模式下显示勾选框
if (this.isMultiSelectMode) {
this.checkboxBuilder(item)
}
Column() {
Text(item.name)
.fontSize(16)
.fontColor('#333333')
.margin({ bottom: 4 })
Text(item.phone)
.fontSize(14)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
Blank()
// 非多选模式下显示箭头图标
if (!this.isMultiSelectMode) {
Image($r('app.media.next'))
.width(20)
.height(20)
.objectFit(ImageFit.Cover)
}
}
.width('100%')
.padding({
top: 16,
bottom: 16,
left: 20,
right: 20
})
.backgroundColor(this.isMultiSelectMode && this.selectedIds.has(item.id!) ? '#F0F8FF' : '#FFFFFF')
.onClick(() => {
if (this.isMultiSelectMode) {
// 多选模式下点击切换选中状态
this.toggleSelectContact(item);
}
})
.gesture(
LongPressGesture({ duration: 500 })
.onAction(() => {
// 长按进入多选模式并选中当前项
if (!this.isMultiSelectMode) {
this.isMultiSelectMode = true;
}
if (item.id) {
this.toggleSelectContact(item);
}
})
)
}
})
}
.width('95%')
.borderRadius(8)
.margin({ top: 12 })
.backgroundColor('#FFFFFF')
.divider({
strokeWidth: 1,
startMargin: 20,
endMargin: 20,
color: '#ffe9f0f0'
})
.layoutWeight(1)
// 底部Tab栏
Row() {
// 左侧按钮:多选模式下显示"取消",非多选模式下显示"添加"
Column() {
if (this.isMultiSelectMode) {
// 多选模式下的取消按钮
Text('×') // 取消符号
.fontSize(24)
.fontColor('#333333')
Text('取消')
.fontSize(12)
.fontColor('#333333')
.margin({ top: 4 })
} else {
// 非多选模式下的添加按钮
Text('+')
.fontSize(24)
.fontColor('#333333')
Text('添加')
.fontSize(12)
.fontColor('#333333')
.margin({ top: 4 })
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
.height('100%')
.onClick(() => {
if (this.isMultiSelectMode) {
// 多选模式下点击取消按钮,退出多选模式
this.cancelMultiSelectMode();
} else {
// 非多选模式下点击添加按钮,添加联系人
let isContactsAvailable = canIUse('SystemCapability.Applications.Contacts');
if (isContactsAvailable) {
this.selectContacts();
} else {
console.info(TAG, 'The device does not support the ability to select contacts.');
}
}
})
// 右侧按钮:多选模式下显示"删除",非多选模式下显示"选择"
Column() {
if (this.isMultiSelectMode) {
// 多选模式下的删除按钮
Text('\u{1F5D1}') // Unicode 垃圾箱符号
.fontSize(22)
.fontColor(this.selectedIds.size > 0 ? '#FF3B30' : '#999999')
Text(`删除(${this.selectedIds.size})`)
.fontSize(12)
.fontColor(this.selectedIds.size > 0 ? '#FF3B30' : '#999999')
.margin({ top: 4 })
} else {
// 非多选模式下的选择按钮
Text('\u{2705}') // Unicode 对勾符号
.fontSize(22)
.fontColor('#333333')
Text('选择')
.fontSize(12)
.fontColor('#333333')
.margin({ top: 4 })
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
.height('100%')
.onClick(() => {
if (this.isMultiSelectMode) {
// 多选模式下执行批量删除
if (this.selectedIds.size > 0) {
this.deleteSelectedContacts();
} else {
myTools.alertMsg('请先选择要删除的联系人!');
}
} else {
// 非多选模式下进入多选模式
this.toggleMultiSelectMode();
}
})
}
.width('100%')
.height(70)
.backgroundColor('#F5F5F5')
.padding({ top: 8, bottom: 8 })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
RdbTools工具类代码如下:
js
import { relationalStore } from "@kit.ArkData"
import Logger from "../Logger";
import { myTools } from "../MyTools";
import { ObjectModel } from "./ObjectModel";
/**
* 操作本地RDB数据库的工具类
*/
export default class RdbTools {
private static rdbStore: relationalStore.RdbStore;
static setStore(store: relationalStore.RdbStore) {
RdbTools.rdbStore = store;
}
static getStore(): relationalStore.RdbStore {
return RdbTools.rdbStore;
}
// 表名
static tableName: string = 'phone_book'
//数据库文件名
static rdbName: string = 'phoneBook.db'
// SQL 语法:(SQL语法的数据类型关键词不一样,可通过AI生成SQL语句)
// 解释说明:
// CREATE TABLE IF NOT EXISTS 如果表不存在才创建新的表
// INTEGER -> number INTEGER 整数型 FLOAT 浮点数
// PRIMARY KEY 主键(唯一标识)
// AUTOINCREMENT 自增
// TEXT -> string 字符串型
// NOT NULL 不允许空
static sqlCreate: string = `CREATE TABLE IF NOT EXISTS ${RdbTools.tableName} (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
phone TEXT NOT NULL
)`
// 创建数据库文件
static async initDataBase() {
try {
// 获取操作数据库的管理对象(如果数据库文件不存在,会自动创建数据库文件)
const store = RdbTools.rdbStore;
// 执行创建表的语句 execute 执行
store.executeSql(RdbTools.sqlCreate)
Logger.debug('数据库文件创建成功!')
} catch (error) {
Logger.error('创建数据库文件,发生异常=' + JSON.stringify(error))
}
}
// 新增单条数据
static async insertData(item: ObjectModel) {
Logger.debug('新增数据=' + JSON.stringify(item))
try {
// 获取操作数据库的对象
const store = RdbTools.rdbStore;
Logger.debug('store=' + JSON.stringify(store))
// 添加一条数据
const id = await store.insert(RdbTools.tableName, item)
Logger.debug('新增单条数据成功,id=' + id);
} catch (error) {
myTools.alertMsg('新增数据,发生异常:' + JSON.stringify(error))
Logger.error('新增单条数据,发生异常=' + JSON.stringify(error))
}
}
// 新增多条数据
static async insertArrayData(item: Array<ObjectModel>) {
try {
// 获取操作数据库的对象
const store = RdbTools.rdbStore;
// 添加一条数据
const id = await store.batchInsert(RdbTools.tableName, item)
Logger.debug('新增多条数据成功,id=' + id)
} catch (error) {
myTools.alertMsg('批量新增数据,发生异常:' + JSON.stringify(error))
Logger.error('新增多条数据,发生异常=' + JSON.stringify(error))
}
}
//删除数据
static async deleteData(id?: number, ids?: Array<number>) {
Logger.debug('需要删除的id=' + id)
Logger.debug('需要删除的ids=' + ids)
try {
// 获取操作数据库对象
const store = RdbTools.rdbStore;
const predicates = new relationalStore.RdbPredicates(RdbTools.tableName)
// 注意!!!:记得添加 predicates 限定条件,否者会删除所有数据
// predicates.equalTo('id', 2) // equalTo 删除一条
if (id) {
predicates.equalTo('id', id) // equalTo 删除一条
} else {
predicates.in('id', ids) // in 批量删除
}
const affectedRows = await store.delete(predicates)
Logger.debug('删除数据,受影响行数=' + affectedRows)
if (affectedRows == 0) {
myTools.alertMsg('数据删除失败!')
} else {
myTools.alertMsg('数据删除成功!')
}
} catch (error) {
myTools.alertMsg('删除数据,发生异常:' + JSON.stringify(error))
Logger.error('删除数据,发生异常=' + JSON.stringify(error))
}
}
//更新数据
static async updateData(item: ObjectModel) {
if (!item.id) {
AlertDialog.show({
message: 'ID缺失,无法修改!'
})
return;
}
try {
// 获取操作数据库对象
const store = RdbTools.rdbStore;
// 谓词(条件)
const predicates = new relationalStore.RdbPredicates(RdbTools.tableName)
// 注意!!!:记得添加 predicates 限定条件,否者修改全部
predicates.equalTo('id', item.id)
const affectedRows = await store.update(item, predicates)
Logger.debug('修改数据 - 受影响行数:' + affectedRows)
} catch (error) {
myTools.alertMsg('更新数据,发生异常:' + JSON.stringify(error))
Logger.debug('更新数据,发生异常=' + JSON.stringify(error))
}
}
// 查询数据
static async queryData(phone?: string) {
// 准备一个数组,用于存储数据库提取的数据
const list: ObjectModel [] = []
try {
Logger.debug('>>>>>>>>>>>>> 1')
// 获取操作数据库的对象
const store = RdbTools.rdbStore;
Logger.debug('>>>>>>>>>>>>> 2')
// 谓词(条件)
const predicates = new relationalStore.RdbPredicates(RdbTools.tableName)
Logger.debug('>>>>>>>>>>>>> 3')
predicates.orderByDesc('id') // 倒序(由大到小,常用于排序)
Logger.debug('>>>>>>>>>>>>> 4')
if (phone) {
Logger.debug('>>>>>>>>>>>>> 5')
predicates.like('phone', '%' + phone + '%') // 模糊匹配查询
}
// predicates.orderByAsc('id') // 正序(小到大,常用于排序)
// predicates.equalTo('id', 1) // 等于(常用于详情页)
// predicates.in('id', [1, 3, 5]) // 查找多项(常用批量删除)
// predicates.like('title', '%哈%') // 模糊匹配(常用于搜索)
// resultSet 结果集
Logger.debug('>>>>>>>>>>>>> 6')
const resultSet = await store.query(predicates)
Logger.debug('>>>>>>>>>>>>> 7')
// resultSet.goToNextRow() 指针移动到下一行
Logger.debug('查询RDB数据,结果集数量条数=' + resultSet.rowCount)
while (resultSet.goToNextRow()) {
// 移动指针的时候提取数据,按列下标提取数据
list.push({
// resultSet.getColumnIndex() 根据列名称获取下标(索引)
id: resultSet.getLong(resultSet.getColumnIndex('id')),
name: resultSet.getString(resultSet.getColumnIndex('name')),
phone: resultSet.getString(resultSet.getColumnIndex('phone')),
})
}
Logger.debug('查询RDB数据,结果集=' + JSON.stringify(list))
} catch (error) {
myTools.alertMsg('查询数据,发生异常:' + JSON.stringify(error))
Logger.error('查询数据,发生异常=' + JSON.stringify(error))
}
return list;
}
}
EntryAbility.ets文件中需要【onWindowStageCreate】方法中初始化数据库,代码如下:
js
relationalStore.getRdbStore(this.context, {
name: RdbTools.rdbName, // 数据库文件名
securityLevel: relationalStore.SecurityLevel.S1 // 数据库安全级别
}, (err, store) => {
if (err) {
Logger.error(`Failed to get RdbStore. Code:${err.code}, message:${err.message}`);
return;
}
Logger.info('Succeeded in getting RdbStore.');
//保存store, 方便后面我们对数据库的操作
RdbTools.setStore(store)
RdbTools.initDataBase()
});
4. 演示
然后使用真机测试即可,最终成果演示视频如下:
鸿蒙读取系统联系人
最后
- 希望本文对你有所帮助!
- 本人如有任何错误或不当之处,请留言指出,谢谢!