OpenHarmony 通用状态加载、空页面、错误兜底组件封装(全局业务通用 UI 组件 API23)

摘要

项目列表页面普遍存在 4 种状态:加载中、空数据、网络错误、正常列表。每个页面重复编写状态判断、Loading 动画、空视图、错误重试按钮,冗余代码极多。封装一套全局通用状态兜底组件StateView,统一承载加载、空数据、网络异常、正常内容四种视图,支持自定义文案、重试回调、图片、动画,所有列表页面直接复用,大幅减少页面重复布局代码。API23 优化组件状态刷新渲染、组件销毁资源回收、动画自动释放逻辑,修复状态切换闪烁、多次重复渲染、加载动画后台持续运行耗电等问题。

关键词

OpenHarmony;ArkUI;通用状态组件;空页面;加载视图;错误重试;状态封装;复用组件

一、引言

1.1 页面状态开发痛点

  1. 每个列表页面重复写@State isLoading / isEmpty / isError状态变量;
  2. 加载动画、空白提示、错误重试布局到处复制粘贴,样式不统一;
  3. 网络出错页面无重试按钮,用户需手动返回刷新;
  4. 状态切换时页面布局抖动、闪烁,无平滑过渡;
  5. 页面销毁后加载动画未终止,持续占用 CPU;
  6. 不同页面空页面文字、图标不统一,UI 视觉割裂。

1.2 页面四种标准业务状态

  1. 加载中 Loading:接口请求未返回,展示旋转加载动画 + 提示文字
  2. 空数据 Empty:接口正常返回,列表长度为 0,展示空图标 + 提示文案
  3. 网络 / 业务错误 Error:接口报错、超时、500,展示错误图标 + 重试按钮
  4. 正常 Content:数据正常返回,渲染业务列表 / 卡片

API23 组件渲染核心升级:

  1. @BuilderParam参数缓存优化,状态切换仅局部刷新,不重绘整个页面;
  2. 内置动画生命周期监听,组件销毁自动停止循环旋转动画;
  3. 条件渲染分支优化,避免多状态同时渲染造成布局重叠;
  4. 支持全局默认样式统一配置,单页面可局部覆盖自定义参数。

二、通用状态组件封装 components/common/StateView.ets

ets

复制代码
@Component
export struct StateView {
  // 页面当前状态枚举
  @Param state: PageState = PageState.LOADING
  // 自定义文字
  @Param loadingText: string = "数据加载中..."
  @Param emptyText: string = "暂无数据"
  @Param errorText: string = "加载失败,请点击重试"
  // 重试回调(错误页面点击执行刷新)
  @Param onRetry: () => void = () => {}
  // 正常业务内容插槽
  @BuilderParam contentBuilder: () => void

  // 旋转动画状态
  @State rotateAngle: number = 0

  aboutToAppear() {
    // 加载状态开启无限旋转动画
    animateTo({
      duration: 1200,
      iterations: Infinity,
      curve: Curve.Linear
    }, () => {
      this.rotateAngle += 360
    })
  }

  aboutToDisappear() {
    // 组件销毁重置角度,终止循环动画
    this.rotateAngle = 0
  }

