引言

在移动应用开发中,界面状态管理是最基础也最容易忽视的问题之一。一个网络请求可能产生四种结果:正在加载中、加载成功返回数据、加载成功但数据为空、加载失败。如果每种状态都缺乏妥善的 UI 呈现,用户就会面对一片空白、永久转圈、或者莫名其妙的死屏。

HarmonyOS NEXT 提供了 LoadingProgress 组件作为加载状态的视觉指示器,但要构建一个完整的用户体验,仅仅有一个旋转动画远远不够。本文将通过一个"消息中心"实战 Demo,深入讲解四种加载状态(Loading / Success / Empty / Error)的 UI 设计模式和状态切换管理,帮助你构建让用户安心、清晰、可控的数据加载体验。

读完本文你将能够:

  • 使用 LoadingProgress 组件构建专业的加载动画
  • 掌握骨架屏(Skeleton Screen)占位技术及其实现
  • 理解空状态和错误状态的设计原则
  • 学会用状态机管理页面状态切换
  • 获得一套可复用的四态加载管理模式

LoadingProgress:ArkUI 的加载指示器

基本用法

LoadingProgress 是 ArkUI 提供的旋转式加载指示器组件,用于告知用户当前正在进行后台操作:

LoadingProgress()
  .width(40)
  .height(40)
  .color('#1677FF')

LoadingProgress 的核心属性:

属性类型说明默认值
widthLength组件宽度系统默认
heightLength组件高度系统默认
colorResourceColor旋转线条颜色系统主题色
enableLoadingboolean是否启用加载动画true

它是 ArkUI 中最"安静"的组件之一——不需要复杂配置,不需要状态管理,放上去就开始旋转,拿下来就停止。这种朴素的 API 设计让开发者可以将精力集中在"什么时候显示它"和"什么时候替换它"这两个核心问题上。

LoadingProgress vs Progress

很多开发者容易混淆 LoadingProgress 和 Progress:

特性LoadingProgressProgress
用途表示"正在进行中"的等待表示"已完成比例"的进度
形态不停旋转的圆环可填充的条形/环形
是否确定性不确定(indeterminate)确定(determinate,有 value)
典型场景网络请求中、数据加载中文件下载、视频缓冲、任务完成度

简单说:如果你不知道还要多久,用 LoadingProgress;如果你知道完成了多少,用 Progress。

四态加载管理模式

一个完整的数据加载 UI 需要处理四种状态:

状态一:Loading(加载中)

这是用户发起请求后看到的第一个状态。UI 需要传达两个信息:“系统正在工作” 和 “请稍等”。

LoadingProgress + 骨架屏是最佳实践:

// Loading 态的设计模式
Column() {
  LoadingProgress()
    .width(40).height(40)
    .color('#1677FF')
    .margin({ bottom: 14 })

  Text('正在加载消息...')
    .fontSize(13).fontColor('#BBBBCC')
    .margin({ bottom: 24 })

  // 骨架占位卡片:让用户预览内容布局
  ForEach([0, 1, 2], (item: number) => {
    Row() {
      Row()  // 圆形头像占位
        .width(44).height(44)
        .borderRadius(22)
        .backgroundColor('#E8E8EE')
      Column() {
        Row()  // 标题行占位
          .width('60%').height(13)
          .borderRadius(4)
          .backgroundColor('#E8E8EE')
        Row()  // 摘要行占位
          .width('85%').height(11)
          .borderRadius(4)
          .backgroundColor('#F0F0F5')
      }
      .layoutWeight(1).margin({ left: 12 })
    }
    .padding(14).borderRadius(10)
    .backgroundColor('#FFFFFF')
  })
}

骨架屏的核心价值在于:用户在看到 LoadingProgress 的同时,也能预览内容的大致结构。这降低了"等待的不确定性",让用户知道页面即将出现的内容是什么样的——三个圆角头像卡片、每张卡片有标题行和摘要行。相比于一片空白加一个转圈,骨架屏让等待体验从"被动忍受"升级为"主动期待"。

状态二:Success(加载成功)

