HarmonyOS 5开发从入门到精通(六):列表组件与数据渲染

HarmonyOS 5开发从入门到精通(六):列表组件与数据渲染

在移动应用开发中,列表是最常见且重要的界面元素之一。HarmonyOS 5提供了强大的List组件和ForEach循环渲染机制,能够高效地展示大量数据。本篇将深入讲解列表组件的使用、数据渲染、性能优化等核心内容。

一、List组件基础

1.1 List组件简介

List组件是HarmonyOS中用于展示结构化、可滚动数据的重要容器组件,广泛应用于商品列表、联系人、消息记录等场景。List组件具有以下特点:

  • 自动提供滚动功能,适合呈现大量数据
  • 支持垂直和水平两种滚动方向
  • 支持条件渲染、循环渲染和懒加载等优化手段
  • 每个列表项(ListItem)只能包含一个根组件

1.2 基础List创建

import { List, ListItem, Text } from '@ohos/arkui';

@Entry
@Component
struct BasicListExample {
  private listData: string[] = ['列表项1', '列表项2', '列表项3', '列表项4', '列表项5'];

  build() {
    Column() {
      List({ space: 20 }) {
        ForEach(this.listData, (item: string) => {
          ListItem() {
            Text(item)
              .width('100%')
              .height(60)
              .textAlign(TextAlign.Center)
              .fontSize(16)
          }
          .backgroundColor('#f0f0f0')
          .margin({ left: 16, right: 16 })
        }, item => item)
      }
      .width('100%')
      .height('100%')
    }
    .padding(16)
  }
}

1.3 List组件常用属性

List组件提供了丰富的属性来控制列表的显示效果:

List({ space: 10, initialIndex: 0 }) {
  // 列表项
}
.divider({ strokeWidth: 1, color: Color.Gray, startMargin: 16, endMargin: 16 }) // 分割线
.listDirection(Axis.Horizontal) // 水平排列
.scrollBar(BarState.Auto) // 滚动条
.onScrollIndex((start: number, end: number) => {
  console.info(`当前显示范围: ${start} - ${end}`);
})
.onReachEnd(() => {
  console.info('已滚动到底部');
})

二、ForEach循环渲染

2.1 ForEach基础用法

ForEach是ArkTS框架中用于循环渲染的核心组件,它能够根据数据源动态生成UI组件:

@Entry
@Component
struct StudentList {
  @State students: string[] = ['张三', '李四', '王五', '赵六'];

  build() {
    Column() {
      Text('学生名单')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .margin(10)
      
      ForEach(this.students, (item: string, index: number) => {
        Text(`${index + 1}. ${item}`)
          .fontSize(18)
          .padding(5)
      })
    }
    .padding(20)
  }
}

2.2 带交互功能的ForEach

结合@State状态管理,可以实现动态可更新的列表:

@Entry
@Component
struct DynamicList {
  @State names: string[] = ['张三', '李四', '王五'];

  build() {
    Column({ space: 12 }) {
      Text('学生列表')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
      
      ForEach(this.names, (item: string, index: number) => {
        Row({ space: 10 }) {
          Text(`${index + 1}. ${item}`)
            .fontSize(18)
          Button('删除')
            .fontSize(14)
            .onClick(() => {
              this.names.splice(index, 1);
            })
        }
        .padding(5)
      })
      
      Button('添加学生')
        .onClick(() => {
          this.names.push('新学生');
        })
        .backgroundColor('#0A59F7')
        .fontColor(Color.White)
        .padding(10)
    }
    .padding(20)
  }
}

2.3 键值优化

对于复杂数据结构,推荐显式设置唯一键值以提高渲染效率:

interface Student {
  id: number;
  name: string;
  age: number;
}

@Entry
@Component
struct StudentListWithKey {
  @State students: Student[] = [
    { id: 1, name: '张三', age: 20 },
    { id: 2, name: '李四', age: 21 },
    { id: 3, name: '王五', age: 22 }
  ];

  build() {
    Column() {
      ForEach(this.students, (item: Student) => {
        Text(`${item.id} - ${item.name} (${item.age}岁)`)
          .fontSize(16)
          .padding(5)
      }, (item: Student) => item.id.toString()) // 使用id作为唯一键值
    }
    .padding(20)
  }
}

