鸿蒙原生 ArkTS 布局之 List 基础案例:联系人列表实战(API 24)


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、背景与目标

1.1 什么是 ArkTS

ArkTS 是鸿蒙原生应用开发的声明式 UI 编程语言,基于 TypeScript 语法扩展而来。它采用 声明式 + 状态驱动 的编程范式——开发者描述 UI 应该"长什么样",框架自动计算 UI 的更新路径,无需手动操作 DOM。

ArkTS 的核心特点:

  • @Entry / @Component 装饰器:标记页面入口和自定义组件
  • @State / @Prop / @Link 装饰器:实现状态驱动的 UI 自动刷新
  • 内置组件库ListGridColumnRowStackTextImage
  • 链式 API:通过 .属性() 方法链式配置组件样式

1.2 本文目标

本文将基于鸿蒙 NEXT(API 24)实现一个经典的联系人列表页面,具有以下特征:

功能点 技术方案
页面标题栏 Text 组件 + 背景色
联系人列表 List + ForEach 循环渲染
圆形头像 Stack + Circle + Text(纯代码,无图片依赖)
昵称 + 描述 Column 纵向排列
列表项分割线 List.divider() 统一配置
滚动回弹 EdgeEffect.Spring
右侧指示箭头 Text('>') 模拟

二、项目创建与配置

2.1 创建 HarmonyOS 项目

打开 DevEco Studio,选择 File → New → Create Project。在模板选择界面选择 Empty Ability(Stage Model),语言选择 ArkTS

提示:本文基于 API 24(HarmonyOS NEXT 6.2+)编写。实际开发时请确保 build-profile.json5compatibleSdkVersion 配置为 "6.2.0(24)"

项目创建完成后,核心文件结构如下:

entry/src/main/ets/
├── entryability/
│   └── EntryAbility.ets        # Ability 生命周期(无需修改)
├── pages/
│   └── Index.ets               # 主页面(本文核心)
└── resources/
    └── base/
        ├── element/
        ├── media/              # 图片资源目录
        └── ...

我们只需要修改 pages/Index.ets 一个文件即可完成整个页面。

2.2 项目依赖说明

本示例不引入任何第三方依赖,全部使用 ArkTS 内置组件和 API 24 的系统能力。头像的实现采用了纯代码绘制的圆形色块 + 姓氏首字,无需在 resources/media 中放置任何图片文件,真正做到"开箱即用"。


三、完整代码实现

下面给出 Index.ets 的完整代码。每一段都配有详细的中文注释,建议读者逐段阅读,理解每一行代码的作用。

/**
 * 联系人列表 — List 基础案例
 * ============================
 * 场景:带头像(圆形)、昵称和描述信息的经典联系人列表。
 * 核心技术:List + ForEach + 自定义组件 + 分割线
 *
 * 布局要点:
 *   1. List 组件作为整体垂直滚动容器;
 *   2. ListItem 内使用 Row + Column 组合实现"头像 | 昵称+描述"布局;
 *   3. 头像使用 Circle + Text 组合生成纯色圆形占位头像(无需本地图片资源);
 *   4. 每个 ListItem 之间通过 List 的 divider 属性添加分割线;
 *   5. 使用 @Builder 封装列表项布局,提升代码复用性。
 */

// ---------- 定义联系人数据类型 ----------
/**
 * 联系人数据模型
 * name    - 联系人昵称
 * avatar  - 头像背景色(使用纯色圆形 + 姓氏首字,无需图片文件)
 * desc    - 联系人描述 / 签名信息
 */
interface ContactItem {
  name: string;
  color: string;     // 头像圆形背景色
  desc: string;
}

// 预定义一组头像背景色(轮流使用)
const AVATAR_COLORS: string[] = [
  '#4A90D9', '#E8833A', '#47B881', '#D85A5A',
  '#7B68EE', '#D97CB5', '#5AB0C6', '#F0A030',
];

