引言

在移动应用开发中,界面状态管理是最基础也最容易忽视的问题之一。一个网络请求可能产生四种结果:正在加载中、加载成功返回数据、加载成功但数据为空、加载失败。如果每种状态都缺乏妥善的 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 的核心属性:

属性 类型 说明 默认值
width Length 组件宽度 系统默认
height Length 组件高度 系统默认
color ResourceColor 旋转线条颜色 系统主题色
enableLoading boolean 是否启用加载动画 true

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

LoadingProgress vs Progress

很多开发者容易混淆 LoadingProgressProgress

特性 LoadingProgress Progress
用途 表示"正在进行中"的等待 表示"已完成比例"的进度
形态 不停旋转的圆环 可填充的条形/环形
是否确定性 不确定(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 错误状态

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

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

状态机设计

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

         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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