三、下拉刷新与上拉加载

3.1 Refresh组件实现下拉刷新

HarmonyOS提供了Refresh组件来实现下拉刷新功能:

import { Refresh, RefreshStatus } from '@ohos/arkui';

@Entry
@Component
struct RefreshList {
  @State data: string[] = ['数据1', '数据2', '数据3', '数据4', '数据5'];
  @State refreshing: boolean = false;

  build() {
    Column() {
      Refresh({
        refreshing: this.refreshing,
        onRefresh: () => {
          this.refreshing = true;
          // 模拟网络请求
          setTimeout(() => {
            this.data = ['新数据1', '新数据2', '新数据3', '新数据4', '新数据5'];
            this.refreshing = false;
          }, 2000);
        }
      }) {
        List() {
          ForEach(this.data, (item: string) => {
            ListItem() {
              Text(item)
                .width('100%')
                .height(60)
                .textAlign(TextAlign.Center)
            }
          }, item => item)
        }
        .width('100%')
        .height('100%')
      }
    }
    .width('100%')
    .height('100%')
  }
}

3.2 上拉加载更多

通过监听滚动事件实现上拉加载:

@Entry
@Component
struct LoadMoreList {
  @State data: string[] = [];
  @State loading: boolean = false;
  @State hasMore: boolean = true;
  private page: number = 1;

  aboutToAppear() {
    this.loadData();
  }

  loadData() {
    if (this.loading || !this.hasMore) {
      return;
    }
    
    this.loading = true;
    // 模拟网络请求
    setTimeout(() => {
      const newData = Array.from({ length: 10 }, (_, i) => `数据${this.page * 10 + i + 1}`);
      this.data = [...this.data, ...newData];
      this.page++;
      this.hasMore = this.page <= 5; // 模拟最多5页
      this.loading = false;
    }, 1000);
  }

  build() {
    Column() {
      List() {
        ForEach(this.data, (item: string) => {
          ListItem() {
            Text(item)
              .width('100%')
              .height(60)
              .textAlign(TextAlign.Center)
          }
        }, item => item)
        
        // 加载更多指示器
        if (this.loading) {
          ListItem() {
            Row() {
              LoadingProgress()
                .width(20)
                .height(20)
              Text('加载中...')
                .fontSize(14)
                .margin({ left: 10 })
            }
            .justifyContent(FlexAlign.Center)
            .width('100%')
            .height(60)
          }
        }
      }
      .width('100%')
      .height('100%')
      .onReachEnd(() => {
        this.loadData();
      })
    }
    .width('100%')
    .height('100%')
  }
}

四、列表性能优化

4.1 LazyForEach懒加载

对于大数据量的列表,使用LazyForEach可以显著提升性能:

import { LazyForEach } from '@ohos/arkui';

// 数据源类
class ListDataSource implements IDataSource<string> {
  private data: string[] = [];
  private listeners: DataChangeListener[] = [];

  constructor(initialData: string[]) {
    this.data = initialData;
  }

  totalCount(): number {
    return this.data.length;
  }

  getData(index: number): string {
    return this.data[index];
  }

  onRegisterDataChangeListener(listener: DataChangeListener): void {
    this.listeners.push(listener);
  }

  onUnregisterDataChangeListener(listener: DataChangeListener): void {
    const index = this.listeners.indexOf(listener);
    if (index !== -1) {
      this.listeners.splice(index, 1);
    }
  }

  addData(item: string): void {
    this.data.push(item);
    this.listeners.forEach(listener => {
      listener.onDataAdd(this.data.length - 1);
    });
  }
}

@Entry
@Component
struct LazyList {
  private dataSource = new ListDataSource(Array.from({ length: 1000 }, (_, i) => `数据${i + 1}`));

  build() {
    Column() {
      List() {
        LazyForEach(this.dataSource, (item: string, index: number) => {
          ListItem() {
            Text(`${index + 1}. ${item}`)
              .width('100%')
              .height(60)
              .textAlign(TextAlign.Center)
          }
        }, (item: string, index: number) => index.toString())
      }
      .width('100%')
      .height('100%')
      .cachedCount(3) // 缓存屏幕外3条数据
    }
  }
}

