鸿蒙原生 ArkTS 布局深度解析:List 空状态占位 emptyState 实战


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

一、引言:为什么「空状态」如此重要?

在移动应用开发中,空状态(Empty State)是指列表、搜索结果等数据容器在没有任何内容时呈现的界面。很多开发者容易忽视这个边界场景,直接将空白页面丢给用户——这会给体验带来明显降级。

1.1 空状态的三种常见形态

类型 说明 示例
首次使用 用户刚安装应用,尚无数据 待办清单首次打开
清空/完成 用户主动将数据消耗完毕 收件箱全部归档
无结果 搜索或筛选没有命中数据 搜索「XYZ」无匹配项

1.2 优秀空状态设计四原则

  • 引导性:告诉用户这里应该有什么、可以做什么
  • 情感化:通过图形、文案传递友好态度,降低挫败感
  • 可操作性:提供明确的下一步入口(新建、添加、刷新)
  • 品牌一致性:配色与字体与 App 整体调性统一

二、HarmonyOS NEXT API 24 的 List + emptyState 方案

在早期 SDK 中,开发者实现空状态需借助 if/else 条件渲染手动切换。当多个列表各自需要空状态时,模板判断代码重复度高。

API 24(SDK 7.x) 引入了 List 组件的 .emptyState() 属性,这是 ArkUI 内置的声明式空状态解决方案。

2.1 核心 API

/**
 * 当 List 子组件数量为 0 时,自动展示占位 UI,
 * 数据恢复后自动隐藏。
 */
emptyState(value: CustomBuilder): ListAttribute

2.2 与传统方案对比

维度 if/else 旧方案 emptyState API 24
代码量 每个 List 需额外 if 分支 一行链式调用的
可维护性 多列表时重复判断 声明式绑定,关注点分离
语义清晰度 需阅读逻辑分支 命名即语义

三、场景设计:待办清单 App

3.1 功能需求

  • 展示待办事项列表(Checkbox + 内容 + 删除按钮)
  • 完成态自动添加删除线
  • 列表为空时显示友好占位提示
  • 提供「清空列表」和「恢复示例数据」用于状态切换
  • 单条删除时若列表全部清空弹出反馈

3.2 数据模型

interface TodoItem {
  id: number;
  content: string;
  isDone: boolean;
}

四、完整代码实现

以下代码基于 HarmonyOS NEXT API 24,使用 List.emptyState() 原生 API。

/**
 * 鸿蒙 ArkTS —— List + emptyState 空状态占位示例
 * 适用:HarmonyOS NEXT API 24 (SDK 7.x)
 */

import { promptAction } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';

interface TodoItem {
  id: number;
  content: string;
  isDone: boolean;
}

@Entry
@Component
struct TodoListPage {

  @State private todoList: TodoItem[] = [
    { id: 1, content: '学习鸿蒙 ArkTS 语法', isDone: true },
    { id: 2, content: '掌握 List.emptyState API', isDone: false },
    { id: 3, content: '编写完整示例应用', isDone: false },
  ];

  /** 空状态占位 UI 构建器 */
  @Builder
  emptyStateBuilder() {
    Column() {
      SymbolGlyph($r('sys.symbol.inbox'))
        .fontSize(72)
        .fontColor(['#BBBBBB'])
      Blank()
      Text('暂无待办事项')
        .fontSize(18)
        .fontColor('#666666')
        .fontWeight(FontWeight.Medium)
        .margin({ top: 16 })
      Text('点击下方按钮添加一条新的待办吧')
        .fontSize(14)
        .fontColor('#999999')
        .margin({ top: 8 })
      Button('添加示例数据')
        .type(ButtonType.Capsule)
        .height(40)
        .width(160)
        .margin({ top: 24 })
        .onClick(() => { this.loadSampleData(); })
    }
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .width('100%').height('100%')
  }

