HarmonyOS开发实战:笔友-EmptyState 空状态组件的条件式占位设计

前言

在应用中,空状态(Empty State) 是提升用户体验的重要细节。xiexin 的 EmptyState 组件通过 49 行代码实现了"标题+副标题+按钮"的可配置空状态占位,配合 if/else 条件渲染,在列表为空时优雅地引导用户操作。

本文将以 CommonComponents.ets 中的 EmptyState 组件和 Index.ets 中的调用为蓝本,详细剖析空状态组件的设计原则、参数化配置、if/else 条件渲染在空状态中的应用,以及回调函数的设计模式。

一、EmptyState 完整代码

typescript 复制代码
// CommonComponents.ets
@Component
export struct EmptyState {
  @Prop title: string = '暂无内容';
  @Prop subtitle: string = '';
  @Prop showButton: boolean = false;
  @Prop buttonText: string = '';
  onButtonClick?: () => void;

  build() {
    Column({ space: 16 }) {
      Column() { Text('🏔').fontSize(64).opacity(0.3) }.margin({ top: 60 })
      Text(this.title).fontSize(16).fontColor(AppColors.TEXT_SECONDARY)
      if (this.subtitle.length > 0) {
        Text(this.subtitle).fontSize(13).fontColor(AppColors.TEXT_SECONDARY).opacity(0.7)
      }
      if (this.showButton) {
        Button(this.buttonText).fontSize(14).fontColor(AppColors.WHITE)
          .backgroundColor(AppColors.PRIMARY).borderRadius(24).height(40).width(140).margin({ top: 16 })
          .onClick(() => { if (this.onButtonClick) { this.onButtonClick(); } })
      }
    }
    .width('100%').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
  }
}

二、参数设计

参数 类型 默认值 用途
title string '暂无内容' 主要提示文字
subtitle string '' 副标题说明
showButton boolean false 是否显示按钮
buttonText string '' 按钮文字
onButtonClick () => void undefined 按钮点击回调

三、在 Index 中的使用

typescript 复制代码
// 信箱 Tab 空状态
if (this.letters.length === 0) {
  EmptyState({ title: '还没有信件,去写第一封吧', subtitle: '每一封信都是一段温暖的旅程' })
}
// 笔友 Tab 空状态(带按钮)
if (this.penPals.length === 0) {
  EmptyState({
    title: '还没有笔友,邀请一位吧', subtitle: '最多5位笔友,深度交流',
    showButton: true, buttonText: '邀请笔友',
    onButtonClick: () => { router.pushUrl({ url: 'pages/AddPenPalPage' }) }
  })
}

四、装饰性元素

typescript 复制代码
Column() { Text('🏔').fontSize(64).opacity(0.3) }.margin({ top: 60 })

五、条件渲染

typescript 复制代码
if (this.subtitle.length > 0) { Text(this.subtitle) }
if (this.showButton) { Button(this.buttonText) }

六、回调函数设计

typescript 复制代码
onButtonClick?: () => void;
.onClick(() => { if (this.onButtonClick) { this.onButtonClick(); } })

七、扩展建议

typescript 复制代码
@Component
export struct EmptyState {
  @Prop icon: string = '🏔';
  @Prop iconSize: number = 64;
  @Prop title: string = '暂无内容';
  @Prop subtitle: string = '';
  @Prop showButton: boolean = false;
  @Prop buttonText: string = '';
  onButtonClick?: () => void;
}

八、使用场景

场景 标题 副标题 按钮
信箱为空 还没有信件,去写第一封吧 每一封信都是一段温暖的旅程
笔友为空 还没有笔友,邀请一位吧 最多5位笔友,深度交流 邀请笔友

九、与设计系统的集成

  1. 字体:标题 16sp/Regular,副标题 13sp/Regular,按钮 14sp/Medium
  2. 颜色 :标题 TEXT_SECONDARY,按钮 PRIMARY 背景
  3. 间距:组件间距 16px,按钮上部间距 16px