4.2 组件复用优化

使用@Reusable装饰器实现组件复用:

@Reusable
@Component
struct ReusableListItem {
  @Prop item: string;
  @Prop index: number;

  aboutToReuse(params: Record<string, any>): void {
    this.item = params.item as string;
    this.index = params.index as number;
  }

  build() {
    Text(`${this.index + 1}. ${this.item}`)
      .width('100%')
      .height(60)
      .textAlign(TextAlign.Center)
      .backgroundColor(Color.White)
      .borderRadius(8)
      .margin({ bottom: 10 })
  }
}

@Entry
@Component
struct ReusableList {
  @State data: string[] = Array.from({ length: 100 }, (_, i) => `数据${i + 1}`);

  build() {
    Column() {
      List() {
        ForEach(this.data, (item: string, index: number) => {
          ListItem() {
            ReusableListItem({ item: item, index: index })
          }
        }, item => item)
      }
      .width('100%')
      .height('100%')
    }
  }
}

4.3 布局优化

减少嵌套层级,使用RelativeContainer替代多层嵌套:

// 优化前(5层嵌套)
Column() {
  Row() {
    Column() {
      Text('标题')
        .fontSize(18)
      Row() {
        Image($r('app.media.icon'))
          .width(20)
          .height(20)
        Text('副标题')
          .fontSize(14)
      }
    }
  }
}

// 优化后(2层嵌套)
RelativeContainer() {
  Text('标题')
    .fontSize(18)
    .alignRules({
      top: { anchor: "__container__", align: VerticalAlign.Top },
      left: { anchor: "__container__", align: HorizontalAlign.Start }
    })
  
  Image($r('app.media.icon'))
    .width(20)
    .height(20)
    .alignRules({
      top: { anchor: "__container__", align: VerticalAlign.Top },
      right: { anchor: "__container__", align: HorizontalAlign.End }
    })
  
  Text('副标题')
    .fontSize(14)
    .alignRules({
      top: { anchor: "__container__", align: VerticalAlign.Top },
      centerX: { anchor: "__container__", align: HorizontalAlign.Center }
    })
}

五、实战案例:商品列表

5.1 商品数据模型

// models/Product.ts
export class Product {
  id: number;
  name: string;
  price: number;
  image: Resource;
  description?: string;

  constructor(id: number, name: string, price: number, image: Resource, description?: string) {
    this.id = id;
    this.name = name;
    this.price = price;
    this.image = image;
    this.description = description;
  }
}

// 商品数据源
export class ProductDataSource implements IDataSource<Product> {
  private products: Product[] = [];
  private listeners: DataChangeListener[] = [];

  constructor() {
    this.loadInitialData();
  }

  private loadInitialData(): void {
    this.products = [
      new Product(1, 'iPhone 15', 5999, $r('app.media.iphone'), '最新款iPhone'),
      new Product(2, '华为Mate 60', 5499, $r('app.media.mate60'), '旗舰手机'),
      new Product(3, '小米14', 3999, $r('app.media.mi14'), '性价比之王'),
      new Product(4, '三星S23', 4999, $r('app.media.s23'), '安卓机皇'),
      new Product(5, 'OPPO Find X6', 4499, $r('app.media.oppo'), '影像旗舰'),
      new Product(6, 'vivo X90', 4299, $r('app.media.vivo'), '专业影像'),
      new Product(7, '荣耀Magic5', 3899, $r('app.media.honor'), '性能旗舰'),
      new Product(8, '一加11', 3999, $r('app.media.oneplus'), '性能怪兽'),
      new Product(9, 'realme GT Neo5', 2999, $r('app.media.realme'), '游戏手机'),
      new Product(10, '红米K60', 2499, $r('app.media.redmi'), '性价比旗舰')
    ];
  }

  totalCount(): number {
    return this.products.length;
  }

  getData(index: number): Product {
    return this.products[index];
  }

  onRegisterDataChangeListener(listener: DataChangeListener): void {
    this.listeners.push(listener);
  }

  onUnregisterDataChangeListener(listener: DataChangeListener): void {
    const index = this.listeners.indexOf(listener);
    if (index !== -1) {
      this.listeners.splice(index, 1);
    }
  }
}

