鸿蒙原生 ArkTS 布局深度解析:List 空状态占位 emptyState 实战
鸿蒙原生 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 支持按需加载。emptyState 对 LazyForEach 同样生效——当 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'))
八、性能注意事项
- 避免占位内包裹大图片:空状态触发时需快速渲染,建议使用矢量图标(SymbolGlyph / Text)
- @Builder 应为无参函数:
emptyState绑定的 builder 应为无参,如需动态数据通过@State间接获取 - 无需嵌套 Scroll:占位内容通常不滚动,保持精简即可
- 检查数据源类型:确保
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 模型。
更多推荐


所有评论(0)