这是最常规的状态——数据已返回,渲染完整的消息列表:

ForEach(this.messages, (msg: MessageItem, idx: number) => {
  Row() {
    // 头像
    Row() {
      Text(msg.avatar)
        .fontSize(18).fontColor('#FFFFFF')
    }
    .width(44).height(44).borderRadius(22)
    .backgroundColor(msg.unread ? '#1677FF' : '#CCCCDD')
    .justifyContent(FlexAlign.Center)

    // 标题 + 摘要
    Column() {
      Row() {
        Text(msg.title)
          .fontSize(15)
          .fontWeight(msg.unread ? FontWeight.Bold : FontWeight.Normal)
          .layoutWeight(1)
        Text(msg.time).fontSize(11).fontColor('#CCCCDD')
      }
      Text(msg.subtitle)
        .fontSize(12).fontColor('#999999')
        .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
    }
    .layoutWeight(1).margin({ left: 12 })

    // 未读标记
    if (msg.unread) {
      Row().width(8).height(8)
        .borderRadius(4)
        .backgroundColor('#FF4D4F')
    }
  }
  .padding(14).borderRadius(10).backgroundColor('#FFFFFF')
}, (msg: MessageItem) => msg.id.toString())

Success 态的设计要点:

  • 信息层次清晰:标题(bold)+ 时间 + 摘要,从上到下优先级递减
  • 未读状态可区分:蓝色头像 vs 灰色头像、粗体 vs 常规字重、红点标记
  • 与骨架屏布局一致:Success 态的卡片结构与 Loading 态的骨架占位一一对应,视觉上的平滑过渡避免了"跳变感"

状态三:Empty(无数据)

空状态是最容易被忽视的状态。很多应用的"暂无数据"只是一行灰色小字,用户看了会怀疑是 Bug 还是真的没数据。

良好设计的空状态包含三个要素:

  1. 视觉插图:一个醒目的大图标或插画(本文使用 emoji 作为示意)
  2. 明确说明:用一句话告诉用户当前的状态
  3. 行动引导:提供一个按钮引导用户采取下一步行动
Column() {
  Text('📋').fontSize(64)          // 视觉焦点
  Text('暂无消息')                   // 状态说明
    .fontSize(17).fontWeight(FontWeight.Bold)
  Text('还没有收到任何消息,点击下方按钮加载示例数据')  // 引导文案
    .fontSize(12).fontColor('#BBBBCC')
  Row() {
    Text('加载示例数据')              // 行动按钮
      .fontColor('#FFFFFF')
      .padding({ top: 10, bottom: 10, left: 28, right: 28 })
      .borderRadius(20).backgroundColor('#1677FF')
      .onClick(() => { this.loadSampleData(); })
  }
}
.justifyContent(FlexAlign.Center)

关键原则:不把用户困在空状态。每个空状态页面都应该有一个"出口"——一个引导用户离开空状态的按钮或链接。

状态四:Error(加载失败)

错误状态需要传达三层信息:“出了什么问题”、“用户该怎么做”、“可以重试”:

Column() {
  Text('⚠️').fontSize(64)           // 错误图标
  Text('加载失败')                    // 状态说明
    .fontSize(17).fontWeight(FontWeight.Bold)
  Text('网络异常,请检查网络连接后重试')  // 原因 + 建议
    .fontSize(12).fontColor('#BBBBCC')
  Row() {
    Text('重新加载')                  // 重试按钮
      .fontColor('#FFFFFF')
      .padding({ top: 10, bottom: 10, left: 28, right: 28 })
      .borderRadius(20).backgroundColor('#1677FF')
      .onClick(() => { this.loadSampleData(); })
  }
}

Error 态设计要点:

  • 不要用技术术语:用户不关心"HTTP 500"或"JSON 解析失败",只需要知道"加载失败"和"可以重试"
  • 重试按钮要突出:这是 Error 态最重要的交互元素,用品牌色/主色调强调
  • 可以提供降级方案:对于关键功能,除了重试还可以提供"查看缓存数据"等备选

空状态 vs 错误状态

