鸿蒙HarmonyOS 5 开发实践:LazyForEach在通讯录应用中的高效渲染(附:代码)
在移动应用开发中,列表渲染性能一直是影响用户体验的关键因素。鸿蒙OS提供的LazyForEach
组件通过按需渲染机制,为大量数据的高效展示提供了优质解决方案。本文将以一个通讯录应用为例,深入解析LazyForEach
在鸿蒙开发中的实际应用,包括数据源设计、组件渲染优化及交互体验提升等核心技术点。
鸿蒙列表渲染架构与LazyForEach特性
鸿蒙OS的列表渲染体系基于声明式UI框架构建,LazyForEach
作为其中的核心组件,与传统ForEach
相比具有显著优势:
按需渲染机制
LazyForEach
仅在组件可见时才进行渲染,避免了一次性创建大量UI元素的性能开销,这对于通讯录这类可能包含成百上千条数据的应用至关重要:
LazyForEach(this.sourceArray, (item: CategoryContact, indexGroup: number) => {ListItemGroup({ header: this.header(item.category) }) {ForEach(item.itemsContact, (contact: Contact, indexItem: number) => {ListItem() {contactSty({ name: contact.name })}})}
}, (item: CategoryContact) => JSON.stringify(item))
上述代码中,LazyForEach
用于渲染通讯录分组,而每个分组内的联系人列表使用ForEach
渲染。这种嵌套结构充分利用了LazyForEach
的懒加载特性,同时保证了分组内数据的快速访问。
数据变更响应机制
LazyForEach
与数据源的变更通知机制深度集成,当数据发生变化时,能够精准更新对应组件而不影响其他部分:
// 数据源通知数据变更
notifyDataChang(index:number){this.listeners.forEach(listener=>{listener.onDataChange(index)})
}
通过实现IDataSource
接口的通知方法(如notifyDataChang
),数据源可以在数据更新时通知LazyForEach
组件,使其仅重新渲染变化的部分,而非整个列表,极大提升了更新效率。
唯一键优化
LazyForEach
要求提供唯一键函数,用于跟踪数据项的身份,避免不必要的重渲染:
LazyForEach(this.sourceArray, (item, index) => { /* 渲染逻辑 */ },
(item: CategoryContact) => JSON.stringify(item))
唯一键函数(item) => JSON.stringify(item)
通过序列化数据项生成唯一标识,鸿蒙框架利用该标识判断数据项是否发生变化,从而决定是否需要重新渲染对应的组件。
通讯录应用的数据模型与架构设计
通讯录应用采用了分层数据模型和自定义数据源,为LazyForEach
的高效渲染奠定了基础。
分层数据模型设计
应用采用两级数据结构存储通讯录信息,第一层为分组(A-Z),第二层为具体联系人:
// 联系人基本信息
@Sendable
export class Contact {id: number;name: string;phone: string;// 其他属性...constructor(id: number = 0, name: string = '', phone: string = '') {this.id = id;this.name = name;this.phone = phone;}
}// 分组数据结构
export interface CategoryContact {category: string; // 分组标识(如'A'、'B')itemsContact: Array<Contact>; // 该分组下的联系人列表
}
这种分层结构与LazyForEach
的嵌套渲染模式完美匹配,通过ListItemGroup
实现分组头部与联系人列表的关联展示。
自定义数据源实现
应用通过ContactDataSource
封装数据操作逻辑,实现了IDataSource
接口以支持LazyForEach
的高效更新:
export class ContactDataSource extends BasicDataSource<CategoryContact> {private ContactList: Array<CategoryContact> = [];// 获取数据长度totalCount(): number {return this.ContactList.length;}// 获取指定位置数据getData(index: number): CategoryContact | void {return this.ContactList[index];}// 数据变更通知notifyDataChang(index: number) {this.listeners.forEach(listener => {listener.onDataChange(index);});}
}
BasicDataSource
基类实现了通用的数据监听和通知机制,ContactDataSource
则针对通讯录数据特性扩展了分组操作、联系人增删改查等功能,形成了完整的数据管理体系。
数据初始化与加载
应用从本地JSON文件加载通讯录数据,并按字母分组组织,为LazyForEach
提供结构化数据源:
initData() {// 从资源文件获取原始数据const value = getContext(this).resourceManager.getRawFileContentSync('addressbook.json');const textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true }).decodeToString(value);const jsonObj: Array<Contact> = JSON.parse(textDecoder) as Array<Contact>;// 按字母分组return jsonObj;
}
数据加载后通过pushDataItem
方法添加到数据源,触发LazyForEach
的增量渲染:
pushDataItem(data: Contact, categoryArray: Array<string>) {// 按字母分组逻辑const category = data.category;let index = categoryArray.indexOf(category);if (index !== -1) {// 分组存在时直接添加this.ContactList[index].itemsContact.push(data);this.notifyDataAdd(index);} else {// 分组不存在时创建新分组// ... 分组插入逻辑this.notifyDataAdd(index);}
}
LazyForEach在通讯录中的深度应用
分组头部粘性展示
通过sticky(StickyStyle.Header)
修饰符实现分组头部的粘性定位,当列表滚动时,分组头部会固定在顶部,提升浏览体验:
List() {LazyForEach(this.sourceArray, (item, index) => {ListItemGroup({ header: this.header(item.category) }) {// 联系人列表}})
}
.sticky(StickyStyle.Header)
header
构建器方法定义了分组头部的样式,包括字体大小、背景色和内边距:
@Builder
header(category: string) {Text(category).fontSize(24).fontWeight(500).backgroundColor('#ffd0cece').width('100%').padding({ left: 12 })
}
可重用组件优化
通过@Reusable
标记的可重用组件contactSty
实现联系人项的高效渲染,避免重复创建组件实例:
@Reusable
@Component
struct contactSty {@State name: string = '';// 组件重用时更新数据aboutToReuse(params: Record<string, Object>): void {this.name = params.name.toString();}build() {Text(this.name).fontSize(20).width('100%').padding({ left: 12 }).height(40);}
}
aboutToReuse
生命周期方法在组件重用时更新显示数据,避免了组件销毁和重建的开销,与LazyForEach
的懒加载机制形成互补,进一步提升性能。
数据变更与列表更新
当联系人数据发生变化(如删除、修改)时,数据源通过通知机制触发LazyForEach
的精准更新:
// 删除联系人数据
deleteDataItem(categoryArray: Array<string>, index: number, indexItem: number) {if (this.ContactList[index].itemsContact.length === 1) {// 分组仅剩一个联系人时删除整个分组this.deleteData(index);categoryArray.splice(indexItem, 1);} else {// 否则仅删除该联系人this.ContactList[index].itemsContact.splice(indexItem, 1);this.notifyDataChang(index);}
}
notifyDataChang(index)
方法通知LazyForEach
第index
个分组的数据发生变更,组件会重新渲染该分组内的联系人列表,而其他分组保持不变,实现了细粒度的更新。
性能优化与最佳实践
虚拟滚动与懒加载
LazyForEach
的核心优势在于虚拟滚动,它只会渲染可见区域的组件,对于长列表场景(如包含数百个联系人的分组)尤为重要。在通讯录应用中,即使数据量庞大,LazyForEach
也能保持流畅的滚动体验,因为它避免了创建所有列表项的开销。
事件监听优化
数据源通过维护监听器数组(listeners: DataChangeListener[]
)管理数据变更通知,避免了频繁的事件绑定与解绑操作:
registerDataChangeListener(listener: DataChangeListener): void {if (this.listeners.indexOf(listener) < 0) {this.listeners.push(listener);}
}unregisterDataChangeListener(listener: DataChangeListener): void {const index = this.listeners.indexOf(listener);if (index >= 0) {this.listeners.splice(index, 1);}
}
这种集中式的事件管理减少了内存占用,确保了数据变更通知的高效性,与LazyForEach
的渲染机制协同工作,形成了完整的性能优化链条。
内存管理与组件回收
LazyForEach
配合可重用组件(@Reusable
)实现了组件的回收利用,当组件滚动出可见区域时,不会被销毁而是进入回收池,等待重新使用时通过aboutToReuse
方法更新数据。这种机制大大减少了组件创建和销毁的开销,对于通讯录这类需要频繁滚动的应用至关重要。
附:代码
import { util } from "@kit.ArkTS"
import json from "@ohos.util.json"/*** 1、定义一个基础类,实现IDataSource接口*/
class BasicDataSource<T> implements IDataSource{/*** 需要对两个东西处理* 1、数据* 2、监听器* @returns*///定义一个监听器数组public listeners:DataChangeListener[] = []//获取数据的长度totalCount(): number {return 0}// 获取指定位置数据项getData(index: number): T | void {}registerDataChangeListener(listener: DataChangeListener): void {if (this.listeners.indexOf(listener) < 0) {this.listeners.push(listener)}}unregisterDataChangeListener(listener: DataChangeListener): void {const index = this.listeners.indexOf(listener)if (index >= 0) {this.listeners.splice(index,1)}}// 让所有的监听器重新加载子组件notifyDataReload(){this.listeners.forEach(listener=>{listener.onDataReloaded()})}// 通知LazyforEach组件在index对应的索引值添加数据notifyDataAdd(index:number){this.listeners.forEach(listener=>{listener.onDataAdd(index)})}// 通知LazyforEach组件在index位置删除数据notifyDataDelete(index:number){this.listeners.forEach(listener=>{listener.onDataDelete(index)})}// 通知LazyforEach组件在index位置更新数据notifyDataChang(index:number){this.listeners.forEach(listener=>{listener.onDataChange(index)})}
}
/*** 2、根据BasicDataSource,转成对现在要改变的数据的方法,extends*/
export class ContactDataSource extends BasicDataSource<CategoryContact>{// 定义数据源private ContactList:Array<CategoryContact> = []// 获取数据源的长度totalCount(): number {return this.ContactList.length}// 获取index位置的数据,数据项getData(index: number): void | CategoryContact {return this.ContactList[index]}// 获取index位置的数据项,获取indexItem位置的数据getDataItem(index:number,indexItem:number):Contact{return this.ContactList[index].itemsContact[indexItem]}// 删除数据项deleteData(index:number){this.ContactList.splice(index,1)this.notifyDataReload()}/*** 删除数据项里面的单个数据* @param categoryArray 数据项* @param index 数据项的索引值* @param indexItem 数据项中数据的索引*/deleteDataItem(categoryArray:Array<string>,index:number,indexItem:number){if (this.ContactList[index].itemsContact.length <= 0) {return}if (this.ContactList[index].itemsContact.length === 1) {this.deleteData(index)categoryArray.splice(indexItem,1)AppStorage.setOrCreate('categoryArray',categoryArray)}else {this.ContactList[index].itemsContact.splice(indexItem,1)this.notifyDataChang(index)}}/*** 添加方法* 1、将数据项添加到数据源* 2、将数据添加到数据项*/pushData(data:CategoryContact){this.ContactList.push(data)this.notifyDataAdd(this.ContactList.length - 1)}pushDataItem(data:Contact,categoryArray:Array<string>){// 获取到当前插入的数据需要插入到哪个数据项中,也就是A|B|C|D...里面的哪一个const category = data.category// 获取category在categoryArray里面的索引值let index:number = categoryArray.indexOf(category)// 判断分组是否存在if(index!== -1){// 分组存在this.ContactList[index].itemsContact.push(data)this.notifyDataAdd(index)}else{// 分组不存在// 在categoryArray中找到要添加的位置categoryArray.findIndex((current)=>{current >= data.category})if (index === -1) {index = this.ContactList.length}this.ContactList.splice(index,0,{category:data.category,itemsContact:[data]})categoryArray.splice(index,0,data.category)AppStorage.setOrCreate('categoryArray',categoryArray)this.notifyDataAdd(index)}}/*** 修改数据的放法*/updateDataItem(categoryArray:Array<string>,index:number,indexItem:number,data:Contact){//先删除数据this.deleteDataItem(categoryArray,index,indexItem)// 再添加数据this.pushDataItem(data,categoryArray)}/*** 删除所有*/clear(){this.ContactList.splice(0,this.ContactList.length)}
}/*** @sendable: 标记成Sendable对象,在不同并发中实现通过引用传递*/
@Sendable
export class Contact{id:numbername:stringphone:stringemail:stringaddress:stringavatar:stringcategory:stringconstructor(id: number=0, name: string='', phone: string='', email: string='', address: string='', avatar: string='',category: string='') {this.id = idthis.name = namethis.phone = phonethis.email = emailthis.address = addressthis.avatar = avatarthis.category = category}
}/*** 定义通讯录以组为单位字段信息*/
export interface CategoryContact{category:stringitemsContact:Array<Contact>
}@Entry
@Component
struct Index{@State sourceArray: ContactDataSource = new ContactDataSource()@StorageProp('categoryArray') categoryArray: Array<string> = [] // 分组// 进入页面aboutToAppear(): void {let array = this.initData()array.forEach((item,index)=>{this.sourceArray.pushDataItem(item,this.categoryArray)})}// 初始化数据initData() {// 从文件中获取数据const value = getContext(this).resourceManager.getRawFileContentSync('addressbook.json')// 解码成utf-8类型的数据const textDecoder = util.TextDecoder.create('utf-8',{ignoreBOM:true}).decodeToString(value)// 把它转换成需要的对象数据类型const jsonObj:Array<Contact> = JSON.parse(textDecoder) as Array<Contact>console.log(`jsonOBJ${JSON.stringify(jsonObj)}`)return jsonObj}build() {Column(){List() {// 懒加载数据源LazyForEach(this.sourceArray, (item: CategoryContact, indexGroup: number) => {ListItemGroup({ header: this.header(item.category) }) {ForEach(item.itemsContact, (contact: Contact, indexItem: number) => { // 遍历联系人ListItem() {contactSty({ name: contact.name })}})}.divider({// 设置分隔线样式strokeWidth: 2, // 线宽startMargin: 12, // 起始边距endMargin: 12// 结束边距})}, (item: CategoryContact) => JSON.stringify(item))}.sticky(StickyStyle.Header)}}//定义分组的头部样式@Builderheader(category: string) {Text(category).fontSize(24).fontWeight(500).backgroundColor('#ffd0cece').width('100%').padding({ left: 12 })}
}@Reusable// 标记为可重用组件
@Component// 标记为自定义组件
struct contactSty {@State name: string = '' // 姓名状态变量aboutToReuse(params: Record<string, Object>): void { // 组件即将重用时执行this.name = params.name.toString() // 更新姓名}build() {Text(this.name).fontSize(20).width('100%').padding({ left: 12 }).height(40)}
}
通讯录数据
📎addressbook.json
结语
鸿蒙OS的LazyForEach
组件为通讯录这类长列表应用提供了高效的渲染解决方案,通过按需渲染、精准更新和组件重用等机制,实现了性能与体验的双重提升。本文介绍的通讯录应用案例充分展示了LazyForEach
与自定义数据源的协同工作模式,从数据模型设计到交互体验优化,形成了完整的鸿蒙开发实践体系。
对于开发者而言,掌握LazyForEach
的核心特性和最佳实践,能够在处理大量数据时显著提升应用性能。随着鸿蒙生态的不断发展,LazyForEach
还将与更多系统能力(如分布式数据、动效引擎)深度融合,为用户带来更加流畅、智能的应用体验。通过本案例,我们可以看到鸿蒙OS在列表渲染领域的技术优势,以及其为开发者提供的强大工具和灵活架构。