HarmonyOS 7 状态手记 01|页面状态别乱放

做 HarmonyOS 7 页面时,最容易把人绕进去的往往不是布局,而是"这个值到底该放哪儿"。页面计数、加载状态与条件渲染 看起来只是几行代码,真接进项目后,经常会遇到 UI 不刷新、返回页面数据变旧、弹窗取消后值却被改掉,或者一个请求失败把整个页面状态搅成一团。

这篇不背概念。我就按项目里最常见的写法,把 @State、普通变量与 UI 刷新的边界 拆开讲。目标很简单:代码能跑,状态变化能解释,出了问题知道先查哪儿。

本文环境按 HarmonyOS 7 + ArkTS + DevEco Studio 的工程习惯组织。不同 API 版本如果有细节差异,以你当前 SDK 的类型提示为准。

1. 先说问题:为什么这个状态值得单独管

很多页面刚开始都很简单。一个变量,一个按钮,一个接口。于是我们很自然地把值全塞进组件里。功能一多,问题就来了。

比如 页面计数、加载状态与条件渲染。用户点一次,你改一个变量;接口回来,再改一个变量;页面切走回来,又要恢复一次。只要其中某一步没有明确"谁负责修改、谁负责展示",状态就开始互相污染。

我判断一个值要不要进入 UI 状态,通常先问三个问题:它变了以后界面要不要立刻跟着变?它是不是只属于当前组件?页面销毁后还需不需要保留?这三个问题基本能过滤掉大多数乱用状态装饰器的情况。

这里有个很实用的习惯:业务数据和界面过程状态分开命名。 数据是 data、list、detail;过程状态是 loading、submitting、dialogVisible、error。别把一个布尔值既当"有没有数据"又当"是不是请求完成",后面一定难维护。

2. 先写一个最小版本

下面这段我故意写得短一点,先把核心跑通:

ts 复制代码
@Entry
@Component
struct StateDemo {
  @State count: number = 0
  @State loading: boolean = false

  build() {
    Column({ space: 16 }) {
      Text(`当前数量:${this.count}`)
      Button('加 1').onClick(() => { this.count++ })
      if (this.loading) { LoadingProgress() }
    }.padding(20)
  }
}

这段代码最重要的不是语法,而是状态变化路径。用户操作发生后,只修改真正需要变化的状态;UI 根据状态重新计算展示结果。这样调试时你可以沿着"事件 → 状态 → UI"往下找,而不是在十几个回调里猜。

实际项目里建议先把最小版本跑通,再加接口、弹窗和缓存。很多人一上来就把所有逻辑堆进去,最后报错时根本分不清是状态问题还是网络问题。

3. 真正容易踩坑的是状态边界

第一类坑是"能不用状态的值也做成状态"。例如一个只在点击事件内部使用的临时变量,它不参与 UI 渲染,就没必要为了保险全部声明成响应式状态。状态越多,阅读成本越高,也更容易出现无意义刷新。

第二类坑是"同一个事实存两份"。比如列表长度本来可以从 list.length 得到,又额外维护一个 count。新增数据时改了 list,忘了改 count,页面马上出现两个答案。能计算出来的值尽量计算,不要重复存。

第三类坑是异步回来以后不管页面当前处境直接赋值。请求发出去时用户可能已经切换筛选条件,旧请求后返回,就可能覆盖新结果。真实业务里要么给请求带查询条件并在返回时校验,要么统一做请求取消/序列控制。

ts 复制代码
private requestSeq: number = 0
async refresh() {
  const seq = ++this.requestSeq
  this.loading = true
  try {
    const result = await loadData()
    if (seq !== this.requestSeq) return
    this.data = result
  } finally {
    if (seq === this.requestSeq) this.loading = false
  }
}

这个小技巧很朴素,但对搜索、筛选、分页这种连续请求特别有用。旧请求即使晚回来,也没有资格覆盖最新状态。

4. 把页面状态画出来,代码会清楚很多

如果一个页面已经出现五六个布尔值,我建议先别继续写。拿纸画一下状态。比如请求页面通常就四种:初始、加载中、成功、失败。弹窗则是关闭、编辑中、确认完成。

布尔值最大的问题是可以组合出很多"不应该存在"的情况:loading=true 同时 error=true 到底展示谁?所以复杂页面可以直接用联合类型或枚举表达互斥状态。

ts 复制代码
type ViewState = 'loading' | 'content' | 'empty' | 'error'
@State viewState: ViewState = 'loading'

渲染时只认这一份状态,页面就不会同时冒出 Loading 和错误提示。这个思路比不停加条件判断好维护得多。

5. 我在项目里会怎么拆

我一般把页面分成三层。第一层是页面容器,负责请求、路由和页面级状态;第二层是业务组件,只接收自己需要的数据;第三层是纯展示组件,尽量不碰网络和持久化。

这么拆的好处不是为了"架构漂亮",而是改需求时少牵连。比如产品只改空状态样式,你不应该碰请求代码;接口字段调整,也不应该顺手改弹窗显示逻辑。

