【寻迹校园 HarmonyOS NEXT 实战 42】Loading、Empty、Error、Disabled:业务页面四类状态如何避免布局跳动

本章导读:这是“寻迹校园 HarmonyOS NEXT 实战”系列第 42 篇。本文以 HomePageMatchResultsPagePublishFormPageModerationReportPage 的现有 ArkUI 状态分支为证据,拆解 Loading、Empty、Error、Disabled 四类业务状态如何共享稳定页面骨架,并区分当前本机读写错误、平台失败和尚未捕获的网络/权限场景。

ArkUI 业务页面四态原创封面图

上图是原创状态概念图,不是项目截图。状态设计的目标不是多放四张插画,而是让标题、内容占位、操作区域和返回路径在状态切换时保持可预测。

一、只做成功态为什么一定会露馅

开发者最容易在种子数据齐全、权限已授予、设备性能正常的环境里看到成功态,于是页面看起来“已经完成”。真实运行时却会经历:

  • Repository 还在初始化;
  • 当前设备没有任何记录;
  • 组合筛选后结果为空;
  • 查询或写入失败;
  • 表单字段不足;
  • 用户重复点击提交;
  • 平台能力不可用。

如果这些阶段没有独立状态,常见表现就是空白、按钮连点、错误覆盖内容、加载完成后整页突然跳动。

二、四态不是四个布尔值的随意组合

建议先定义优先级,再写 UI:

Loading > Error > Empty > Content
Submitting/Saving -> Disabled overlay on actions
Pressed/Selected -> transient visual feedback

loading=true 时不应同时展示“暂无数据”;errorMessage 存在时不应把旧列表伪装成最新结果;saving=true 时按钮必须禁用,而不仅是把文案改成“正在保存”。

优先级就是状态机的最小版本。

三、首页的 Loading:保留列表容器,不抢占导航

HomePagerefreshReports() 开始时设置 loading=true、清空 errorMessage,结束后再关闭加载态。页面仅在内容区域显示 LoadingProgress 和“正在加载本机记录”。

导航、筛选标题和页面背景仍然保留,所以用户知道自己在哪里,也不会因为数据加载而看到整页闪白。

if (this.loading) {
  Column({ space: AppSpacing.SM }) {
    LoadingProgress().width(32).height(32)
    Text('正在加载本机记录')
  }
  .width('100%')
  .padding(AppSpacing.XL)
} else if (this.errorMessage.length > 0) {
  // 错误与重试
} else if (this.reports.length === 0) {
  // 空态与下一步
}

关键不在转圈,而在互斥分支:加载期间不会误显示旧空态。

四、Empty 不是“没有数据”四个字

首页区分两种空态:

  1. 当前设备从未登记记录;
  2. 已有记录,但当前关键词或组合筛选没有命中。

两者的下一步不同。第一种提供“登记丢失/登记拾得”,第二种提供“重置筛选”。如果统一写成“暂无数据”,用户无法判断是数据真的不存在,还是自己把筛选条件收得太窄。

Empty 状态至少要回答:为什么为空、可以做什么、做完后会发生什么。

五、匹配页的空态要解释业务前置条件

MatchResultsPage 不只是显示“0 个候选”,而是根据当前记录类型解释:丢失记录需要存在拾得记录,拾得记录需要存在丢失记录,并提供登记相反类型记录的按钮。

这能把“算法没工作”的误解转换成“本机缺少可比较数据”。同时页面持续强调相似分只用于排序,不代表归属,避免空态消失后又产生错误承诺。

六、Error 必须提供可恢复动作

首页错误分支显示“本机记录加载失败,请重试”,并提供“重新加载”;匹配页使用 OperationResult.userMessage,并提供“重新匹配”。

错误卡片具有独立背景色和圆角,但仍占据与内容区相同的父容器。这样错误不会以 Toast 一闪而过,也不会把重试按钮挤到不可预测的位置。

需要特别说明:这些代码能证明本地加载或业务返回失败后的 UI,不能自动证明网络超时、系统权限拒绝等未在当前路径出现的场景已经真机捕获。

七、Disabled 是业务门禁,不是浅灰色装饰

PublishFormPage 的提交条件不是一个简单的 title.length > 0。当前 canSubmit() 同时检查:

  • 页面不在 loading;
  • 没有正在保存草稿或记录;
  • 没有正在选择照片;
  • 标题、类别、地点、日期、描述满足最小长度;
  • 拾得记录还需要私密特征。
private canSubmit(): boolean {
  return !this.loading && !this.saving && !this.draftSaving && !this.photoSelecting &&
    this.title.trim().length >= 2 && this.category.trim().length >= 2 &&
    this.area.trim().length >= 2 && this.eventDate.length === 10 &&
    this.description.trim().length >= 5 &&
    (this.reportType === ReportType.LOST || this.privateFeature.trim().length >= 4);
}

同一个结果同时驱动 .enabled()、前景色、背景色和按钮文案,避免视觉显示可用但事件层仍可点击。

八、按钮文案要告诉用户缺什么

表单前两步的按钮会在条件不足时显示“完成本页必填”,满足后才显示“下一步”。最后一步保存时会显示“正在保存…”。

这比永远写“下一步”再弹出错误更直接。但它仍不等于完整字段级提示,所以提交时还会调用 reportService.validateDraft(),把业务校验结果返回页面。

Disabled 负责防误触,Validation 负责解释原因,两者不能互相替代。

