当前位置: 首页 > news >正文

鸿蒙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)方法通知LazyForEachindex个分组的数据发生变更,组件会重新渲染该分组内的联系人列表,而其他分组保持不变,实现了细粒度的更新。

性能优化与最佳实践

虚拟滚动与懒加载

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在列表渲染领域的技术优势,以及其为开发者提供的强大工具和灵活架构。

http://www.lryc.cn/news/574452.html

相关文章:

  • 前端vue2每三十秒被动接受后端服务器发送过来得数据
  • 前端react使用 UmiJS 构建框架 在每次打包时候记录打包时间并在指定页面显示
  • Linux 启动过程流程图
  • PDF全能转换工具,支持图片转PDF,多图合并转PDF,word转PDF,PDF转WORD,PDF转图片
  • TouchDIVER Pro触觉手套:虚拟现实中的多模态交互新选择
  • Flask(五) 表单处理 request.form
  • 鸿蒙开发深入解析:Data Ability 数据共享机制全面指南
  • Java并发编程中高效缓存设计的哲学
  • 【格与代数系统】示例2
  • PyTorch 实现的 GlobalPMFSBlock_AP_Separate:嵌套注意力机制在多尺度特征聚合中的应用
  • 关于 pdd:anti_content参数分析与逆向
  • C#图书管理系统笔记(残缺版)
  • 【数据标注师】词性标注2
  • 【AI News | 20250623】每日AI进展
  • 基于 SpringBoot+JSP 的医疗预约与诊断系统设计与实现
  • 华为OD机试_2025 B卷_矩形相交的面积(Python,100分)(附详细解题思路)
  • leetcode82.删除排序链表中的重复元素II
  • EEG 分类攻略1- theta, alpha, beta和gamma频谱
  • C++语言发展历程-2025
  • python中学物理实验模拟:平抛运动和抛物运动
  • Python csv 模块
  • 数组题解——​轮转数组【LeetCode】
  • 华为云 Flexus+DeepSeek 征文|文案魔盒・Emoji 菌:基于华为云 CCE 集群 Dify 大模型,创意文案智能生成助手
  • 数组题解——​最大子数组和​【LeetCode】(更新版)
  • 黑马程序员苍穹外卖DAY1
  • 【软考高级系统架构论文】论数据分片技术及其应用
  • C指针总结复习(结合deepseek)
  • 深入浅出Node.js后端开发
  • 【TCL 脚本学习 4 -- tcl 脚本 数组定义和使用】
  • 触摸屏(典型 I2C + Input 子系统设备)从设备树解析到触摸事件上报