// ============================================================
// 页面入口组件
// ============================================================
@Entry
@Component
struct Index {
  /**
   * @State 装饰的 contacts 数组驱动 UI 更新。
   * 头像使用姓氏首字 + 纯色圆形背景,无需本地图片资源,
   * 开箱即用,预览或真机均可直接展示。
   */
  @State contacts: ContactItem[] = [
    { name: '张三',  color: AVATAR_COLORS[0], desc: '鸿蒙开发者 · 全栈工程师' },
    { name: '李四',  color: AVATAR_COLORS[1], desc: 'UI/UX 设计师 · 爱画画' },
    { name: '王五',  color: AVATAR_COLORS[2], desc: '后端架构师 · 马拉松爱好者' },
    { name: '赵六',  color: AVATAR_COLORS[3], desc: '测试工程师 · 代码质量守护者' },
    { name: '孙七',  color: AVATAR_COLORS[4], desc: '产品经理 · 需求文档书写者' },
    { name: '周八',  color: AVATAR_COLORS[5], desc: '运维工程师 · 7×24 在线' },
    { name: '吴九',  color: AVATAR_COLORS[6], desc: '数据科学家 · 用数据说话' },
    { name: '郑十',  color: AVATAR_COLORS[7], desc: '安全工程师 · 守护每一行代码' },
  ];

