三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

山海万灵 HarmonyOS 文化知识实战续篇(17):loading、empty、error、retry 统一状态落地

山海万灵 HarmonyOS 文化知识实战续篇(17):loading、empty、error、retry 统一状态落地

知识应用启动时,读者看到的并不是一个抽象的请求过程,而是图鉴、地域和探索进度能否在合适的时刻出现。山海万灵把加载、内容、空态和恢复动作收敛为同一份状态,让首页、图鉴、探索、馆长和档案页共享数据变化的语义。

一、四种状态比多个布尔值更稳定

单独维护 isLoading、isEmpty 和 hasError 容易产生互相矛盾的组合:页面既在加载又提示没有内容,或者已经回退到本地资料却仍显示错误。统一 phase 只允许一个主状态生效,isLocalFallback 则补充内容的来源信息。

export type ShanhaiAppDataPhase = 'LOADING' | 'CONTENT' | 'EMPTY' | 'ERROR'; export interface ShanhaiAppLoadState { phase: ShanhaiAppDataPhase; message: string; isLocalFallback: boolean; } export function createShanhaiLoadingState(): ShanhaiAppLoadState { return { phase: 'LOADING', message: '正在同步山海资料,请稍候。', isLocalFallback: false }; }

这个模型的关键不在于枚举数量,而在于页面只消费一个可信结果。内容可用时进入 CONTENT;必要资料为空时进入 EMPTY;远端与本地都无法提供内容时才进入 ERROR。每个状态都能映射到明确的视觉反馈和下一步动作。

二、启动流程先写入 Loading,再决定去向

启动阶段先让页面进入 LOADING,再由 Bootstrap 结果决定后续状态。内容加载和状态写入在同一处完成,避免各个 Feature 页面分别猜测数据是否已经准备好。

private async refreshBootstrap(): Promise<void> { this.appLoadState = createShanhaiLoadingState(); try { const result = await this.appViewModel.loadBootstrapWithStatus(); if (!result.hasRequiredContent) { this.appLoadState = createShanhaiEmptyState(); return; } this.applyBootstrap(result.bootstrap); this.appLoadState = createShanhaiContentState( result.isLocalFallback ? 'mock-api 暂不可用,正在使用本地内容。' : '内容与图谱已同步。', result.isLocalFallback ); this.refreshAiContext(result.bootstrap.beasts[0].id); } catch (_) { this.appLoadState = createShanhaiErrorState(); } }

这段顺序同时保留了两层信息:页面有没有可展示资料,以及资料是否来自本地回退。远端请求暂时失败并不等于读者无法继续阅读;只要本地资料完整,页面仍进入 CONTENT,并用提示语说明当前来源。

三、空态和错误态承担不同责任

状态出现条件页面反馈用户动作
LOADING正在读取启动资料显示进度与同步提示等待
CONTENT必要资料已经可用展示图鉴与探索内容继续浏览
EMPTY启动结果缺少必要内容说明暂无可展示资料重新同步
ERROR没有可用内容可以恢复给出恢复提示重试加载

EMPTY 表示结果有效但没有可展示内容;ERROR 表示无法获得可用内容。把两者合并会让恢复策略变得模糊:空态需要重新同步,错误态需要重试加载,并且提示文字必须让读者知道接下来能够做什么。

四、恢复面板只解释状态,不复制业务流程

统一面板根据 phase 选择标题和按钮文案。它不直接处理网络细节,也不复制 Bootstrap 的读取逻辑;所有恢复操作都回到 onRetry,从而避免不同页面分别实现一套不一致的重试链路。

private title(): string { if (this.state.phase === 'LOADING') { return '正在准备山海资料'; } if (this.state.phase === 'EMPTY') { return '暂未发现可展示内容'; } return '内容加载失败'; } private actionText(): string { return this.state.phase === 'EMPTY' ? '重新同步' : '重试加载'; }

当页面进入恢复模式,读者看到的是同一套术语和动作。后续如果增加网络状态、缓存更新或内容版本提示,只需要扩展状态层的契约,Feature 页面仍然通过同一面板获得一致反馈。

五、重试入口必须回到同一条启动链路

首页、图鉴、探索和馆长页将同一个 appLoadState 传入各自内容组件,并把恢复动作回调到 refreshBootstrap。这样一次点击不会只刷新局部卡片,而是重新取得完整 Bootstrap 并再计算跨页面共享的数据。

ShanhaiHomePageContent({ beasts: this.beasts, discoveredCount: this.discoveredIds.length, passportCount: this.passportRegionIds.length, syncState: this.appLoadState, onRetrySync: () => { this.refreshBootstrap(); } })

