前置知识: HarmonyOS

列表与网格

00:00
4 min Intermediate 2026/6/14

List与Grid组件

概述

列表和网格是移动应用中最常见的布局方式。HarmonyOS 提供了 List 和 Grid 两个核心组件,分别用于线性列表和二维网格的展示。它们内置了滚动、复用和懒加载能力,能够高效地处理大量数据的渲染。配合 ForEach 或 LazyForEach,可以灵活地实现动态数据驱动的列表与网格界面。

基础概念

List 组件:垂直或水平方向的线性列表容器,每个子项通过 ListItem 包裹。支持分组(ListItemGroup)、滑动删除、下拉刷新、滚动定位等特性。

Grid 组件:二维网布局容器,每个子项通过 GridItem 包裹。通过 columnsTemplate 和 rowsTemplate 定义行列结构,支持不规则网格和可滚动网格。

LazyForEach:与 ForEach 不同,LazyForEach 采用懒加载机制,仅渲染可视区域内的组件,适合大数据量场景。需要配合 IDataSource 数据使用。

cachedCount列表渲染缓存数量,在可视区域外额外渲染若干项以提升滑动

快速上手

基本列表

@Component
struct BasicList {
  // 定义数据源
  @State items: string[] = ['项目一', '项目二', '项目三', '项目四', '项目五']

  build() {
    Column() {
      List() {
        // 使用 ForEach 遍历数据
        ForEach(this.items, (item: string, index?: number) => {
          ListItem() {
            Row() {
              Text(`第${index}项`)
                .fontSize(16)
              Text(item)
                .fontSize(14)
                .fontColor('#666666')
            }
            .width('100%')
            .padding(12)
          }
        }, (item: string, index?: number) => `${index}`)
      }
      .width('100%')
      .height('100%')
    }
  }
}

基本网格

@Component
struct BasicGrid {
  @State items: number[] = [1, 2, 3, 4, 5, 6]

  build() {
    Grid() {
      ForEach(this.items, (item: number) => {
        GridItem() {
          Text(`项目${item}`)
            .fontSize(18)
            .textAlign(TextAlign.Center)
        }
        .backgroundColor('#f0f0f0')
        .borderRadius(8)
        .padding(20)
      }, (item: number) => item.toString())
    }
    // 定义三列等宽布局
    .columnsTemplate('1fr 1fr 1fr')
    .rowsGap(10)
    .columnsGap(10)
    .padding(10)
  }
}

详细用法

List 分组与粘性标题

@Component
struct GroupedList {
  // 按分组组织数据
  @State groups: object[] = [
    { title: '水果', items: ['苹果', '香蕉', '橙子'] },
    { title: '蔬菜', items: ['番茄', '黄瓜', '胡萝卜'] },
    { title: '饮料', items: ['咖啡', '茶', '果汁'] },
  ]

  build() {
    List({ space: 8 }) {
      ForEach(this.groups, (group: Record<string, Object>) => {
        ListItemGroup({ header: this.groupHeader(group.title) }) {
          ForEach(group.items, (item: string) => {
            ListItem() {
              Text(item)
                .fontSize(15)
                .padding({ left: 16, top: 10, bottom: 10 })
            }
          }, (item: string) => item)
        }
        // 粘性标题:滑动时标题固定在顶部
        .sticky(StickyStyle.Header)
      }, (group: Record<string, Object>) => group.title)
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  groupHeader(title: string) {
    Row() {
      Text(title)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#e8e8e8')
  }
}

LazyForEach 懒加载列表

// 实现 IDataSource 接口的数据源
class ListDataSource implements IDataSource {
  private dataArray: string[] = []
  private listeners: DataChangeListener[] = []

  // 构造函数中初始化数据
  constructor(count: number) {
    for (let i = 0; i < count; i++) {
      this.dataArray.push(`数据项 ${i}`)
    }
  }

  // 返回数据总数
  totalCount(): number {
    return this.dataArray.length
  }

  // 返回指定位置的数据
  getData(index: number): string {
    return this.dataArray[index]
  }

  // 注册数据变更监听器
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener)
    }
  }

  // 注销数据变更监听器
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener)
    if (pos >= 0) {
      this.listeners.splice(pos, 1)
    }
  }

  // 追加数据并通知监听器
  public appendData(item: string): void {
    this.dataArray.push(item)
    this.listeners.forEach(listener => listener.onDataAdd(this.dataArray.length - 1))
  }
}

@Component
struct LazyListPage {
  // 使用 LazyForEach 数据源
  private dataSource: ListDataSource = new ListDataSource(100)