十、无障碍适配

typescript 复制代码
Column().accessibilityText(this.title).accessibilityDescription(this.subtitle)

十一、动画效果

typescript 复制代码
Text('🏔').fontSize(64).opacity(0.3).animation({ duration: 600, curve: Curve.EaseOut })

十二、组件测试

typescript 复制代码
import { describe, it, expect } from '@ohos/hypium';
describe('EmptyState', () => {
  it('should display title', () => {
    const component = new EmptyState(); component.title = '测试标题';
    expect(component.title).toBe('测试标题');
  });
  it('should show button when showButton is true', () => {
    const component = new EmptyState(); component.showButton = true;
    expect(component.showButton).toBeTrue();
  });
});

十三、性能优化

  1. 条件渲染 :使用 if/else 控制按钮和副标题的渲染
  2. 避免过度渲染:空状态组件只在列表为空时渲染
  3. 图片懒加载:装饰性图标使用 emoji 而非图片资源

十四、常见问题排查

问题 原因 解决方案
按钮不显示 showButton 为 false 设置 showButton: true
点击无反应 onButtonClick 未设置 传入回调函数
显示异常 容器未设置 width 确保容器宽度为 100%

十五、组件复用策略

EmptyState 组件在多处复用时,建议:

  1. 统一文案管理 :将空状态文案集中到 Constants.ets
  2. 差异化配置 :通过 @Prop 参数实现不同场景的差异化
  3. 回调函数 :通过 onButtonClick 实现按钮操作的定制

十六、与 Layout 组件的协作

typescript 复制代码
Column() {
  if (this.letters.length === 0) {
    EmptyState({ title: '还没有信件', subtitle: '去写第一封吧' })
  } else {
    Scroll() {
      Column({ space: 12 }) {
        ForEach(this.letters, (letter: Letter) => { this.LetterCard(letter) }, (l: Letter) => l.id.toString())
      }.padding(16)
    }.layoutWeight(1)
  }
}.width('100%').height('100%')

十七、代码规范

typescript 复制代码
// 推荐:明确的参数名
EmptyState({ title: '暂无内容', subtitle: '请稍后再试' })
// 不推荐:模糊的布尔参数
EmptyState({ showButton: true })

十八、状态管理

typescript 复制代码
@State isEmpty: boolean = false;
public setEmpty(empty: boolean): void { this.isEmpty = empty; }

十九、补充说明

版本 新增功能 变更说明
v1.0 基础标题+副标题 初始版本
v1.1 按钮支持 新增 showButton/buttonText
v1.2 回调函数 新增 onButtonClick
v2.0 图标自定义 新增 icon/iconSize

二十、深度实现分析

20.1 核心原理

本功能的核心原理基于 ArkUI 的响应式状态管理机制。当 @State 或 @Prop 装饰的变量发生变化时,ArkUI 引擎会自动触发依赖该变量的 UI 部分重新渲染,无需手动操作 DOM。

20.2 数据流设计

渲染错误: Mermaid 渲染失败: Parse error on line 2: ... LR A用户交互 --> B@State 变量变化 B ----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'

20.3 性能考虑

  1. 避免不必要渲染:使用 @Watch 控制渲染时机
  2. 减少嵌套深度:保持组件树扁平化
  3. 合理使用缓存:计算结果可缓存避免重复计算

二十一、实际项目应用

在 xiexin 项目中,本功能被应用于以下场景:

  1. 笔友列表:展示笔友通信状态和关系阶段
  2. 信件卡片:展示信件内容和状态标签
  3. 统计页面:展示写信趋势数据和统计指标
typescript 复制代码
@Component
export struct ExampleComponent {
  @Prop data: string[] = [];
  build() {
    Column() {
      ForEach(this.data, (item: string) => {
        Text(item).fontSize(14).padding(8)
      }, (item: string) => item)
    }
  }
}

