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