鸿蒙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 控制切换、页面缓存优化; 结合本节联系人列表,快速搭建仿微信主界面框架。