很多开发者将空状态和错误状态混为一谈,这对用户是不友好的。两者的本质区别:

维度EmptyError
原因确实没有数据无法获取数据
用户预期“哦,还没有消息”“嗯?出问题了”
解决方案引导创建/添加重试获取
按钮文案“加载示例/创建”“重试/重新加载”
视觉色调温和(中性色)警示(暖色)

状态机设计

四态之间的转换不是随意的,它们构成一个有限状态机:

         aboutToAppear
              ↓
          LOADING ──模拟网络延迟──→ SUCCESS
             │                        │
             │                 "清空消息"
             │                        ↓
             │                      EMPTY
             │                        │
             │                "加载示例数据"
          "模拟错误"                  │
             │                       │
             ↓                       ↓
          LOADING ←──────────────────┘
             │
          模拟延迟
             ↓
           ERROR ──"重试"──→ LOADING → SUCCESS

在代码中,这个状态机通过 loadState 这个单一的 @State 变量来驱动:

@State loadState: number = 0; // 0=Loading, 1=Success, 2=Empty, 3=Error

每次状态切换,只需修改这一个值,UI 就会自动切换到对应的渲染分支。

状态转换的核心方法:

// 加载数据:任何状态 → Loading → Success
loadSampleData(): void {
  this.loadState = 0;        // 先进入 Loading
  this.messages = [];        // 清空旧数据
  setTimeout(() => {
    this.messages = this.getSampleMessages();  // 模拟数据
    this.loadState = 1;      // 切换到 Success
  }, 1500);
}

// 清空数据:Success → Empty(无需 Loading 过渡)
clearMessages(): void {
  this.messages = [];
  this.loadState = 2;        // 直接进入 Empty
}

// 模拟错误:任何状态 → Loading → Error
simulateError(): void {
  this.loadState = 0;        // 先进入 Loading
  this.messages = [];
  setTimeout(() => {
    this.messages = [];
    this.loadState = 3;      // 切换到 Error
  }, 1000);
}

关键设计原则:

  1. 网络操作前必须先进入 Loading:让用户知道系统在做什么
  2. 纯本地操作无需 Loading:如"清空消息"直接到 Empty
  3. 状态数组和数据数组同步管理:切到 Loading/Empty/Error 时清空 messages,切到 Success 时填充
    在这里插入图片描述
    在这里插入图片描述

状态徽章:让当前状态对外可见

在 Demo 的标题栏中,我们放置了一个状态徽章,实时显示当前处于哪种状态:

Row() {
  Circle({ width: 7, height: 7 })
    .fill(this.getStateColor())   // 状态对应颜色
    .margin({ right: 5 })
  Text(this.getStateLabel())       // "加载中"/"已加载"/"无数据"/"加载失败"
    .fontSize(11)
    .fontColor('#FFFFFFCC')
}
.padding({ top: 5, bottom: 5, left: 10, right: 10 })
.borderRadius(10)
.backgroundColor('#FFFFFF20')

颜色映射表:

getStateColor(): string {
  if (this.loadState === 0) return '#1677FF'; // 蓝色 - 加载中
  if (this.loadState === 1) return '#52C41A'; // 绿色 - 成功
  if (this.loadState === 2) return '#FAAD14'; // 橙色 - 空
  return '#FF4D4F';                            // 红色 - 错误
}

状态徽章看似是一个小细节,但它有两个重要作用:

  1. 让 Demo 意图清晰:在演示四态切换时,开发者和用户都能明确看到当前状态
  2. 为生产应用提供设计参考:在产品中加入状态指示器(如"网络请求中"小圆点),有助于用户理解应用的当前行为

底部控制栏:状态切换的操作面板

Demo 的底部控制栏提供了两个操作按钮,用于触发状态切换:

主操作按钮:根据当前状态动态切换文案和功能:

getLoadButtonLabel(): string {
  if (this.loadState === 1) return '清空消息';      // Success → Empty
  if (this.loadState === 3) return '重试';          // Error → Loading → Success
  return '加载示例数据';                             // Empty → Loading → Success
}

模拟错误按钮:仅在 Success 状态下显示:

if (this.loadState === 1) {
  Row() {
    Text('模拟错误')
      .fontColor('#FF4D4F')
      .border({ width: 1, color: '#FF4D4F33' })
      .onClick(() => { this.simulateError(); })
  }
}

这种按钮的动态显隐和文案切换,让控制栏始终保持简洁——用户只看到当前状态下有意义的操作。

骨架屏实现技术

骨架屏(Skeleton Screen)是一种在加载过程中展示内容占位的技术。它用灰色块模拟即将出现的内容布局,让页面在加载时看起来"已经有内容了"。

实现骨架屏不需要特殊组件,使用普通的布局组件 + 背景色即可:

// 头像骨架:圆角矩形模拟头像
Row()
  .width(44).height(44)
  .borderRadius(22)
  .backgroundColor('#E8E8EE')

// 标题骨架:短横条模拟标题文字
Row()
  .width('60%').height(13)
  .borderRadius(4)
  .backgroundColor('#E8E8EE')

// 摘要骨架:长横条模拟摘要文字
Row()
  .width('85%').height(11)
  .borderRadius(4)
  .backgroundColor('#F0F0F5')

三点技巧:

  1. 宽度用百分比:模拟标题用 60% 宽度(标题通常不是满宽的),模拟摘要用 85%(更长的文本行),更接近真实内容
  2. 颜色分层:标题骨架稍深(#E8E8EE),摘要骨架稍浅(#F0F0F5),模仿真实文本的视觉权重差异
  3. 与 Success 态布局完全对齐:骨架中头像 44×44、内边距 14、圆角 10,与 Success 态的消息卡片完全一致

完整代码中的关键技巧

1. 使用 setTimeout 模拟异步操作

在真实应用中,Loading 状态是被网络请求驱动的。Demo 中使用 setTimeout 模拟:

setTimeout(() => {
  this.messages = this.getSampleMessages();
  this.loadState = 1;
}, 1500);

1500ms 的延迟量足够让用户感知到 Loading 状态,但又不至于等待太久。在生产代码中,这个位置应替换为 http.createHttp().request() 等实际网络请求。

2. 状态与数据的一致性

Loading / Empty / Error 状态下,messages 数组应为空;Success 状态下,messages 包含实际数据。每次状态切换都要同步更新两者:

// 永远以"先设状态,再等结果"的模式操作
this.loadState = 0;      // 1. 先显示 Loading
this.messages = [];      // 2. 清空旧数据
// ... 网络请求 ...
this.messages = result;  // 3. 填充新数据
this.loadState = 1;      // 4. 显示 Success

3. 按钮状态驱动

底部控制栏的主按钮根据当前状态改变行为和样式:

handleMainAction(): void {
  if (this.loadState === 1) {
    this.clearMessages();    // Success → Empty
  } else {
    this.loadSampleData();   // Empty/Error → Loading → Success
  }
}

这是一种"语境响应式"的交互模式——同一个按钮,不同状态下执行不同操作。它减少了按钮数量,保持了界面简洁。

总结

本文通过一个"消息中心"Demo,系统讲解了四种加载状态的 UI 设计和管理方法。我们从 LoadingProgress 组件的基本用法出发,逐步构建了骨架屏占位、空状态引导、错误状态重试等完整的用户体验闭环。

四态管理模式的核心可概括为以下几点:

  1. Loading 不等于只放一个转圈:加上骨架屏,让用户预览内容结构
  2. Empty 要有出口:每个空状态页面都要有引导用户"离开空状态"的按钮
  3. Error 要讲人话:用用户能理解的语言描述问题和解决方案,不要暴露技术细节
  4. 状态切换要走状态机:网络操作必须有 Loading 过渡;本地操作可以直达目标状态
  5. 数据与状态同生命周期:切出 Success 态时清空数据,切回 Success 时重新填充

在真实的商业应用中,你还可以在此基础上扩展到 5 态(增加"网络离线"状态)或 6 态(增加"需要登录/授权"状态)。核心的设计哲学是不变的:让用户在任何状态下都感到安心和有控制感。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