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緩存屏幕外數據