  build() {
    List() {
      // 懒加载:仅渲染可视区域内的项
      LazyForEach(this.dataSource, (item: string) => {
        ListItem() {
          Text(item)
            .width('100%')
            .height(60)
            .fontSize(15)
            .padding({ left: 16 })
          Divider().margin({ left: 16 })
        }
      }, (item: string) => item)
    }
    .cachedCount(5) // 预渲染5个缓存项
    .width('100%')
    .height('100%')
  }
}

List 滑动删除与下拉刷新

@Component
struct SwipeListPage {
  @State items: string[] = ['待办事项一', '待办事项二', '待办事项三']

  build() {
    List({ space: 10 }) {
      ForEach(this.items, (item: string, index?: number) => {
        ListItem() {
          Row() {
            Text(item).fontSize(15)
          }
          .width('100%')
          .padding(16)
          .backgroundColor(Color.White)
          .borderRadius(8)
        }
        // 左滑显示删除按钮
        .swipeAction({ end: this.deleteButton(index) })
      }, (item: string, index?: number) => `${index}`)
    }
    .padding(16)
    .onRefresh(() => {
      // 下拉刷新回调
      console.info('正在刷新数据...')
    })
  }

  @Builder
  deleteButton(index?: number) {
    Button('删除')
      .backgroundColor(Color.Red)
      .fontColor(Color.White)
      .borderRadius(8)
      .onClick(() => {
        if (index !== undefined) {
          this.items.splice(index, 1)
        }
      })
  }
}

Grid 不规则布局

@Component
struct IrregularGrid {
  build() {
    Grid() {
      // 大图项:占据两列
      GridItem() {
        Text('推荐')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
      }
      .columnStart(0).columnEnd(1) // 跨两列
      .backgroundColor('#ff6b6b')
      .borderRadius(8)
      .padding(30)

      GridItem() {
        Text('热门')
          .fontSize(16)
      }
      .backgroundColor('#ffd93d')
      .borderRadius(8)
      .padding(20)

      GridItem() {
        Text('最新')
          .fontSize(16)
      }
      .backgroundColor('#6bcb77')
      .borderRadius(8)
      .padding(20)

      GridItem() {
        Text('精选')
          .fontSize(16)
      }
      .backgroundColor('#4d96ff')
      .borderRadius(8)
      .padding(20)
    }
    .columnsTemplate('1fr 1fr')
    .rowsGap(10)
    .columnsGap(10)
    .padding(10)
  }
}

常见场景

聊天消息列表

interface ChatMessage {
  id: string
  content: string
  isMine: boolean  // 是否为自己发送的消息
  time: string
}

@Component
struct ChatListPage {
  @State messages: ChatMessage[] = [
    { id: '1', content: '你好!', isMine: false, time: '10:00' },
    { id: '2', content: '你好,最近怎么样?', isMine: true, time: '10:01' },
    { id: '3', content: '挺好的,在学习 HarmonyOS', isMine: false, time: '10:02' },
  ]

  build() {
    Column() {
      // 消息列表
      List() {
        ForEach(this.messages, (msg: ChatMessage) => {
          ListItem() {
            Row() {
              if (msg.isMine) {
                Blank() // 自己的消息靠右
              }
              Column() {
                Text(msg.content)
                  .fontSize(15)
                  .padding(10)
                  .borderRadius(12)
                  .backgroundColor(msg.isMine ? '#95ec69' : '#ffffff')
                Text(msg.time)
                  .fontSize(10)
                  .fontColor('#999999')
                  .margin({ top: 4 })
              }
              .alignItems(msg.isMine ? HorizontalAlign.End : HorizontalAlign.Start)

              if (!msg.isMine) {
                Blank() // 对方的消息靠左
              }
            }
            .width('100%')
          }
        }, (msg: ChatMessage) => msg.id)
      }
      .layoutWeight(1)
    }
  }
}

商品网格展示

interface Product {
  id: string
  name: string
  price: number
  image: Resource
}

@Component
struct ProductGridPage {
  @State products: Product[] = [
    { id: '1', name: '商品A', price: 99.9, image: $r('app.media.product1') },
    { id: '2', name: '商品B', price: 199.0, image: $r('app.media.product2') },
    { id: '3', name: '商品C', price: 49.9, image: $r('app.media.product3') },
    { id: '4', name: '商品D', price: 299.0, image: $r('app.media.product4') },
  ]