二十二、生产环境注意事项

  1. 错误处理:所有异步操作需要 try-catch 包围
  2. 日志记录:使用 hilog 记录关键操作和异常信息
  3. 性能监控:使用 hiTraceMeter 进行性能埋点分析
  4. 内存管理:及时清理定时器和监听器避免内存泄漏
typescript 复制代码
try {
  await this.loadData();
  hilog.info(0xFF00, 'TAG', 'Data loaded successfully');
} catch (err) {
  hilog.error(0xFF00, 'TAG', 'Failed to load: %{public}s', err.message);
}

二十三、代码审查清单

  1. @Prop 变量是否已赋默认值
  2. 定时器是否在 aboutToDisappear 中清理
  3. 列表渲染的 keyGenerator 是否唯一且稳定
  4. 条件渲染是否使用 if/else 而非 Visibility.Hidden
  5. 复杂计算结果是否已缓存
  6. 事件监听器是否在 aboutToDisappear 中取消注册

二十四、综合示例

typescript 复制代码
@Entry
@Component
struct DemoPage {
  @State items: string[] = ['示例1', '示例2', '示例3'];
  @State count: number = 0;
  build() {
    Column({ space: 16 }) {
      Text('综合示例').fontSize(24).fontWeight(FontWeight.Bold)
      Text(`计数: ${this.count}`).fontSize(16)
      Row({ space: 8 }) {
        Button('增加').onClick(() => { this.count++ })
        Button('减少').onClick(() => { if (this.count > 0) this.count-- })
        Button('重置').onClick(() => { this.count = 0 })
      }
      List() {
        ForEach(this.items, (item: string) => {
          ListItem() { Text(item).fontSize(14).padding(12) }
        }, (item: string) => item)
      }.height(200)
    }.padding(16).width('100%')
  }
}

二十五、相关 API 参考

API 说明 版本要求
@State 组件内部状态管理 API 9+
@Prop 父子单向传递 API 9+
@Link 父子双向同步 API 9+
@Watch 状态变化监听 API 9+
AppStorage 全局状态存储 API 9+
PersistentStorage 持久化存储 API 9+

二十六、常见面试题

Q1: @State 和 @Prop 的区别是什么?

A: @State 是组件内部私有状态,只能在当前组件内修改;@Prop 是父组件传递进来的数据,在子组件中只能读取,修改不会影响父组件。

Q2: ForEach 的 keyGenerator 为什么重要?

A: keyGenerator 决定了 ForEach 进行 Diff 算法的依据。如果键值不稳定或重复,会导致列表项渲染异常,如闪烁、状态丢失等问题。

二十七、调试技巧

  1. 使用 DevEco Profiler:监控帧率和布局耗时,定位卡顿根因
  2. 使用 hilog:打印关键日志,追踪代码执行路径
  3. 使用 hiTraceMeter:进行性能埋点分析,识别性能瓶颈
  4. 使用 @Watch:监听状态变化,调试状态更新逻辑
typescript 复制代码
@State @Watch('onDebugChange') debugValue: string = '';
onDebugChange(): void {
  console.log('Value changed to:', this.debugValue);
}

二十八、补充说明

提示:本文提供的代码示例基于 HarmonyOS API 12,适用于 HarmonyOS 5.0 及以上版本。如果你使用的是较低版本,部分 API 可能不兼容。

  1. 本文所有代码均可在 xiexin 项目中找到实际应用场景
  2. 建议结合 DevEco Studio 开发工具进行调试和验证
  3. 如有疑问,欢迎在评论区留言讨论,我会及时回复
  4. 更多 HarmonyOS 开发资源请参考官方文档和开发者社区

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

二十九、补充说明