九、举报 Sheet 的 loading 与 submitting 要分开

ModerationReportPage 先读取关联记录,再允许用户提交举报。按钮启用条件是:

.enabled(!this.loading && !this.submitting && this.errorMessage.length === 0)

加载关联记录和提交举报是两个不同阶段。如果只用一个 loading,提交完成后可能又触发整块内容骨架,用户不知道操作到底发生在哪一步。

页面还把被举报详情以降低透明度的背景展示,Sheet 固定在底部。加载态只替换 Sheet 内部内容,外部上下文不消失。

十、布局稳定要先固定三块骨架

一张业务页面通常可以抽象为:

Header:标题、返回、说明
Content:列表、表单、空态、错误态
Action:主按钮、次按钮、安全区

状态切换时尽量替换 Content 内部,而不是重建整个页面。底部操作区保留相同高度,按钮从 enabled 变为 disabled;标题区保持一致;空态和错误态使用相近的 padding 与卡片圆角。

ArkUI 稳定状态矩阵原创结构图

上图展示了同一页面框架在多状态下的固定锚点。图中尺寸是概念说明,不是寻迹校园真机像素测量结果。

十一、避免 if 分支导致高度突变

布局跳动常来自三个细节:

  • Loading 只有 32vp 转圈,成功态却是数百 vp 列表;
  • Error 文案长度变化后把底部按钮推走;
  • Empty 插画没有约束宽高,资源比例影响容器。

寻迹校园为空态插画设置 156×130,并给状态卡片统一 AppSpacing.XL;错误态也使用固定 padding。对于全屏表单,底部按钮放在内容滚动区之外,避免错误文案增长后消失。

十二、长文本要限制,但不能截断行动信息

标题和列表摘要可以使用 maxLines;错误文案与下一步说明不应只留一行省略号。用户最需要知道的恰好是失败原因和恢复办法。

推荐规则:

  • 卡片标题最多两行;
  • 辅助摘要可省略;
  • 错误原因允许换行;
  • 重试按钮始终可见;
  • 表单字段错误贴近字段或集中展示,但需保持可滚动。

十三、小屏与底部安全区

Phone 上软键盘、系统手势区和长文案会共同压缩空间。底部主按钮应拥有明确高度和下边距,表单主体使用 Scroll,不要用固定整页高度塞下所有字段。

举报 Sheet 当前高度为页面的 72%,内容区使用 Scroll,底部提示和提交按钮位于 Sheet 的稳定尾部。仍需在系统字体放大和不同安全区设备上单独复核,不能仅凭代码断言无溢出。

十四、暗色模式不是把错误卡片变黑

状态颜色应来自语义 Token:页面背景、卡片背景、错误容器、警告文字、禁用背景和禁用文字分别定义。Disabled 必须保留足够可读性,不能把文字和背景都压成相近灰色。

加载指示器也要使用品牌或前景 Token,不能硬编码只在浅色背景可见的颜色。

十五、Pressed 与 Selected 是第五类短生命周期状态

Loading、Empty、Error、Disabled 描述业务阶段;Pressed 和 Selected 描述用户交互反馈。

记录卡片在大屏主从布局中有选中态,举报原因使用单选图标和 accessibilityText 说明“已选中”。这些状态不能只靠颜色,还要用图标、背景、边框或可访问名称提供第二种线索。

十六、异步结果要防止回写错误对象

MatchResultsPage 保存了 requestedReportId,异步返回后再次比较当前 queryReportId

const requestedReportId: string = this.queryReportId;
const result = await reportService.getMatchBundle(requestedReportId);
if (requestedReportId !== this.queryReportId) return;

这能避免用户快速切换记录后,旧请求结果覆盖新页面。状态设计不仅是 UI 分支,也包括异步生命周期的正确性。

十七、状态组件要复用什么,不复用什么

适合复用:颜色、间距、插画尺寸、重试按钮风格、标题层级、无障碍文案结构。

不适合强行复用:业务解释和下一步动作。首页空态、匹配空态、举报失败的恢复路径不同,不能为了一个通用 EmptyState 把它们都压成“暂无内容”。

共享组件提供骨架,页面提供业务语义。

十八、建议的验收表

状态 验证点
Loading 不显示 Empty;导航和返回可理解;无重复请求
Empty 解释原因;提供正确下一步;筛选空与数据空区分
Error 错误可读;重试可用;旧数据不冒充新结果
Disabled 视觉、事件和文案一致;不能连点
Pressed 有即时反馈,不改变布局尺寸
Selected 不只靠颜色;切换断点后状态仍一致

十九、本章证据边界

本章能证明当前代码已存在上述页面状态分支、互斥顺序、提交门禁和部分异步防回写逻辑。

本章不能证明所有网络超时、照片权限拒绝、暗色模式、系统字体放大和所有机型安全区都完成真机验收。概念配图也不能代替运行截图或自动化日志。

二十、小结

稳定状态体验的核心是:业务状态互斥、页面骨架稳定、错误可恢复、空态有行动、禁用态同时约束视觉和事件。只要把这些规则写进组件边界和 Service 返回契约,页面就不会在真实初始化和失败阶段突然变成另一套产品。

下一篇:《【寻迹校园 HarmonyOS NEXT 实战 43】Agent Framework Kit 还是 Intents Kit:HarmonyOS 小艺能力选型别走错路》。

Logo

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

更多推荐