Skip to content

移动端页面状态契约:加载、离线与过期数据如何共存

一个 isLoading 布尔值不足以描述真实移动页面。页面可能正在刷新,但旧数据仍可读;也可能请求成功却为空;断网时有缓存与没有缓存,用户应该看到完全不同的内容。

用数据和同步状态两个维度建模 ​

保留 snapshot、lastSyncedAt、requestState、error。渲染规则从这些字段推导,不把每一种组合都塞进一个巨大的枚举。这样“旧数据 + 刷新中”和“旧数据 + 刷新失败”都能保留页面主体,同时展示不同提示。

快照请求页面表现
无首次加载骨架屏,保留返回操作
无失败错误说明与重试入口
有刷新中继续展示快照,局部刷新提示
有失败展示快照、同步时间和非阻断错误
空列表成功真正的空态和创建入口

把状态规则放在视图外 ​

在 RN 的 selector 或 Flutter 的 ViewModel 中实现纯函数 deriveScreenState(snapshot, request),让多个页面共享规则。进入页面、下拉刷新、前台恢复、推送消息触发刷新,都调用同一数据入口;每个入口只改变请求原因,不改变“什么是可信快照”。

验收时模拟慢网和离线:连续刷新不会把列表清空;失败时不会把旧列表误判为空;账户切换后绝不展示上一账户的快照。空态只能由一次可信的成功响应确认。

把“空”变成有证据的状态 ​

以消息列表为例,首次进入页面时没有缓存,状态是 initialLoading;成功返回零条消息后才是 confirmedEmpty。如果先前有二十条消息,用户下拉刷新时应保持列表可读,状态是 refreshingWithData。刷新失败则进入 staleWithError,继续保留二十条和上次成功时间。把所有情况压成 loading | success | error,会迫使界面在刷新期间清空内容,或把失败误画成空列表。

数据仓库建议保存 {accountId, items, lastSuccessAt, revision, pending},展示状态由这些事实和当前请求推导,不要让每个组件各自维护一套布尔值。items=[] 只表示当前数组为空;只有“当前账号的一次可信成功响应”才能证明真正空态。账户切换时先隔离旧账号快照,再加载新账号缓存,避免上一账号的消息在新账号页面闪现。

事件保留的数据页面反馈
首次加载失败无错误页与重试按钮
有缓存时刷新失败上次确认的列表内联错误、同步时间与重试
请求成功返回空列表当前账号的空快照明确的空态文案
账号切换仅新账号缓存骨架屏或新账号快照

状态转换比截图更值得测试 ​

以 reducer 或状态机测试事件序列:成功(20条) → 刷新 → 超时 必须保留二十条;成功(20条) → 成功(0条) 才能变成空态;账号A加载 → 切换B → A响应 必须丢弃 A 的结果。UI 测试再验证重试入口、读屏提示和旧数据标识。这样测试的是用户会经历的变化,而不仅是一张静态页面。

MIT Licensed