首页 / 资讯中心 / 文章详情

HarmonyOS 7 状态管理 |页面数据为什么总是不同步

HarmonyOS 7 状态管理 |页面数据为什么总是不同步 ★ FEATURED ARTICLE
很多人第一次写 HarmonyOS 页面最容易产生一个错觉变量改了页面就应该跟着变。听起来没毛病。毕竟我们写的是声明式 UI代码里把count从 0 改成 1界面上的数字不就应该自动变成 1 吗实际开发没这么省心。尤其是项目稍微复杂一点之后你会遇到一些很“玄学”的现象接口数据明明回来了日志也打印出来了页面就是没刷新弹窗里修改了数据关闭弹窗之后列表还是旧值父组件传给子组件的数据变了子组件却像没看见一样甚至同一个变量第一次点按钮有效第二次点就没反应。这类问题看着散其实经常指向同一件事你还没有把“普通变量”和“状态”真正区分开。这一篇不急着堆一大堆装饰器。先把最基础、也是后面所有状态管理问题的地基打牢HarmonyOS 7 页面为什么会刷新什么数据值得交给状态系统以及状态到底应该放在哪里。一、先看一个最常见的坑假设我们做一个很简单的查询页面。页面上有一个数字下面一个按钮。点击按钮后数字加 1。很多刚接触 ArkUI 的开发者会顺手这么写EntryComponentstruct CounterPage{privatecount:number0build(){Column({space:16}){Text(当前次数${this.count}).fontSize(22)Button(查询一次).onClick((){this.countconsole.info(count ${this.count})})}.width(100%).height(100%).justifyContent(FlexAlign.Center)}}逻辑看起来非常直白。点击按钮以后控制台里的count会增加。但是我们真正关心的是UI 是否知道这个值发生了变化普通成员变量的职责只是保存数据。它本身并没有明确告诉 UI 框架“这个字段变化以后你需要重新更新依赖它的界面。”所以这里需要建立第一个意识数据发生变化不等于 UI 一定会感知到变化。在声明式 UI 中页面更新并不是我们手动找到Text然后调用一个setText()。更常见的思路是让 UI 依赖某个可观察状态当状态变化时由框架完成对应界面的更新。二、把 count 真正变成页面状态对于页面自己的简单状态我们先用最容易理解的方式处理。EntryComponentstruct CounterPage{Statecount:number0build(){Column({space:16}){Text(当前次数${this.count}).fontSize(22).fontWeight(FontWeight.Medium)Button(查询一次).onClick((){this.count})}.width(100%).height(100%).justifyContent(FlexAlign.Center)}}真正值得注意的只有这一行Statecount:number0不要把State理解成“让变量变高级”的语法糖。更实用的理解是这个字段参与当前组件的 UI 状态管理。Text使用了this.count。当count发生变化以后框架能够感知到这个状态变化并更新依赖它的界面。这时候代码关系就清楚多了用户点击按钮 ↓ 修改 count ↓ 状态变化被感知 ↓ 依赖 count 的 UI 更新这里有个很重要的思维转换。以前写命令式 UI我们容易想点击按钮 → 找到 Text → 修改 Text而现在应该逐渐改成点击按钮 → 修改状态 → UI 根据状态重新呈现这两个思路看起来只差一步后面的工程复杂度却完全不一样。三、别看到变量就加 State理解了上面的例子之后另一个极端马上就来了。有些项目会变成这样StateuserName:stringStaterequestUrl:stringStatepageSize:number20StatecurrentIndex:number0Stateloading:booleanfalseStatelogText:stringStatetempId:string只要是变量全加状态装饰。这样写短期很爽因为“反正能刷新”。但项目大了之后状态会越来越难追。判断一个变量要不要成为 UI 状态可以先问自己一句它变化以后当前页面展示需要跟着变化吗比如privaterequestUrl:string/api/query如果这个字段只是请求接口时内部使用页面根本不展示它那通常没有必要为了 UI 更新把它做成页面状态。再比如Stateloading:booleanfalse如果页面会根据loading显示加载中的进度条那它就是非常典型的 UI 状态。if(this.loading){Progress().width(36).height(36)}所以别机械记语法。可以先按用途粗暴地分成两类业务过程内部使用的数据 → 普通字段也许就够了 会直接影响当前 UI 展示的数据 → 应重点考虑状态管理这条判断在实际项目里非常有用。四、真实业务里最常见的其实是 loading计数器只是为了说明机制。真正项目里你更容易碰到的是请求状态。比如一个用户信息查询页面EntryComponentstruct UserPage{Stateloading:booleanfalseStateuserName:string--asyncqueryUser(){this.loadingtruetry{constresultawaitthis.mockRequest()this.userNameresult}catch(error){console.error(query user failed:${error})}finally{this.loadingfalse}}mockRequest():Promisestring{returnnewPromise((resolve){setTimeout((){resolve(Harmony Developer)},1200)})}build(){Column({space:20}){Text(用户${this.userName}).fontSize(20)if(this.loading){Progress().width(40).height(40)}Button(this.loading?查询中...:开始查询).enabled(!this.loading).onClick((){this.queryUser()})}.width(100%).height(100%).justifyContent(FlexAlign.Center)}}这个例子已经很接近真实开发了。点击按钮之后loading变成true页面出现进度组件按钮暂时不可点击异步请求完成userName更新finally把loading恢复成false页面隐藏加载状态。这里最值得学的不是Progress()怎么写而是状态设计。页面并不需要知道“进度条什么时候调用 show”。页面只需要知道this.loadingtrue那就展示加载效果。当它变成false加载效果自然消失。这就是声明式 UI 写起来舒服的地方。五、为什么我更喜欢在 finally 里恢复状态很多项目会这么写this.loadingtrueconstresultawaitrequest()this.loadingfalse接口成功的时候没问题。但是接口一旦抛异常最后一行可能根本执行不到。于是页面就会一直显示“查询中”。用户再点按钮也没反应因为按钮还被loading禁用了。这种问题特别烦因为正常网络下你可能测半天都复现不了。更稳妥的写法是this.loadingtruetry{constresultawaitrequest()// 处理成功数据}catch(error){// 处理异常}finally{this.loadingfalse}finally很适合处理这种“无论成功失败都应该恢复”的页面状态。除了 loading还包括一些类似状态Statesubmitting:booleanfalseStaterefreshing:booleanfalseStateexporting:booleanfalse它们本质上都属于过程状态。这里再提醒一句不要为了防重复点击到处手写时间戳判断。如果按钮在请求期间本来就不应该再次点击直接让状态控制按钮是否可用通常更直观。Button(提交).enabled(!this.submitting)读代码的人一眼就知道是什么意思。六、一个页面不要塞十几个“真假开关”状态用起来以后还有一个很容易出现的问题布尔变量泛滥。比如Stateloading:booleanfalseStatesuccess:booleanfalseStatefailed:booleanfalseStateempty:booleanfalse乍一看挺清晰。问题是这四个状态可能互相打架。有没有可能出现loading true success true当然可能只要某个分支忘记重置。甚至还可能出现success true failed true页面到底听谁的如果这些状态本来就是互斥的我更建议把它们合成一个明确的页面状态。enumPageStatus{Idle,Loading,Success,Empty,Error}然后页面只维护一个字段StatepageStatus:PageStatusPageStatus.Idle请求开始this.pageStatusPageStatus.Loading拿到数据if(data.length0){this.pageStatusPageStatus.Empty}else{this.pageStatusPageStatus.Success}请求失败this.pageStatusPageStatus.Error这样状态关系就从“四个开关随便组合”变成了“一次只能处于一种明确状态”。七、把页面状态真正落到 UI 上有了PageStatus页面代码也会变得更容易读。EntryComponentstruct ResultPage{StatepageStatus:PageStatusPageStatus.IdleStatedataList:Arraystring[]build(){Column(){if(this.pageStatusPageStatus.Idle){Text(点击下方按钮开始查询)}elseif(this.pageStatusPageStatus.Loading){Progress()}elseif(this.pageStatusPageStatus.Empty){Text(暂无数据)}elseif(this.pageStatusPageStatus.Error){Text(加载失败请稍后重试)}else{List(){ForEach(this.dataList,(item:string){ListItem(){Text(item).fontSize(16).padding(12)}})}}}.width(100%).height(100%)}}这段代码的价值不是“写得高级”恰恰相反是它够直白。打开页面代码不用去猜loading 为 true 的时候 empty 会不会也是 true因为这种组合已经不存在了。对于中小型页面这种状态收口方式非常实用。当然如果你的页面只有一个按钮和一个 Text就没必要为了“架构感”硬上枚举。工程化不是代码越多越专业而是复杂度出现以后再用合适的结构把它压回去。八、状态应该离使用它的地方近一点再说一个我在项目里经常看到的问题。明明只是一个弹窗自己的展开状态却被放到了页面最外层StatedialogExpand:booleanfalse甚至再往后所有页面状态都塞进一个巨大的对象里。最后改一个弹窗整个页面都要跟着理解一遍。一个比较实用的原则是谁使用状态就尽量让状态靠近谁。比如某个状态只属于一个独立组件那就优先考虑让这个组件自己维护。Componentstruct FilterPanel{Stateexpanded:booleanfalsebuild(){Column(){Row(){Text(筛选条件)Blank()Text(this.expanded?收起:展开)}.onClick((){this.expanded!this.expanded})if(this.expanded){Text(这里放筛选项).margin({top:12})}}.padding(16)}}这个expanded对外部页面没有意义。父页面没必要知道用户现在有没有展开筛选区。如果以后业务真的要求父页面控制它再重新设计父子状态关系也不迟。别一上来就把所有东西全局化。全局状态看起来“哪里都能拿”后期排查问题的时候也会变成“哪里都可能改”。九、今天先别急着研究所有装饰器学 HarmonyOS 状态管理时很容易掉进一个坑打开文档看到一排状态相关能力然后开始背。背完之后还是不会设计页面。我更建议反过来。先拿一个真实页面问三个问题第一这个数据变化以后会不会影响 UI不会那它未必需要成为 UI 状态。第二这个状态是谁在用只有当前组件用就先留在当前组件。不要没事就往外提。第三几个状态是不是互斥如果 loading、empty、success、error 本质上描述的是同一件事的不同阶段就考虑把它们收成一个状态而不是维护一排 boolean。把这三件事搞清楚很多页面已经不会写得太乱了。十、给这一篇留一个小练习可以自己建一个很简单的 HarmonyOS 7 页面不需要接真实接口。做一个“订单查询”页面要求只有下面几种状态Idle Loading Success Empty Error点击查询以后用setTimeout模拟网络请求。第一次返回三条订单。第二次返回空数组。第三次主动模拟异常。不要创建isLoading、isEmpty、isError三个布尔值只允许维护一个pageStatus。你会发现一旦状态设计清楚UI 代码其实很好写。这也是这一篇真正想说明的事情状态管理首先不是装饰器问题而是数据和界面关系怎么设计的问题。本篇小结普通变量和 UI 状态不是一回事真正影响界面的数据才值得重点纳入状态管理异步请求里的过程状态要考虑异常恢复互斥的页面阶段尽量不要堆成一排 boolean局部状态尽量靠近使用它的组件。这些东西看起来都不复杂但实际项目里大量“第一次正常、第二次不刷新”“日志有值、页面没变化”“请求失败后按钮永远点不了”的问题往往就是从这里开始的。
阅读完成 · 觉得有帮助?
咨询建站