  build() {
    Grid() {
      ForEach(this.products, (product: Product) => {
        GridItem() {
          Column() {
            Image(product.image)
              .width('100%')
              .height(120)
              .objectFit(ImageFit.Cover)
              .borderRadius({ topLeft: 8, topRight: 8 })
            Text(product.name)
              .fontSize(14)
              .margin({ top: 8 })
            Text(`¥${product.price.toFixed(2)}`)
              .fontSize(16)
              .fontColor('#ff4d4f')
              .fontWeight(FontWeight.Bold)
              .margin({ top: 4 })
          }
          .padding(8)
          .backgroundColor(Color.White)
          .borderRadius(8)
        }
      }, (product: Product) => product.id)
    }
    .columnsTemplate('1fr 1fr')
    .rowsGap(12)
    .columnsGap(12)
    .padding(12)
  }
}

注意事项

  • ForEach 与 LazyForEach 的选择:数据量小于 100 条时使用 ForEach 即可,数据量较大时必须使用 LazyForEach 以保证性能。ForEach 会一次性创建所有组件,LazyForEach 仅创建可视区域内的组件
  • 生成:ForEach 和 LazyForEach 的键函数必须返回唯一且稳定,避免使用 index 作为键,否则在数据增删时可能导致渲染异常。
  • cachedCount 设置缓存数量不宜过大,一般设置为 3-5 即可。过大的缓存增加内存占用,过小则影响滑动
  • ListItem List 组件中所有 ListItem 的度应尽量一致,度不一致时可能导致滚动位置计算不准确。
  • Grid模板:columnsTemplate 和 rowsTemplate 使用 fr 单位定义比例支持混合使用固定值比例值,如 '100px 1fr 2fr'

进阶用法

列表滚动定位

@Component
struct ScrollToListPage {
  private listScroller: Scroller = new Scroller()
  @State currentIndex: number = 0

  build() {
    Column() {
      // 定位按钮
      Row() {
        Button('跳到顶部').onClick(() => this.listScroller.scrollToIndex(0))
        Button('跳到第50项').onClick(() => this.listScroller.scrollToIndex(50))
        Button('跳到底部').onClick(() => this.listScroller.scrollEdge(Edge.Bottom))
      }

      List({ space: 5, scroller: this.listScroller }) {
        ForEach(Array.from({ length: 100 }, (_, i) => i), (item: number) => {
          ListItem() {
            Text(`第 ${item} 项`)
              .width('100%')
              .height(50)
              .fontSize(15)
              .padding({ left: 16 })
          }
        }, (item: number) => item.toString())
      }
      .layoutWeight(1)
    }
  }
}

WaterFlow 瀑布流布局

@Component
struct WaterFlowPage {
  @State items: number[] = Array.from({ length: 20 }, (_, i) => i + 1)

  build() {
    // WaterFlow 是更高级的网格组件,支持不等高子项
    WaterFlow() {
      ForEach(this.items, (item: number) => {
        FlowItem() {
          Column() {
            Text(`内容 ${item}`)
              .fontSize(14)
          }
          .width('100%')
          // 不同高度模拟瀑布流效果
          .height(item % 3 === 0 ? 150 : item % 3 === 1 ? 100 : 200)
          .backgroundColor(item % 2 === 0 ? '#e3f2fd' : '#fff3e0')
          .borderRadius(8)
          .padding(12)
        }
      }, (item: number) => item.toString())
    }
    .columnsTemplate('1fr 1fr')
    .columnsGap(10)
    .rowsGap(10)
    .padding(10)
  }
}

List 嵌套与吸顶效果

@Component
struct StickyHeaderList {
  @State sections: object[] = [
    { title: '推荐', items: ['推荐内容一', '推荐内容二', '推荐内容三'] },
    { title: '热门', items: ['热门内容一', '热门内容二'] },
    { title: '最新', items: ['最新内容一', '最新内容二', '最新内容三', '最新内容四'] },
  ]

  build() {
    List({ space: 0 }) {
      ForEach(this.sections, (section: Record<string, Object>) => {
        ListItemGroup({
          header: this.sectionHeader(section.title),
          space: 8
        }) {
          ForEach(section.items, (item: string) => {
            ListItem() {
              Text(item)
                .fontSize(14)
                .padding(12)
                .backgroundColor(Color.White)
                .borderRadius(6)
                .width('100%')
            }
          }, (item: string) => item)
        }
      }, (section: Record<string, Object>) => section.title)
    }
    .sticky(StickyStyle.Header) // 吸顶效果
    .divider({ strokeWidth: 1, color: '#eeeeee' })
    .width('100%')
    .height('100%')
  }

  @Builder
  sectionHeader(title: string | undefined) {
    Row() {
      Text(title ?? '')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
    .padding({ left: 16, top: 12, bottom: 12 })
    .backgroundColor('#f5f5f5')
  }
}

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式