ts 复制代码
@Component
struct ContentPanel {
  @Prop title: string
  @Prop disabled: boolean = false
  build() {
    Row() {
      Text(this.title)
      Blank()
      Text(this.disabled ? '不可操作' : '可操作')
    }.width('100%').padding(16)
  }
}

子组件只拿它需要的东西。不要图省事把整个巨大对象传进去,再让组件内部到处读字段。字段依赖越隐蔽,后面越难判断哪个变化会引起哪个组件更新。

6. 调试时别只盯着 UI

状态类问题有个特点:界面表现是结果,真正的原因往往发生在前面。所以我会在关键状态入口打日志,而不是每一行都打。建议至少记录操作名、请求序号、旧值和新值。

ts 复制代码
function stateLog(name: string, before: object, after: object) {
  console.info(`[state] ${name} before=${JSON.stringify(before)} after=${JSON.stringify(after)}`)
}

如果第二次点击没有反应,就先确认点击事件有没有进;进了以后状态条件是不是把逻辑提前 return;再看异步请求有没有真正发出。这个顺序比一上来清缓存、重装模拟器靠谱得多。

7. 再补一个完整一点的处理方式

真实页面里,我更喜欢把一次操作包成一个完整方法:入口校验、切换过程状态、执行业务、处理异常、最后恢复。这样按钮事件本身很薄。

ts 复制代码
async onQueryClick() {
  if (this.loading) return
  this.loading = true
  this.errorText = ''
  try {
    const result = await this.queryService()
    this.applyResult(result)
  } catch (err) {
    this.errorText = '操作失败,请稍后重试'
  } finally {
    this.loading = false
  }
}

finally 很关键。成功、失败、提前抛异常,最终都要把 loading 收回来。项目里那种"第一次能点,第二次永远没反应"的问题,经常就是某条异常路径没有恢复状态。

另外,不建议为了让 UI 立即变化到处加延时。setTimeout 能暂时把问题藏起来,但它没有解决状态归属和执行顺序。除非业务真的需要延时,否则先查数据流。

8. 再往真实项目推进一步

页面状态不是孤立的。真正的项目还会碰到返回页面是否重新请求、多个组件是否共享同一份筛选条件、缓存和网络谁先展示、快速重复点击如何防抖等问题。处理这些问题时,不要先问"该用哪个装饰器",先问数据生命周期:谁创建、谁修改、谁消费、什么时候失效。

如果一份数据只服务一个局部组件,就尽量留在局部;如果父组件需要控制子组件,就让依赖方向清晰;如果需要跨页面长期保存,再考虑持久化或更高层的数据模型。把生命周期想明白以后,API 反而是最简单的一步。

9. 这一篇记住什么

回头看 @State、普通变量与 UI 刷新的边界,核心其实就几句话:需要驱动 UI 的值才进入响应式状态;同一个事实尽量只有一个数据源;异步结果要防止旧请求覆盖新状态;复杂页面用明确的状态枚举替代一堆互相打架的布尔值;组件只接收自己真正需要的数据。

你可以拿手上的一个 HarmonyOS 7 页面做个小练习:把所有 @State 列出来,逐个问"它变化后真的需要刷新 UI 吗""这个值能不能从别的状态算出来""页面离开以后还要不要它"。通常扫一遍,就能删掉一批没必要的状态。

下一篇继续往真实项目里走,不讲虚的,直接处理下一层状态协作问题。

相关推荐
李游Leo2 小时前
HarmonyOS 7 + Hvigor-Localization Kit:多语言资源占位符签名与回退链路预检【鸿蒙心迹】
harmonyos
李游Leo2 小时前
HarmonyOS 7 AVSession + Float Window:闪控窗播放控制权的会话移交与重复指令去重【鸿蒙心迹】
华为·harmonyos
李游Leo2 小时前
HarmonyOS 7 + ArkTS-UDMF:精准碰一碰载荷的协议版本协商与字段降级【鸿蒙心迹】
华为·harmonyos
李游Leo3 小时前
HarmonyOS 7 + Core Vision Kit-TaskPool:文搜图相似度阈值分桶与硬负样本回流【鸿蒙心迹】
华为·harmonyos
李游Leo4 小时前
HarmonyOS 7 EasyGo + ArkUI Navigation:平行视界导航模式的列表选中态、右栏路由栈与全局返回收口【鸿蒙心迹】
华为·harmonyos
李游Leo4 小时前
HarmonyOS 7 + ArkUI Accessibility:闪控窗紧凑态的语义树裁剪与读屏焦点收敛【鸿蒙心迹】
华为·harmonyos
李游Leo5 小时前
HarmonyOS 7 ArkUI Navigation + WindowStage:折叠屏窗口宽度驱动的单双栏切换与草稿状态保持【鸿蒙心迹】
华为·harmonyos
HwJack205 小时前
【共创稿事节】HarmonyOS 7材质与光照:真实感的营造与性能的平衡
pytorch·harmonyos·材质
翼辉cto5 小时前
otlinx-datetime 官方 KMP 日期时间库的 OpenHarmony 鸿蒙化适配实战
华为·harmonyos