  /** 列表项卡片构建器 */
  @Builder
  todoItemBuilder(item: TodoItem, index: number) {
    Row() {
      Checkbox()
        .select(item.isDone)
        .shape(CheckBoxShape.CIRCLE)
        .size({ width: 22, height: 22 })
        .onChange((v: boolean) => { this.todoList[index].isDone = v; })
      Text(item.content)
        .fontSize(16)
        .fontColor(item.isDone ? '#BBBBBB' : '#333333')
        .decoration({
          type: item.isDone
            ? TextDecorationType.LineThrough
            : TextDecorationType.None
        })
        .margin({ left: 12 }).flexGrow(1)
      Button({ type: ButtonType.Circle, stateEffect: true }) {
        Text('✕').fontSize(16).fontColor('#FF6B6B')
      }
      .width(32).height(32)
      .backgroundColor('rgba(255,107,107,0.1)')
      .onClick(() => { this.deleteItem(index); })
    }
    .width('100%').height(56)
    .padding({ left: 16, right: 12 })
    .alignItems(VerticalAlign.Center)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }

  build() {
    Column() {
      // 标题栏
      Column() {
        Text('📋 我的待办').fontSize(22)
          .fontWeight(FontWeight.Bold).fontColor('#333333')
        Text('List + emptyState 示例').fontSize(12)
          .fontColor('#999999').margin({ top: 4 })
      }
      .width('100%')
      .padding({ top: 24, bottom: 12, left: 20, right: 20 })

      // ===== 核心:List + emptyState =====
      List({ space: 10 }) {
        ForEach(this.todoList, (item: TodoItem, index?: number) => {
          ListItem() { this.todoItemBuilder(item, index as number) }
        }, (item: TodoItem) => item.id.toString())
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ left: 16, right: 16, top: 8 })
      .backgroundColor('#F5F5F5')
      .emptyState(this.emptyStateBuilder)  // 绑定空状态占位

      // 底部操作栏
      Row({ space: 16 }) {
        Button('清空列表').type(ButtonType.Outlined)
          .height(44).layoutWeight(1).fontSize(15)
          .onClick(() => { this.clearList(); })
        Button('恢复示例数据').type(ButtonType.Capsule)
          .height(44).layoutWeight(1).fontSize(15)
          .onClick(() => { this.loadSampleData(); })
      }
      .width('100%').padding(16).backgroundColor(Color.White)
    }
    .width('100%').height('100%').backgroundColor('#F5F5F5')
  }

  private showToast(msg: string): void {
    try {
      promptAction.showToast({ message: msg, duration: 1500 });
    } catch (err) {
      hilog.error(0x0001, 'Page',
        'showToast failed: %{public}s', JSON.stringify(err));
    }
  }

  private clearList(): void {
    this.todoList = [];
    this.showToast('列表已清空,空状态已触发');
  }

  private loadSampleData(): void {
    const now = Date.now();
    this.todoList = [
      { id: now + 1, content: '学习鸿蒙 ArkTS 语法', isDone: true },
      { id: now + 2, content: '掌握 List.emptyState API', isDone: false },
      { id: now + 3, content: '编写完整示例应用', isDone: false },
    ];
  }

  private deleteItem(index: number): void {
    this.todoList.splice(index, 1);
    if (this.todoList.length === 0) this.showToast('全部清空 🎯');
  }
}

五、代码分层解析

5.1 状态层:@State todoList

@State 装饰的 todoList 是整个页面的数据核心。数组内容变化时,ArkUI 自动触发 UI 重渲染:

  • this.todoList = [] → 清空 → ForEach 无数据 → emptyState 激活
  • this.todoList = [...] → 恢复 → ForEach 有数据 → emptyState 隐去

5.2 视图层:两个 @Builder

构建器 渲染条件 用途
emptyStateBuilder 列表为空 图标 + 提示文字 + 操作按钮
todoItemBuilder 列表有数据 复选框 + 文本 + 删除按钮

这种拆分让 build() 函数极其干净——List 只需关心「容器」角色。

5.3 控制层:.emptyState(this.emptyStateBuilder)

这是 API 24 的关键能力。emptyState 是一个布林条件属性:

  • 条件 true(列表无数据):框架调用 builder 生成占位节点
  • 条件 false(列表有数据):框架销毁占位节点,正常渲染列表

开发者无需任何 if/else 即可获得完整的空状态管理。

5.4 交互层:状态切换驱动

两个底部按钮分别触发 clearList()loadSampleData()。空状态 UI 内部也放置了「添加示例数据」按钮,让用户不需要滚动到底部即可恢复数据——这是移动端空状态设计的黄金法则。


六、运行时效果预览

初始态(有数据):