提示:本文提供的代码示例基于 HarmonyOS API 12,适用于 HarmonyOS 5.0 及以上版本。部分 API 在低版本中可能不兼容,请根据实际开发环境调整。

  1. 本文所有代码均可在 xiexin 项目中找到实际应用场景
  2. 建议结合 DevEco Studio 开发工具进行调试和验证
  3. 如有疑问,欢迎在评论区留言讨论
  4. 更多 HarmonyOS 开发资源请参考官方文档

29.1 扩展阅读推荐

29.2 代码规范建议

在编写 HarmonyOS 应用时,建议遵循以下代码规范:

  1. 组件命名使用 PascalCase,如 AvatarComponent
  2. 变量命名使用 camelCase,如 avatarSize
  3. 常量命名使用 UPPER_CASE,如 MAX_COUNT
  4. 私有方法以 _ 开头,如 _getAvatarColor
  5. 文件命名使用 kebab-case,如 common-components.ets

三十、总结与最佳实践

30.1 核心要点总结

  1. 状态管理:合理选择 @State/@Prop/@Link/@StorageProp 装饰器
  2. 组件设计:遵循单一职责原则,保持组件聚焦
  3. 性能优化:大数据量使用 LazyForEach,组件复用使用 @Reusable
  4. 代码质量:编写单元测试,使用 Hypium 框架
  5. 样式管理:使用 AppColors 设计令牌统一管理颜色

30.2 推荐实践

  1. 使用 AppColors 设计令牌统一管理颜色,避免硬编码色值
  2. 使用 Constants.ets 集中管理常量,避免魔法数字
  3. 使用 DataStore 门面模式封装数据操作,统一访问入口
  4. 使用 @Builder 提取复用 UI 片段,减少重复代码
  5. 使用 @BuilderParam 实现组件插槽,提升组件灵活性

30.3 避免的反模式

  1. 避免在 build 函数中执行耗时操作,这会阻塞 UI 渲染
  2. 避免在 @State 中存储大型对象,会导致不必要的重渲染
  3. 避免过度使用 @Link 增加组件耦合,优先使用 @Prop
  4. 避免在 aboutToAppear 中执行异步操作,使用生命周期合理分配
  5. 避免使用全局变量替代 @StorageProp,全局变量无法触发响应式更新

提示:以上最佳实践基于 xiexin 项目的实际开发经验总结,建议在项目开发中遵守这些原则,可以有效提升代码质量和开发效率。

三十一、参考文档

  1. HarmonyOS 应用开发指南
  2. ArkUI 声明式开发范式
  3. 状态管理 V1
  4. 状态管理 V2
  5. 高性能编程实践
  6. 自定义组件
相关推荐
qizayaoshuap2 小时前
# 倒计时器 — HarmonyOS TextInput与计时任务管理深入实践
pytorch·深度学习·华为·harmonyos
木木子222 小时前
# 猜数字游戏 — HarmonyOS交互逻辑与随机算法实现
算法·游戏·华为·交互·harmonyos
爱写代码的阿木2 小时前
基于鸿蒙OS开发附近社交游戏平台(十二)-狼人杀角色技能与投票系统
游戏·华为·harmonyos
程序员黑豆3 小时前
鸿蒙开发:RelativeContainer相对布局组件实战——实现5个文本组件对齐
前端·华为·harmonyos
木木子223 小时前
# 待办清单应用 — HarmonyOS状态管理与列表渲染实战
华为·harmonyos
爱写代码的阿木5 小时前
基于鸿蒙OS开发附近社交游戏平台(五)-构建配置与签名部署
游戏·华为·harmonyos
爱写代码的阿森5 小时前
鸿蒙三方库 | harmony-utils之CharUtil字符类型判断详解
华为·harmonyos·鸿蒙·huawei
木木子225 小时前
# 计数器应用 — 从零开始的HarmonyOS ArkUI开发实战
华为·harmonyos
爱写代码的森6 小时前
鸿蒙三方库 | harmony-utils之PasteboardUtil剪贴板数据读写详解
服务器·华为·harmonyos·鸿蒙·huawei