鸿蒙原生 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):ListAttribute2.2 与传统方案对比
| 维度 | if/else 旧方案 | emptyState API 24 |
|---|---|---|
| 代码量 | 每个 List 需额外 if 分支 | 一行链式调用的 |
| 可维护性 | 多列表时重复判断 | 声明式绑定,关注点分离 |
| 语义清晰度 | 需阅读逻辑分支 | 命名即语义 |
三、场景设计:待办清单 App
3.1 功能需求
- 展示待办事项列表(Checkbox + 内容 + 删除按钮)
- 完成态自动添加删除线
- 列表为空时显示友好占位提示
- 提供「清空列表」和「恢复示例数据」用于状态切换
- 单条删除时若列表全部清空弹出反馈
3.2 数据模型
interfaceTodoItem{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';interfaceTodoItem{id:number;content:string;isDone:boolean;}@Entry@Componentstruct TodoListPage{@StateprivatetodoList:TodoItem[]=[{id:1,content:'学习鸿蒙 ArkTS 语法',isDone:true},{id:2,content:'掌握 List.emptyState API',isDone:false},{id:3,content:'编写完整示例应用',isDone:false},];/** 空状态占位 UI 构建器 */@BuilderemptyStateBuilder(){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%')}/** 列表项卡片构建器 */@BuildertodoItemBuilder(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,indexasnumber)}},(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')}privateshowToast(msg:string):void{try{promptAction.showToast({message:msg,duration:1500});}catch(err){hilog.error(0x0001,'Page','showToast failed: %{public}s',JSON.stringify(err));}}privateclearList():void{this.todoList=[];this.showToast('列表已清空,空状态已触发');}privateloadSampleData():void{constnow=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},];}privatedeleteItem(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 模型。