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、关系型数据库等存储方案。
浙公网安备 33010602011771号