  // ============================================================
  // build 方法 — 页面 UI 树
  // ============================================================
  build() {
    // ---- 外层列:标题 + 列表 ----
    Column() {
      // ---- 页面标题栏 ----
      Text('通讯录')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .width('100%')
        .height(56)
        .textAlign(TextAlign.Center)
        .backgroundColor('#0078D7')
        .fontColor(Color.White)
        .padding({ top: 12 })

      // =========================================================
      // 【核心】List 组件 — 垂直滚动的联系人列表
      // 布局要点:
      //   ① List 本身不设固定高度,由父容器 Column 约束;
      //   ② 通过 ForEach 循环渲染每一个 ListItem;
      //   ③ ListItem 内部使用 Row + Column 实现横向图文混排;
      //   ④ 分割线通过 List.divider() 属性统一配置,简洁统一。
      // =========================================================
      List({ space: 0 }) {
        // ForEach 遍历 contacts 数组,为每个联系人创建一个列表项
        ForEach(this.contacts, (item: ContactItem) => {
          // ---- 单个列表项 ----
          ListItem() {
            // 使用 @Builder 构建的列表项内容
            this.ContactListItem(item)
          }
          // -------- 分割线说明 --------
          // 分割线已通过 List.divider() 全局配置(见下方),
          // 无需在每个 ListItem 后手动添加 Divider 组件。
          // 若需每个列表项独立控制分割线样式,可使用
          // ListItemSeparator { .strokeWidth(0.5).startMargin(72) }
        })
      }
      // -------- List 全局属性 --------
      .width('100%')
      .layoutWeight(1)                         // 填满剩余垂直空间
      .divider({                               // 统一设置列表项分割线
        strokeWidth: 0.5,                      // 分割线线宽 0.5vp
        color: '#e0e0e0',                      // 浅灰色分割线
        startMargin: 72,                       // 从左 72vp 处开始(对齐头像右侧)
        endMargin: 16                          // 距右 16vp 结束
      })
      .edgeEffect(EdgeEffect.Spring)           // 列表滚动到边缘时回弹效果
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f5f5f5')               // 整体背景色:浅灰
  }

  // ============================================================
  // @Builder 封装列表项布局
  // 将每个 ListItem 的内部布局提取为独立构建函数,便于复用。
  // 布局结构: [圆形头像] [昵称 + 描述] [> 箭头]
  // ============================================================
  @Builder
  ContactListItem(item: ContactItem) {
    // ---- 行容器:头像 | 文本信息 | 箭头 ----
    Row() {
      // ---- ① 头像(圆形,纯色背景 + 姓氏首字) ----
      // 使用 Stack 叠加圆形背景和文字,避免依赖任何图片资源
      Stack() {
        Circle()
          .width(48)
          .height(48)
          .fill(item.color)                    // 使用预设颜色填充
        Text(item.name.slice(0, 1))            // 取姓氏第一个字
          .fontSize(18)
          .fontColor(Color.White)
          .fontWeight(FontWeight.Bold)
      }
      .width(48)
      .height(48)
      .margin({ left: 16, right: 12 })

      // ---- ② 昵称 + 描述(纵向排列) ----
      Column() {
        // 昵称
        Text(item.name)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#333333')
          .width('100%')
          .textAlign(TextAlign.Start)

        // 描述
        Text(item.desc)
          .fontSize(13)
          .fontColor('#999999')
          .width('100%')
          .textAlign(TextAlign.Start)
          .margin({ top: 4 })                  // 与昵称保持 4vp 间距
      }
      .alignItems(HorizontalAlign.Start)        // Column 内部左对齐
      .layoutWeight(1)                          // 文本区域填满剩余宽度

      // ---- ③ 右侧箭头图标(使用文本替代,无需系统资源) ----
      Text('>')
        .fontSize(16)
        .fontColor('#cccccc')
        .margin({ right: 16 })
    }
    .width('100%')
    .height(72)                                  // 列表项高度 72vp(符合 HIG 建议)
    .alignItems(VerticalAlign.Center)            // Row 内垂直居中
    .backgroundColor(Color.White)                // 列表项白色背景
    .padding({ top: 0, bottom: 0 })
  }
}

四、布局要点逐层解析

4.1 整体页面结构

Column(全屏)
 ├── Text "通讯录"          ← 标题栏(蓝底白字,高 56vp)
 └── List                   ← 列表容器(layoutWeight:1 填满剩余空间)
      ├── ForEach
      │    ├── ListItem 0   ← 联系人条目
      │    ├── ListItem 1
      │    └── ...
      └── .divider()        ← 统一分割线

最外层 Column 将页面划分为上下两个区域:标题栏和列表区域。layoutWeight(1)List 自动填充扣除标题栏之后的所有剩余高度,无需手动计算。

4.2 List 组件的核心属性

List 是鸿蒙中最重要的滚动容器组件之一。本示例用到的关键属性:

属性 作用
{ space: 0 } 构造参数 列表项之间的间距(0 表示紧贴,由分割线提供视觉分隔)
.width('100%') 字符串 宽度占满父容器
.layoutWeight(1) 数字 弹性权重,填满父容器剩余空间
.divider({...}) 对象 统一配置列表项分割线
.edgeEffect(EdgeEffect.Spring) 枚举 滚动到头/尾时的弹簧回弹动画

特别说明List 构造参数 { space: 0 } 控制项间距。如果设置为非零值,space 在下拉刷新或滑动删除等交互中会影响动画效果,因此建议使用 divider 替代 space 做视觉分隔。

4.3 列表项的内部布局

每个 ListItem 的内部结构(由 @Builder ContactListItem 定义):

Row(高 72vp,白底,垂直居中)
 ├── Stack(48×48)           ← 圆形头像容器
 │    ├── Circle(fill: color)← 彩色圆形
 │    └── Text(姓氏首字)    ← 白色文字居中
 ├── Column(layoutWeight:1) ← 文本信息
 │    ├── Text(昵称)        ← 16px 深色
 │    └── Text(描述)        ← 13px 灰色
 └── Text(">")               ← 右侧箭头

头像的设计考量:传统联系人列表需要为每个联系人准备头像图片,这在 DEMO 阶段会增加复杂度。本示例采用 Stack + Circle + Text 组合——Circle 设置不同的 fill 颜色作为背景,Text 显示姓氏的第一个字。这样既美观又完全脱离了图片资源的依赖,适合作为快速原型或演示项目。

4.4 分割线的两种实现方式

ArkTS 为列表分割线提供了两种配置途径:

方式一:List.divider() 全局配置(推荐,本示例采用)

List() { ... }
  .divider({
    strokeWidth: 0.5,     // 线宽
    color: '#e0e0e0',     // 颜色
    startMargin: 72,      // 起始缩进(对齐头像右侧)
    endMargin: 16         // 结束缩进
  })

优点:一行配置,全局生效;分隔线是列表底层绘制,不影响列表项的高度计算。

方式二:ListItemSeparator 组件

ListItem() { ... }
ListItemSeparator {
  .strokeWidth(0.5)
  .startMargin(72)
}

优点:每个列表项可以独立控制分割线样式(例如最后一项可以隐藏)。

最佳实践:如果所有列表项的分割线样式一致,优先使用 .divider();如果某些项需要特殊处理(如隐藏、粗线、不同颜色),则使用 ListItemSeparator

4.5 @Builder 装饰器的作用

@Builder 是 ArkTS 中用来封装可复用 UI 片段的装饰器。在本例中,我们将每个列表项的布局抽出为 ContactListItem 方法,而不是直接写在 build() 方法中。

这样做的好处:

  1. 代码可读性build() 方法结构清晰,一眼看出整体布局骨架
  2. 复用性:如果页面其他地方也需要展示联系人卡片,直接调用 this.ContactListItem(item) 即可
  3. 可维护性:修改列表项 UI 时只需改动 @Builder 内部,不影响外层逻辑

五、状态驱动与数据流

5.1 @State 装饰器的用法

@State contacts: ContactItem[] = [ ... ];

@State 是 ArkTS 中最重要的响应式装饰器之一。被 @State 装饰的变量具有以下特性:

  • 值变化自动触发 UI 重新渲染:当 contacts 数组的元素被修改(增删改)时,List 会自动重新绘制受影响的 ListItem
  • 深层次监听:数组内部元素的属性变更也会触发更新(例如修改某个联系人的 desc 字段)

这种机制使得开发者只需要关注数据层的变更,UI 层由框架自动维护——这就是"状态驱动 UI"的核心思想。

5.2 数据变更示例

假设我们需要动态添加联系人,只需这样写:

// 在 struct Index 内部
addContact() {
  this.contacts.push({
    name: '新联系人',
    color: AVATAR_COLORS[this.contacts.length % AVATAR_COLORS.length],
    desc: '新加入的联系人'
  });
  // 无需手动操作 DOM,List 会自动更新
}

再比如修改某个联系人的描述:

updateDesc(index: number, newDesc: string) {
  this.contacts[index].desc = newDesc;
  // 框架自动找到对应的 ListItem 并重新渲染
}

六、常见问题与避坑指南

6.1 头像图片加载失败怎么办?

如果你坚持使用图片作为头像(例如从网络加载),建议设置兜底策略:

Image(item.avatarUrl)
  .width(48)
  .height(48)
  .borderRadius(24)
  .backgroundColor('#cccccc')           // 图片加载前的占位色
  .alt($r('sys.media.ohos_app_icon'))   // 加载失败时的兜底图

6.2 API 24 中 List 组件的变化

在 API 24(HarmonyOS NEXT 6.2+)中,List 组件有如下演进:

  • 性能优化:懒加载机制更完善,万级列表不需要额外配置 LazyForEach 也能保持流畅
  • 手势冲突处理ListItem 内嵌可滑动组件(如 SwipeAction)时,滚动冲突自动解决
  • 新属性.scrollBar() 支持更细粒度的滚动条样式控制
  • 废弃 APIListItemsticky 属性已由 List.sticky() 代替

6.3 编译错误排查

如果你遇到 ArkTS 编译错误,优先检查以下几点:

错误类型 常见原因 解决方案
',' expected 链式调用写在了不支持的语法位置(如 ForEach 的 {} 后直接加 .xxx() 将属性移动到 List 级别或 ListItem 内部
Cannot find name 'xxx' 拼写错误或未 import ArkTS 内置组件无需 import,但枚举值如 EdgeEffect.Spring 需确认包名
Unknown resource name $r('sys.media.xxx') 引用了不存在的系统资源 改用 app.media 自有资源,或纯代码实现
Property 'xxx' does not exist API 版本不支持该属性 检查 build-profile.json5 中的 compatibleSdkVersion

七、总结

本文从零到一构建了一个鸿蒙 NEXT(API 24)联系人列表页面,涵盖了以下核心知识点:

  1. ArkTS 页面结构@Entry @Component 装饰器定义页面入口,build() 方法构建 UI 树
  2. List 组件:作为垂直滚动容器,配合 ForEach 循环渲染数据驱动的列表项
  3. @Builder 封装:将列表项布局独立为可复用的构建函数
  4. 纯代码头像Stack + Circle + Text 组合实现圆形色彩头像,零图片依赖
  5. 分割线配置List.divider() 统一声明式配置 vs. ListItemSeparator 细粒度控制
  6. 状态驱动@State 响应式装饰器实现数据 → UI 的自动映射

这套布局模式适用于鸿蒙应用中绝大多数列表类场景:通讯录、设置页、消息列表、商品列表、订单列表等。掌握了 List + ForEach + @Builder 的组合拳,就掌握了鸿蒙页面开发中最核心的布局能力。

配套资源:本文完整代码可在 DevEco Studio 中直接导入运行。如有任何问题,欢迎在评论区留言交流。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