5.2 商品列表组件

// pages/ProductList.ets
import { ProductDataSource } from '../models/Product';

@Entry
@Component
struct ProductList {
  private dataSource = new ProductDataSource();
  @State refreshing: boolean = false;
  @State loading: boolean = false;
  @State hasMore: boolean = true;
  private page: number = 1;

  @Builder
  ProductItem(product: Product) {
    Row() {
      Image(product.image)
        .width(80)
        .height(80)
        .objectFit(ImageFit.Cover)
        .borderRadius(8)
        .margin({ right: 12 })
      
      Column({ space: 4 }) {
        Text(product.name)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor(Color.Black)
        
        Text(product.description || '')
          .fontSize(14)
          .fontColor('#666')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        
        Text(`¥${product.price}`)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FF3B30')
      }
      .layoutWeight(1)
      
      Button('购买')
        .width(60)
        .height(32)
        .fontSize(14)
        .backgroundColor('#007AFF')
        .fontColor(Color.White)
        .borderRadius(16)
        .onClick(() => {
          prompt.showToast({ message: `购买 ${product.name}` });
        })
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(12)
    .margin({ bottom: 10 })
  }

  // 下拉刷新
  private handleRefresh(): void {
    this.refreshing = true;
    setTimeout(() => {
      this.refreshing = false;
      prompt.showToast({ message: '刷新成功' });
    }, 1500);
  }

  // 加载更多
  private loadMore(): void {
    if (this.loading || !this.hasMore) {
      return;
    }
    
    this.loading = true;
    setTimeout(() => {
      this.loading = false;
      this.page++;
      this.hasMore = this.page <= 3; // 模拟最多3页
      if (!this.hasMore) {
        prompt.showToast({ message: '已加载全部数据' });
      }
    }, 1000);
  }

  build() {
    Column() {
      // 搜索栏
      Row() {
        TextInput({ placeholder: '搜索商品' })
          .layoutWeight(1)
          .height(40)
          .backgroundColor('#F5F5F5')
          .borderRadius(20)
          .padding({ left: 16, right: 16 })
        
        Button('搜索')
          .width(60)
          .height(40)
          .margin({ left: 10 })
          .backgroundColor('#007AFF')
          .fontColor(Color.White)
          .borderRadius(20)
      }
      .padding(16)
      .backgroundColor(Color.White)
      
      // 商品列表
      Refresh({
        refreshing: this.refreshing,
        onRefresh: () => this.handleRefresh()
      }) {
        List() {
          LazyForEach(this.dataSource, (item: Product, index: number) => {
            ListItem() {
              this.ProductItem(item)
            }
          }, (item: Product, index: number) => item.id.toString())
          
          // 加载更多指示器
          if (this.loading) {
            ListItem() {
              Row() {
                LoadingProgress()
                  .width(20)
                  .height(20)
                Text('加载中...')
                  .fontSize(14)
                  .margin({ left: 10 })
              }
              .justifyContent(FlexAlign.Center)
              .width('100%')
              .height(60)
            }
          }
        }
        .width('100%')
        .height('100%')
        .divider({ strokeWidth: 0 }) // 隐藏分割线
        .onReachEnd(() => {
          this.loadMore();
        })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

六、总结

通过本篇学习,您已经掌握了:

List组件的基础用法和常用属性配置

ForEach循环渲染的数据绑定机制

下拉刷新和上拉加载的实现方法

LazyForEach懒加载的性能优化技巧

组件复用和布局优化的最佳实践

完整的商品列表实战案例

关键知识点回顾

  • List组件是展示结构化数据的核心容器
  • ForEach通过数据驱动UI,实现动态渲染
  • Refresh组件提供下拉刷新功能
  • LazyForEach按需加载数据,优化内存占用
  • @Reusable装饰器实现组件复用,提升性能
  • 合理使用cachedCount缓存屏幕外数据

下一篇我们将学习本地数据存储,掌握如何在设备本地持久化存储应用数据,包括Preferences、关系型数据库等存储方案。

posted @ 2025-12-23 18:09  奇崽  阅读(0)  评论(0)    收藏  举报