这种入口收敛还能避免并发状态漂移。刷新前先切到 LOADING,成功后一次性应用 Bootstrap,失败时统一进入 ERROR;页面不会在旧数据与新提示之间留下半更新的组合。

六、页面证据观察的是恢复动作

下图展示空资料状态下的“重新同步”入口。读者可以看到状态面板没有把空内容伪装成正常图鉴,也保留了恢复动作。执行重试后,应用会重新走 Bootstrap 流程,并用内容状态或新的恢复状态替换当前面板。

七、回退内容仍然属于 Content

远端服务不可用而本地资料仍然完整时,页面不会切换到 ERROR。内容、图谱和探索入口保持可用,状态消息提示正在使用本地内容。这个取舍把“数据来源暂时变化”和“读者无法继续阅读”分开处理,避免把可用的离线资料隐藏在错误页之后。

结果主状态isLocalFallback页面行为
远端成功CONTENTfalse显示同步完成后的内容
远端失败、本地成功CONTENTtrue继续展示本地资料并提示来源
必要资料为空EMPTYfalse显示重新同步
没有可用资料ERRORtrue显示重试加载

八、验收关注状态切换而不是单个控件

验收这条链路时,应先观察启动时的同步提示,再检查内容可用后的一级页面;随后进入空资料状态并点击重新同步,确认恢复入口回到完整启动流程。验证重点是 phase、提示语和用户动作是否同步变化,而不是只确认某个按钮能够被点击。

function applyState(state: ShanhaiAppLoadState): string { if (state.phase === 'LOADING') return '显示加载进度'; if (state.phase === 'CONTENT' && state.isLocalFallback) return '显示本地内容提示'; if (state.phase === 'CONTENT') return '显示已同步内容'; if (state.phase === 'EMPTY') return '显示重新同步入口'; return '显示重试加载入口'; }

九、状态协议为后续功能保留边界

统一状态并不要求每个页面拥有相同布局,但要求它们对同一份数据结果作出一致解释。未来增加缓存刷新、内容版本或更细粒度的局部加载时,可以在状态层增加字段或子状态,而不让五个页面各自引入新的布尔值组合。状态模型、恢复入口和页面回读共同形成了一条可维护的加载链路。

HarmonyOS 的状态管理机制可参考 ArkTS 状态管理官方概览。

十、把状态更新视为一次完整提交

加载链路还要处理快速重复触发。读者连续点击“重新同步”时,旧请求可能晚于新请求返回;如果旧结果直接覆盖页面,就会把已经恢复的内容重新写回空态。状态协调层应为每次启动请求维护递增序号,只接受仍然是最新序号的结果。这样 Loading、Content 和恢复面板始终对应同一次请求,而不会因返回顺序随机变化。

对本地回退也应采用同样的提交原则。先确认远端结果是否仍属于当前请求,再决定是否采用本地内容;本地内容一旦被选中,就连同 isLocalFallback 和提示语一起写入状态。页面只在完整状态对象到达后重新渲染,避免列表已经替换而顶部提示仍停留在旧请求的信息。

private bootstrapSequence: number = 0; private async refreshWithSequence(): Promise<void> { const sequence = ++this.bootstrapSequence; this.appLoadState = createShanhaiLoadingState(); try { const result = await this.appViewModel.loadBootstrapWithStatus(); if (sequence !== this.bootstrapSequence) return; if (!result.hasRequiredContent) { this.appLoadState = createShanhaiEmptyState(); return; } this.applyBootstrap(result.bootstrap); this.appLoadState = createShanhaiContentState( result.isLocalFallback ? '正在使用本地内容。' : '内容与图谱已同步。', result.isLocalFallback ); } catch (_) { if (sequence === this.bootstrapSequence) this.appLoadState = createShanhaiErrorState(); } }

这里的序号不是为了给请求编号展示给读者,而是为了保护界面事实:较早请求只能在它仍然最新时提交结果。网络慢、切换页面或重复点击都不会让旧回调覆盖当前状态。若页面已经卸载,协调层也可以在生命周期结束时递增序号,使未完成请求自然失效。

状态消息同样需要在一次提交中更新。成功内容、空态说明和恢复按钮的文案不应分别异步写入;它们属于一个对外可观察的状态快照。读者在任何时刻看到的标题、提示和动作都来自同一个 phase,这也是排查加载问题时最容易复现、最容易回读的边界。

当后续接入缓存刷新时,可以把“正在显示旧内容并刷新”表示为 CONTENT 下的附加字段,而不是重新引入多个互斥布尔值。页面继续展示当前图鉴,提示区显示刷新进度;只有缓存与网络内容都不可用时才转入恢复面板。这样既不会遮蔽可阅读内容,也能让失败路径保持明确。

← 返回列表