  // 加载视图
  @Builder LoadingView() {
    Column({ space: 16 }) {
      Text("⟳")
        .fontSize(42)
        .rotate({ angle: this.rotateAngle })
        .animation({ duration: 1200, iterations: Infinity, curve: Curve.Linear })
      Text(this.loadingText)
        .fontSize(16)
        .fontColor("#999999")
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }

  // 空数据视图
  @Builder EmptyView() {
    Column({ space: 16 }) {
      Image($r("sys.media.ohos_ic_public_empty"))
        .width(80)
        .height(80)
        .fillColor("#cccccc")
      Text(this.emptyText)
        .fontSize(16)
        .fontColor("#999999")
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }

  // 错误重试视图
  @Builder ErrorView() {
    Column({ space: 20 }) {
      Image($r("sys.media.ohos_ic_public_fail"))
        .width(80)
        .height(80)
        .fillColor("#cccccc")
      Text(this.errorText)
        .fontSize(16)
        .fontColor("#999999")
      Button("点击重试")
        .width(140)
        .height(44)
        .backgroundColor("#007DFF")
        .onClick(this.onRetry)
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }

  build() {
    Column() {
      if (this.state === PageState.LOADING) {
        this.LoadingView()
      } else if (this.state === PageState.EMPTY) {
        this.EmptyView()
      } else if (this.state === PageState.ERROR) {
        this.ErrorView()
      } else {
        // 正常业务列表内容
        this.contentBuilder()
      }
    }
    .width("100%")
    .layoutWeight(1)
  }
}

// 页面状态枚举,全局统一管理
export enum PageState {
  LOADING,
  EMPTY,
  ERROR,
  CONTENT
}

三、列表页面实战完整调用(笔记列表页改造)

ets

复制代码
import StateView, { PageState } from '../components/common/StateView'
import RdbUtil, { Note } from '../utils/rdb_util'
import LogUtil from '../utils/log_util'

@Entry
@Component
struct NoteListPage {
  @State pageState: PageState = PageState.LOADING
  @State noteList: Note[] = []

  async aboutToAppear() {
    RdbUtil.setContext(getContext(this))
    await this.loadData()
  }

  // 统一刷新数据方法,重试按钮复用
  async loadData() {
    this.pageState = PageState.LOADING
    try {
      const list = await RdbUtil.queryNoteList(0, 20)
      this.noteList = list
      if (list.length === 0) {
        this.pageState = PageState.EMPTY
      } else {
        this.pageState = PageState.CONTENT
      }
      LogUtil.info("NoteList", "笔记数据加载完成,条数:", list.length)
    } catch (err)
      LogUtil.error("NoteList", "数据库查询失败", err)
      this.pageState = PageState.ERROR
    }
  }

  build() {
    Column({ space: 12 }) {
      Row() {
        Text("我的笔记").fontSize(22).layoutWeight(1)
        Button("新增笔记").backgroundColor("#007DFF")
      }
      .width("95%")

      // 通用状态容器,传入状态、重试回调、业务列表插槽
      StateView({
        state: this.pageState,
        onRetry: () => this.loadData()
      }) {
        // 正常业务列表内容插槽
        List({ space: 10 }) {
          ForEach(this.noteList, (item: Note) => {
            ListItem() {
              Row() {
                Column().layoutWeight(1) {
                  Text(item.title).fontSize(17)
                  Text(item.content).fontColor("#666")
                }
                Button("删除").backgroundColor("#f56c6c")
              }
              .width("100%")
              .padding(16)
              .backgroundColor(Color.White)
              .borderRadius(10)
            }
          })
        }
        .width("95%")
      }
    }
    .width("100%")
    .height("100%")
    .padding(12)
    .backgroundColor("#F5F5F5")
  }

  async aboutToDisappear() {
    await RdbUtil.closeDB()
  }
}

四、自定义文案局部覆盖示例(资讯页面)

ets

复制代码
StateView({
  state: this.pageState,
  loadingText: "资讯拼命加载中...",
  emptyText: "暂无资讯内容",
  errorText: "网络开小差了,点击重新加载",
  onRetry: () => this.refreshNews()
}) {
  // 资讯列表业务布局
  List() { ... }
}

五、通用状态组件开发编码规范

5.1 状态变量规范

  1. 所有列表页面统一使用@State pageState: PageState,禁止自定义 isLoading、isEmpty 零散变量;
  2. 数据请求开始强制赋值PageState.LOADING
  3. 请求成功判断数组长度,0 条切换 EMPTY,有数据切换 CONTENT;
  4. 请求捕获异常统一切换 ERROR。

5.2 组件复用规范

  1. 所有列表、分页、接口页面统一使用 StateView 包裹业务列表;
  2. 全局默认文字统一写在组件入参默认值,统一产品 UI 风格;
  3. 单页面特殊场景仅局部传参覆盖文字,不重复新建空页面布局。

5.3 动画生命周期规范

  1. 组件内置 aboutToDisappear 重置旋转角度,终止无限加载动画;
  2. 禁止页面内单独写加载动画,统一复用组件内置动画。

5.4 重试逻辑规范

  1. 数据加载逻辑抽离独立loadData方法,页面初始化、重试按钮共用;
  2. 错误页面点击重试自动执行完整刷新流程,无需重复编写请求代码。

5.5 性能渲染规范

  1. 使用@BuilderParam插槽,仅切换状态时局部重渲染,不刷新页面其他控件;
  2. 四种视图互斥渲染,同一时间仅显示一种,避免多层组件重叠占用渲染资源。

六、高频问题与解决方案

问题 1:页面退出后加载旋转动画持续后台运行 解决:组件内部 aboutToDisappear 重置 rotateAngle 终止无限循环动画。

问题 2:接口返回空列表,页面同时显示加载和空视图 解决:请求完成后同步更新 pageState,互斥 if 分支只会渲染单一视图。

问题 3:多个页面空页面图标、文字不统一 解决:统一在 StateView 设置默认参数,全局一套 UI 标准,特殊页面单独覆盖文字。

问题 4:点击重试重复发起多次请求 解决:页面增加请求 loading 锁,请求期间拦截重复点击按钮。

问题 5:状态切换页面明显抖动、布局偏移 解决:四种视图统一设置宽高 100% 居中,固定占位区域,布局尺寸无变化。

七、总结

StateView 通用状态兜底组件统一封装加载、空数据、网络错误三大兜底页面,通过枚举统一管理页面业务状态,利用 Builder 插槽实现业务布局与状态视图解耦,一套组件适配项目所有列表、资讯、笔记、商品页面,消除大量重复布局代码,统一 APP 全局空 / 错误 / 加载 UI 风格。 完全兼容前文 RDB、Http 网络请求、日志工具配套使用,API23 优化组件渲染与动画回收,是企业级鸿蒙项目通用基础业务组件,可直接整合进整套 HAR 分层架构。

相关推荐
绝世番茄8 小时前
HarmonyOS NEXT 实战:SideBar + Navigation 侧边导航布局完全指南
华为·harmonyos·鸿蒙
施棠海8 小时前
设计稿一键变成可运行的Android页面:Pixel2XML全流程UI开发框架实战(附完整源码)
android·ui·架构
geats人山人海8 小时前
c# 第九章 record
开发语言·c#
早期的虫儿有鸟吃8 小时前
vue2--Vuex 模块化
开发语言·前端·javascript
xd1855785558 小时前
家电选购参谋 —— 鸿蒙AI智能助手开发全流程解析
人工智能·华为·harmonyos·鸿蒙
FrameNotWork8 小时前
HarmonyOS 6.0 相机开发——拍照与录像
数码相机·华为·harmonyos
浪客川8 小时前
Android的SystemUI的启动流程简析
android·开发语言
-银雾鸢尾-9 小时前
C#中的抽象类与抽象方法
开发语言·c#
FrameNotWork9 小时前
HarmonyOS 6.0 自定义TabBar——从基础到动效
华为·harmonyos
世人万千丶9 小时前
鸿蒙Flutter Flex布局性能优化
学习·flutter·性能优化·harmonyos·鸿蒙·鸿蒙系统