鸿蒙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:number
name:string
phone:string
email:string
address:string
avatar:string
category:string
constructor(id: number=0, name: string='', phone: string='', email: string='', address: string='', avatar: string='',
category: string='') {
this.id = id
this.name = name
this.phone = phone
this.email = email
this.address = address
this.avatar = avatar
this.category = category
}
}
/**
* 定义通讯录以组为单位字段信息
*/
export interface CategoryContact{
category:string
itemsContact: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)
}
}
//定义分组的头部样式
@Builder
header(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)
}
}
通讯录数据
结语
鸿蒙OS的LazyForEach
组件为通讯录这类长列表应用提供了高效的渲染解决方案,通过按需渲染、精准更新和组件重用等机制,实现了性能与体验的双重提升。本文介绍的通讯录应用案例充分展示了LazyForEach
与自定义数据源的协同工作模式,从数据模型设计到交互体验优化,形成了完整的鸿蒙开发实践体系。
对于开发者而言,掌握LazyForEach
的核心特性和最佳实践,能够在处理大量数据时显著提升应用性能。随着鸿蒙生态的不断发展,LazyForEach
还将与更多系统能力(如分布式数据、动效引擎)深度融合,为用户带来更加流畅、智能的应用体验。通过本案例,我们可以看到鸿蒙OS在列表渲染领域的技术优势,以及其为开发者提供的强大工具和灵活架构。
- 0回答
- 0粉丝
- 0关注
- 鸿蒙HarmonyOS 5开发:AlphabetIndexer组件在通讯录中的高效索引实现(附:代码)
- 【HarmonyOS】获取通讯录信息
- 鸿蒙Next实现通讯录索引条AlphabetIndexer
- 鸿蒙ArkTS+ArkUI仿微信通讯录页面制作【1】
- 鸿蒙ArkTS+ArkUI仿微信通讯录页面制作【2】
- 鸿蒙HarmonyOS 5小游戏实践:记忆翻牌(附:源代码)
- 如何添加联系人到手机通讯录
- 鸿蒙HarmonyOS 5小游戏实践:数字记忆挑战(附:源代码)
- 鸿蒙HarmonyOS 5小游戏实践:打砖块游戏(附:源代码)
- HarmonyNext技术解析:ArkTS在鸿蒙系统中的高效性能优化实践
- 鸿蒙HarmonyOS 5 小游戏实践:数字华容道(附:源代码)
- 鸿蒙HarmonyOS 5小游戏实践:动物连连看(附:源代码)
- 第四九课:HarmonyOS Next在医疗应用开发中的应用与实践
- 鸿蒙HarmonyOS ArkTS LazyForEach懒加载渲染控制详解
- (八九)HarmonyOS Design 在企业级应用中的实践