┌─────────────────────────────┐
│  📋 我的待办                  │
├─────────────────────────────┤
│  ○ 学习鸿蒙 ArkTS 语法     ✕ │  ← 已完成(灰色+删除线)
│  ● 掌握 List.emptyState   ✕ │  ← 未完成
│  ● 编写完整示例应用        ✕ │  ← 未完成
├─────────────────────────────┤
│  [ 清空列表 ]  [ 恢复示例数据 ] │
└─────────────────────────────┘

空状态(清空后):

┌─────────────────────────────┐
│  📋 我的待办                  │
├─────────────────────────────┤
│            📭                │  ← SymbolGlyph 图标
│     暂无待办事项              │  ← 主提示
│  点击下方按钮添加新的待办吧     │  ← 副提示
│     [ 添加示例数据 ]           │  ← 操作入口
├─────────────────────────────┤
│  [ 清空列表 ]  [ 恢复示例数据 ] │
└─────────────────────────────┘

占位 UI 居于 List 区域正中央,视觉聚焦、层次分明。


七、进阶技巧与最佳实践

7.1 配合 LazyForEach

大数据量时应使用 LazyForEach 支持按需加载。emptyStateLazyForEach 同样生效——当 totalCount 为 0 时自动触发。

List({ space: 10 }) {
  LazyForEach(this.dataSource, (item: TodoItem) => {
    ListItem() { ... }
  }, (item: TodoItem) => item.id.toString())
}
.emptyState(this.emptyStateBuilder)

7.2 空状态动效过渡

通过 .transition() 为 emptyState 的进出添加微动效:

.emptyState(this.emptyStateBuilder)
.transition(TransitionEffect.opacity(0.3))

7.3 多 List 独立空状态

页面中有多个 List 时,各自绑定自己的 @Builder 即可互不干扰:

Column() {
  List(...) { ... }.emptyState(this.categoryEmpty)
  List(...) { ... }.emptyState(this.todayEmpty)
}

7.4 嵌入更多交互元素

空状态中可放置 Refresh 组件、图像动画、推荐词链接等,增强引导性。

7.5 多语言 / 主题适配

使用 $r('app.string.xxx') 引用资源文件,使空状态随系统语言和主题自动切换:

Text($r('app.string.empty_todo_title'))
  .fontColor($r('sys.color.ohos_id_color_text_primary'))

八、性能注意事项

  1. 避免占位内包裹大图片:空状态触发时需快速渲染,建议使用矢量图标(SymbolGlyph / Text)
  2. @Builder 应为无参函数emptyState 绑定的 builder 应为无参,如需动态数据通过 @State 间接获取
  3. 无需嵌套 Scroll:占位内容通常不滚动,保持精简即可
  4. 检查数据源类型:确保 ForEach / LazyForEach 的 dataSource 正确绑定,避免非预期触发

九、常见问题 FAQ

Q:emptyState 在有静态子组件时如何工作?

A:emptyState 仅根据 ForEach / LazyForEach 绑定的数据源判断。List 中的静态 ListItem 不受影响。

Q:为什么我的 emptyState 不显示?

A:检查三点:

  • API 版本 ≥ 24
  • @Builder 不带参数,通过 this.xxxBuilder 引用
  • ForEach 的数据源确实为 [] 且绑定的是 @State 变量

Q:可以在 emptyState 中使用路由跳转吗?

A:可以。@Builder 内部支持所有标准事件处理,包括 router.pushUrl()NavPathStack 跳转。


十、总结

HarmonyOS NEXT API 24 的 List.emptyState() 是 ArkUI 在声明式编程方向上的重要进化。它让开发者以 一行代码 的增量,获得原本需要多层模板判断才能实现的空状态管理能力。

核心收益:

维度 收益
代码可读性 语义化命名,一目了然
开发效率 零模板代码,关注点分离
维护成本 统一管理,一处修改全局生效
用户体验 一致性占位设计,专为交互优化

更重要的是,emptyState 体现出 「状态驱动 UI」 的设计哲学——开发者只需关心 what(空状态长什么样),框架自动处理 when(何时显示)和 how(如何过渡与回收)。

希望本文能帮助你深入理解 List + emptyState 的使用方式,并在自己的 HarmonyOS NEXT 项目中落地这一优雅的设计模式。


本文由 AtomCode 撰写,发布于 2026 年 6 月。示例代码基于 HarmonyOS NEXT API 24(SDK 7.x),兼容 stage 模型。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