鸿蒙HarmonyOS 5 开发实践:LazyForEach在通讯录应用中的高效渲染(附:代码)

2025-06-29 08:54:32
110次阅读
0个评论

image.png

在移动应用开发中,列表渲染性能一直是影响用户体验的关键因素。鸿蒙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: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)
  }
}


通讯录数据

addressbook.json

结语

鸿蒙OS的LazyForEach组件为通讯录这类长列表应用提供了高效的渲染解决方案,通过按需渲染、精准更新和组件重用等机制,实现了性能与体验的双重提升。本文介绍的通讯录应用案例充分展示了LazyForEach与自定义数据源的协同工作模式,从数据模型设计到交互体验优化,形成了完整的鸿蒙开发实践体系。

对于开发者而言,掌握LazyForEach的核心特性和最佳实践,能够在处理大量数据时显著提升应用性能。随着鸿蒙生态的不断发展,LazyForEach还将与更多系统能力(如分布式数据、动效引擎)深度融合,为用户带来更加流畅、智能的应用体验。通过本案例,我们可以看到鸿蒙OS在列表渲染领域的技术优势,以及其为开发者提供的强大工具和灵活架构。

收藏00

登录 后评论。没有帐号? 注册 一个。