鸿蒙UI懒加载,微信联系人如何为?
摘要:【学习目标】 掌握 双层 LazyForEach 分组架构(分组 + 列表项); 实现 右侧字母索引栏与列表联动; 掌握 侧滑删除、修改备注 列表交互; 理解 @Observed + @ObjectLin
【学习目标】
掌握 双层 LazyForEach 分组架构(分组 + 列表项);
实现 右侧字母索引栏与列表联动;
掌握 侧滑删除、修改备注 列表交互;
理解 @Observed + @ObjectLink 深层响应式;
掌握 @Reusable 正确使用规则。
一、工程目录结构
WechatContactDemo/
├── entry/src/main/ets/
│ ├── components/
│ │ ├── ContactGroupComponent.ets // 分组组件(不复用)
│ │ └── ContactItemComponent.ets // 列表项组件(开启复用)
│ ├── datasource/
│ │ ├── BasicDataSource.ets // 通用数据源基类
│ │ ├── ContactListDataSource.ets // 联系人列表数据源
│ │ └── ContactGroupDataSource.ets // 分组数据源
│ ├── model/
│ │ └── ContactModel.ets // 数据模型
│ ├── utils/
│ │ └── JsonUtil.ets // JSON工具
│ └── pages/
│ └── Index.ets // 主页面
二、通用泛型数据源(基类)
// datasource/BasicDataSource.ets
/**
* 通用列表数据源基类
* 实现 IDataSource 接口,封装列表刷新、增删改查等通用逻辑
* 所有列表都可以继承此类,减少重复代码
*/
export class BasicDataSource<T> implements IDataSource {
// 数据源数组,存储列表真实数据
protected dataArray: T[] = [];
// 数据监听器,通知列表刷新
private listeners: DataChangeListener[] = [];
// 获取列表总数量
totalCount(): number {
return this.dataArray.length;
}
// 根据索引获取数据
getData(index: number): T {
return this.dataArray[index];
}
// 获取全部数据
getAllData(): T[] {
return this.dataArray;
}
// 注册列表数据监听
registerDataChangeListener(listener: DataChangeListener): void {
if (!this.listeners.includes(listener)) {
this.listeners.push(listener);
}
}
// 注销监听
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
// 通知列表:指定索引新增数据
notifyDataAdd(index: number): void {
this.listeners.forEach(l => l.onDataAdd(index));
}
// 通知列表:指定索引数据变化
notifyDataChange(index: number): void {
this.listeners.forEach(l => l.onDataChange(index));
}
// 通知列表:删除指定索引数据
notifyDataDelete(index: number): void {
this.listeners.forEach(l => l.onDataDelete(index));
}
// 通知列表:数据移动
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(l => l.onDataMove(from, to));
}
// 通知列表:全局刷新
notifyDataReload(): void {
this.listeners.forEach(l => l.onDataReloaded());
}
// 批量操作通知
notifyDatasetChange(ops: DataOperation[]): void {
this.listeners.forEach(l => l.onDatasetChange(ops));
}
// 末尾添加一条数据
pushData(data: T): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
// 删除指定索引数据
deleteData(index: number): void {
if (index < 0 || index >= this.dataArray.length) return;
this.dataArray.splice(index, 1);
this.notifyDataDelete(index);
}
// 更新指定索引数据
updateData(index: number, newData: T): void {
if (index < 0 || index >= this.dataArray.length) return;
this.dataArray[index] = newData;
this.notifyDataChange(index);
}
// 移动数据位置
moveData(from: number, to: number): void {
if (from < 0 || from >= this.dataArray.length || to < 0 || to >= this.dataArray.length) return;
const item = this.dataArray.splice(from, 1)[0];
this.dataArray.splice(to, 0, item);
this.notifyDataMove(from, to);
}
// 全量替换数据
reloadData(newDataList: T[]): void {
this.dataArray = newDataList;
this.notifyDataReload();
}
}
三、联系人数据模型
// model/ContactModel.ets
import { util } from "@kit.ArkTS";
/**
* 联系人实体模型
* 使用 @Observed 实现响应式,修改后自动刷新UI
*/
@Observed
export class ContactItem {
name: string; // 姓名
avatar: string; // 头像
uuid: string; // 唯一标识,作为列表Key,保证渲染稳定
constructor(name: string, avatar: string, uuid: string = util.generateRandomUUID(true)) {
this.name = name;
this.avatar = avatar;
this.uuid = uuid;
}
}
/**
* 联系人分组模型(A-Z)
* 每个分组拥有独立的数据源,用于内层 LazyForEach
*/
@Observed
export class ContactGroup {
initial: string; // 分组字母(A/B/C...)
list: ContactItem[]; // 本组联系人列表
dataSource: ContactListDataSource; // 本组列表数据源(双层LazyForEach关键)
uuid: string; // 分组唯一标识
constructor(initial: string, list: ContactItem[], dataSource: ContactListDataSource, uuid: string = util.generateRandomUUID(true)) {
this.initial = initial;
this.list = list;
this.dataSource = dataSource;
this.uuid = uuid;
}
}
四、分组数据源 & 列表数据源
// datasource/ContactListDataSource.ets
import { ContactItem } from "../model/ContactModel";
import { BasicDataSource } from "./BasicDataSource";
/**
* 联系人列表数据源(内层 LazyForEach 使用)
*/
export class ContactListDataSource extends BasicDataSource<ContactItem> {
constructor(initData: ContactItem[] = []) {
super();
this.dataArray = initData;
}
// 根据uuid查找索引
indexOf(item: ContactItem): number {
return this.dataArray.findIndex(i => i.uuid === item.uuid);
}
}
// datasource/ContactGroupDataSource.ets
import { ContactGroup, ContactItem } from "../model/ContactModel";
import { BasicDataSource } from "./BasicDataSource";
import { ContactListDataSource } from "./ContactListDataSource";
import { JsonUtil } from "../utils/JsonUtil";
/**
* 分组数据源(外层 LazyForEach 使用)
* 管理 A-Z 所有分组
*/
export class ContactGroupDataSource extends BasicDataSource<ContactGroup> {
private letterList: string[] = []; // 右侧索引字母列表
constructor(initData: ContactGroup[] = []) {
super();
this.dataArray = initData;
this.updateLetterList();
}
// 从本地JSON加载联系人
async loadContacts(context: Context): Promise<void> {
if (!context) return;
try {
const jsonStr = await JsonUtil.readRawFileJson(context, "contacts.json");
const groups = JSON.parse(jsonStr);
this.dataArray = [];
// 遍历分组,为每个分组创建独立数据源
for (const g of groups) {
const dataSource = new ContactListDataSource();
g.list.forEach(item => {
dataSource.pushData(new ContactItem(item.name, item.avatar));
});
this.dataArray.push(new ContactGroup(g.initial, g.list, dataSource));
}
this.notifyDataReload();
this.updateLetterList();
} catch (err) {
console.error("加载数据失败:", JSON.stringify(err));
}
}
// 删除分组中的某一行,组为空则删除本组
deleteContactForGroup(section: number, row: number) {
const group = this.dataArray[section];
if (!group) return;
group.dataSource.deleteData(row);
// 本组数据为空,删除分组
if (group.dataSource.totalCount() === 0) {
this.deleteData(section);
}
this.updateLetterList();
}
// 根据分组查找索引
indexOf(group: ContactGroup): number {
return this.dataArray.findIndex(i => i.uuid === group.uuid);
}
// 更新右侧索引字母
private updateLetterList(): void {
this.letterList = this.dataArray.map(g => g.initial);
}
// 获取字母表
getLetterList(): string[] {
return this.letterList;
}
}
五、联系人列表项
// components/ContactItemComponent.ets
import { ContactItem } from '../model/ContactModel';
/**
* 联系人列表项组件
* 【性能关键】这里开启 @Reusable,因为 item 数量大、滑动频繁
* 【不推荐嵌套】父组件不加 @Reusable
*/
@Reusable
@Component
export struct ContactItemComponent {
// 响应式绑定联系人数据
@ObjectLink item: ContactItem;
// 删除回调
onDelete?: (item: ContactItem) => void;
// 修改备注回调
onEditRemark?: (item: ContactItem) => void;
/**
* 组件复用时触发
* 长列表优化核心:滑出屏幕的组件会被回收,滑入时直接复用更新数据
* @ObjectLink 系统会自动更新不需要我们手动赋值
*/
aboutToReuse(params: Record<string, Object | null | undefined>): void {
console.info(`[组件复用] ContactItem -> ${this.item.name}`);
}
/**
* 组件首次创建时触发
*/
aboutToAppear(): void {
console.info(`[组件创建] ContactItem -> ${this.item.name}`);
}
/**
* 组件被回收进缓存池时触发
*/
aboutToRecycle(): void {
console.info(`[组件回收] ContactItem -> ${this.item.name}`);
}
/**
* 侧滑菜单:备注 + 删除
*/
@Builder
SwipeMenu() {
Row() {
// 备注按钮
Text("备注")
.backgroundColor("#007AFF")
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.height('100%')
.onClick(() => {
const item = this.item
this.onEditRemark?.(item)
});
// 删除按钮
Text("删除")
.backgroundColor("#FF3B30")
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.height('100%')
.layoutWeight(1)
.onClick(() => {
const item = this.item
this.onDelete?.(item)
});
}.width(150);
}
build() {
ListItem() {
Row({ space: 15 }) {
// 头像
Image(this.item.avatar)
.width(44)
.height(44)
.borderRadius(6);
// 姓名
Text(this.item.name)
.fontSize(17)
.fontColor('#000');
}
.width('100%')
.padding({ left: 15, top: 12, bottom: 12 });
}
// 右侧侧滑菜单
.swipeAction({ end: this.SwipeMenu() });
}
}
六、分组组件
// components/ContactGroupComponent.ets
import { ContactGroup, ContactItem } from '../model/ContactModel'
import { ContactItemComponent } from './ContactItemComponent';
/**
* 联系人分组组件
* 包含 ListItemGroup + 内层 LazyForEach
*/
@Component
export struct ContactGroupComponent {
@ObjectLink contactGroup: ContactGroup;
onDelete?: (contactGroup: ContactGroup, item: ContactItem) => void;
onEditRemark?: (contactGroup: ContactGroup, item: ContactItem) => void;
/**
* 分组标题构建
*/
@Builder
groupHeaderBuilder(title: string) {
Text(title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('100%')
.padding({ left: 16, top: 8, bottom: 8 })
.backgroundColor('#F7F7F7');
}
/**
* 组件首次创建
*/
aboutToAppear(): void {
console.info(`[组件创建] ContactGroup -> ${this.contactGroup.initial}`);
}
/**
* 组件消失
*/
aboutToDisappear(): void {
console.info(`[组件消失] ContactGroup -> ${this.contactGroup.initial}`);
}
build() {
ListItemGroup({ header: this.groupHeaderBuilder(this.contactGroup.initial) }) {
LazyForEach(
this.contactGroup.dataSource,
(item: ContactItem) => {
ContactItemComponent({
item: item,
onDelete: (item: ContactItem) => {
const group = this.contactGroup;
this.onDelete?.(group, item);
},
onEditRemark: (item: ContactItem) => {
const group = this.contactGroup;
this.onEditRemark?.(group, item);
}
});
},
(item: ContactItem) => item.uuid
);
}
.divider({ strokeWidth: 0.5, color: '#E5E5E5', startMargin: 74 });
}
}
七、主页面(双层 LazyForEach + 索引联动 + 下拉刷新)
// pages/Index.ets
import { ContactGroupComponent } from '../components/ContactGroupComponent';
import { ContactGroup, ContactItem } from '../model/ContactModel';
import { ContactGroupDataSource } from '../datasource/ContactGroupDataSource';
@Entry
@Component
struct Index {
private scroller: Scroller = new Scroller();
private context: Context | undefined = this.getUIContext().getHostContext();
private contactGroupDataSource: ContactGroupDataSource = new ContactGroupDataSource();
@State isRefreshing: boolean = false;
@State promptText: string = "下拉开始刷新...";
@State letterList: string[] = [];
async aboutToAppear() {
if (this.context) {
await this.contactGroupDataSource.loadContacts(this.context);
this.letterList = this.contactGroupDataSource.getLetterList();
}
}
build() {
RelativeContainer() {
// 顶部标题
Text("微信通讯录")
.fontSize(18)
.fontWeight(FontWeight.Medium)
.textAlign(TextAlign.Center)
.padding(12)
.backgroundColor('#FFF')
.id('constant_title')
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
left: { anchor: '__container__', align: HorizontalAlign.Start },
right: { anchor: '__container__', align: HorizontalAlign.End }
});
// 下拉刷新
Refresh({ refreshing: $$this.isRefreshing, promptText: this.promptText }) {
List({ scroller: this.scroller }) {
LazyForEach(
this.contactGroupDataSource,
(group: ContactGroup) => {
ContactGroupComponent({
contactGroup: group,
onEditRemark:(contactGroup: ContactGroup, item: ContactItem)=>{
item.name = "新名字"
const section = this.contactGroupDataSource.indexOf(contactGroup)
const row = contactGroup.dataSource.indexOf(item)
if (row < 0 || section < 0) {
return
}
this.contactGroupDataSource.notifyDataChange(section)
},
onDelete: (contactGroup: ContactGroup,item: ContactItem)=> {
const section = this.contactGroupDataSource.indexOf(contactGroup)
const row = contactGroup.dataSource.indexOf(item)
if (row < 0 || section < 0) {
return
}
this.contactGroupDataSource.deleteContactForGroup(section, row);
const letterList= this.contactGroupDataSource.getLetterList();
this.letterList = letterList
}
});
},
(group: ContactGroup) => group.uuid);
}
.sticky(StickyStyle.Header)
.scrollBar(BarState.Off)
.cachedCount(5)
.width('100%')
.layoutWeight(1);
}
.alignRules({
top: { anchor: 'constant_title', align: VerticalAlign.Bottom },
left: { anchor: '__container__', align: HorizontalAlign.Start },
bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
right: { anchor: '__container__', align: HorizontalAlign.End }
})
.onStateChange((state: RefreshStatus) => {
switch (state) {
case RefreshStatus.Inactive:
this.promptText = '';
break;
case RefreshStatus.Drag:
this.promptText = '下拉刷新';
break;
case RefreshStatus.OverDrag:
this.promptText = '松手立即刷新';
break;
case RefreshStatus.Refresh:
this.promptText = '正在刷新...';
break;
case RefreshStatus.Done:
this.promptText = '刷新完成';
break;
default:
this.promptText = '';
}
})
.onRefreshing(() => {
setTimeout(async () => {
if (this.context) {
await this.contactGroupDataSource.loadContacts(this.context);
const letterList= this.contactGroupDataSource.getLetterList();
this.letterList = letterList
}
this.isRefreshing = false;
}, 1500);
});
AlphabetIndexer({
arrayValue: this.letterList,
selected: 0
})
.color(Color.Black)
.selectedColor(Color.White)
.selectedBackgroundColor('#007AFF')
.onSelect((index: number) => {
if (index >= 0 && index < this.letterList.length) {
this.scroller.scrollToIndex(index);
}
})
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
right: { anchor: '__container__', align: HorizontalAlign.End }
})
.margin({ right: 5 });
}
.width('100%')
.height('100%');
}
}
运行效果
八、内容总结
双层 LazyForEach = 外层分组 + 内层列表,提升组件复用性能。
@Reusable 只能给列表项使用,禁止嵌套使用。
嵌套复用会生成多个缓存池,导致内存增加、生命周期管理复杂、维护困难。
分组组件(A-Z)数量少,完全不需要复用。
唯一 uuid 作为列表 key,保证滑动不混乱、不闪烁。
@Observed + @ObjectLink 实现深层响应式,修改数据自动局部刷新。
九、仓库代码
工程名称:WechatContactDemo
仓库地址:https://gitee.com/HarmonyOS-UI-Basics/harmony-os-ui-basics.git
十、下节预告
下一节我们将正式学习 鸿蒙官方标准导航组件:Tabs 选项卡,一次性掌握应用最核心的底部导航、顶部导航、侧边导航全套方案:
学会 Tabs + TabContent 基础结构与用法;
实现 底部导航、顶部导航、侧边导航 三种主流布局;
掌握 固定/滚动导航栏、禁止滑动、自定义TabBar;
学会 TabsController 控制切换、页面缓存优化;
结合本节联系人列表,快速搭建仿微信主界面框架。
