HarmonyOS应用实战-启示散页-71-切后台回来别显示旧题库:用 onPageShow 触发服务层刷新
HarmonyOS 应用实战 71:切后台回来别显示旧题库:用 onPageShow 触发服务层刷新
用户从后台回到应用时,如果只依赖页面首次出现,选择器可能仍停留在旧牌组名上。这一篇不从界面现象兜圈子,先盯住 currentDeckId、lastForegroundAt 和 libraryHSP/src/main/ets/components/DeckPicker.ets 之间的交接;只要这条线说不清,回到前台刷新 后面一定会变成临时补丁。下面直接以 The_Book_of_Answers 工程里的 libraryHSP/src/main/ets/components/DeckPicker.ets 为锚点,把 回到前台刷新 拆成可以落地的工程方案。

当前代码锚点:回到前台刷新 先看 libraryHSP/src/main/ets/components/DeckPicker.ets
先看工程里和 回到前台刷新 最贴近的实现。锚点是 async aboutToAppear,它能说明当前功能已经由哪个文件承接,也能提醒我们别把新规则塞到错误层级。
0026 @StorageLink('lastDeckUpdateAt') @Watch('onUpdate') lastDeckUpdateAt: number = 0;
0027 @State summaries: DeckSummary[] = [];
0028 /** 显式 @State 驱动 Select;ArkUI Select 不会响应方法返回值的后续变化 */
0029 @State private selectedIndex: number = 0;
0030 @State private displayName: string = '默认题库';
0031 @State private options: SelectOption[] = [];
0032
0033 async aboutToAppear(): Promise<void> {
0034 await this.refresh();
0035 }
0036
0037 /** lastDeckUpdateAt 变化(增/删/改):重拉列表 + 重算选中 */
0038 onUpdate(): void {
0039 this.refresh().catch((e: Error) => {
0040 hilog.warn(DOMAIN, TAG, 'refresh failed: %{public}s', e.message);
这段代码里的关键事实是:当前选择器已经用 currentDeckId 和 lastDeckUpdateAt 两个 StorageLink 接收牌组变更。 继续重构时要尊重这个事实,否则就会把已经清晰的边界重新搅乱。
问题链路:回到前台刷新 为什么会出错
用户从后台回到应用时,如果只依赖页面首次出现,选择器可能仍停留在旧牌组名上。
真正的风险点在这里:刷新信号需要由 Ability 生命周期明确触发,组件只订阅状态,不直接感知窗口生命周期。 如果只在页面里补一个临时变量,下一次遇到后台恢复、导入、删除、重命名或升级迁移时,问题还会换一种方式出现。

责任边界:回到前台刷新 不能越过哪条线
EntryAbility 负责前后台事件,DeckPicker 负责选项和选中项重算,DeckService 负责持久化牌组。
| 层级 | 应该负责 | 不应该负责 |
|---|---|---|
| entry | 启动、前后台、路由入口 | 直接拼业务数据 |
| libraryHAR | 模型、仓储、偏好 key | 页面交互和弹窗 |
| libraryHSP | 服务编排、页面展示、组件状态 | 越过服务直接改底层 key |
状态模型:ForegroundDeckRefreshState
这个模型是建议新增或补强的设计,不是把页面里的局部状态简单换个名字。它的作用是让 回到前台刷新 有明确输入、输出和验证点。
interface ForegroundDeckRefreshState {
currentDeckId: string; // 当前牌组 id,只表达选择结果
lastDeckUpdateAt: number; // 牌组列表发生变化的时间戳
lastForegroundAt: number; // 应用回到前台时写入的刷新信号
}
function createForegroundDeckRefreshState(input: Partial<ForegroundDeckRefreshState>): ForegroundDeckRefreshState {
return {
currentDeckId: input.currentDeckId || '',
lastDeckUpdateAt: typeof input.lastDeckUpdateAt === 'number' ? input.lastDeckUpdateAt : 0,
lastForegroundAt: typeof input.lastForegroundAt === 'number' ? input.lastForegroundAt : 0,
};
}
字段不要只追求多,而要能解释失败。比如 currentDeckId 是主判断依据,lastForegroundAt 则用于收口边界或刷新时机。
服务落点:回到前台刷新 的规则放回 owner
前台事件只写 AppStorage 时间戳,不直接调用组件方法。 这类规则放在服务层,页面才不会因为不同入口而出现两套行为。
class ForegroundDeckRefreshService {
async buildTopic71(): Promise<ForegroundDeckRefreshState> {
const state = createForegroundDeckRefreshState({
currentDeckId: ''
});
await this.verifyBoundary(state);
return state;
}
private async verifyBoundary(state: ForegroundDeckRefreshState): Promise<void> {
// 前台事件只写 AppStorage 时间戳,不直接调用组件方法。
if (!state) {
throw new Error('回到前台刷新 state is empty');
}
}
}
这里的重点不是类名本身,而是 ForegroundDeckRefreshState 的调用方向:页面拿状态,服务管 回到前台刷新 的规则,仓储只处理读写。方向一旦倒过来,后续排查就会在 UI、服务和 key 之间反复横跳。
页面接入:回到前台刷新 页面只表达用户动作
DeckPicker 继续在 watch 回调里 refresh,避免页面持有服务细节。 页面应该给用户一个明确反馈,但不要把数据 owner 搬到 UI 里。
@Builder
function ForegroundDeckRefreshStatePanel(state: ForegroundDeckRefreshState) {
Column() {
Text('回到前台刷新')
.fontSize(18)
.fontWeight(FontWeight.Medium)
Text('DeckPicker 继续在 watch 回调里 refresh,避免页面持有服务细节。')
.fontSize(13)
.fontColor('#725D4C')
}
.padding(16)
}
如果页面需要显示中间态,就显示 回到前台刷新 的处理中、失败原因和下一步动作;不要让页面直接决定底层数据如何恢复。

验证样本:回到前台刷新 先跑边界
| 序号 | 验证点 |
|---|---|
| 1 | 后台编辑牌组后返回首页,选择器文字应该随 lastDeckUpdateAt 更新。 |
| 2 | 仅前后台切换、不改牌组时,列表刷新不应该改变 currentDeckId。 |
| 3 | 删除当前牌组后返回首页,选择器应该落到默认牌组。 |
| 4 | 模拟 Preferences 为空时,currentDeckId 仍有 DEFAULT_DECK_ID 兜底。 |
Write-Host "定位本文涉及的源码"
rg -n "async\ aboutToAppear|ForegroundDeckRefreshState|回到前台刷新" "D:\ProgramData\huawei\lesson\The_Book_of_Answers\libraryHSP\src\main\ets\components\DeckPicker.ets"
Write-Host "查看 回到前台刷新 相关的状态字段和刷新信号"
rg -n "currentDeckId|lastDeckUpdateAt|lastForegroundAt|AppStorageKey|AppPrefKey|StorageLink" "D:\ProgramData\huawei\lesson\The_Book_of_Answers"
这些命令只能确认 回到前台刷新 的源码落点、状态字段和刷新信号有没有写散,不能替代 DevEco 或真机交互。涉及前后台切换、页面返回、剪贴板、备份或发布构建的场景,还要继续走设备侧验证。
排查表:回到前台刷新 出问题先看哪里
| 现象 | 常见原因 | 优先检查 |
|---|---|---|
| 返回首页仍显示旧名字 | Ability 没有写前台刷新信号 | 查看 EntryAbility.onForeground 是否更新 lastForegroundAt |
| 下拉项为空 | 刷新早于 PreferencesStore.init | 确认 init 在 loadContent 前完成 |
| 选择后又跳回旧值 | setCurrent 写入失败后触发回滚 | 看 DeckService.setCurrent 是否抛出牌组不存在 |
排查 回到前台刷新 时先看 owner,再看信号,最后才看样式。比如 返回首页仍显示旧名字,通常不是单纯的布局问题,而是 Ability 没有写前台刷新信号 这类链路断点。
落地顺序:回到前台刷新 从最小闭环开始
- 先在模型或服务层补
ForegroundDeckRefreshState,不要先改 UI。 - 再把现有
libraryHSP/src/main/ets/components/DeckPicker.ets的调用点接到新状态。 - 最后补页面反馈和异常分支,避免用户看到无响应。
// 回到前台刷新 的最小回归清单
const checklist: string[] = [
'后台编辑牌组后返回首页,选择器文字应该随 lastDeckUpdateAt 更新。',
'仅前后台切换、不改牌组时,列表刷新不应该改变 currentDeckId。',
'删除当前牌组后返回首页,选择器应该落到默认牌组。',
'模拟 Preferences 为空时,currentDeckId 仍有 DEFAULT_DECK_ID 兜底。'
];
收口:回到前台刷新 的交付边界
这篇的结论可以压成一句话:EntryAbility 负责前后台事件,DeckPicker 负责选项和选中项重算,DeckService 负责持久化牌组。 只要这个边界不变,后续加设置项、恢复入口、批量操作或发布校验,都能沿着同一条链路扩展。
如果要把 ForegroundDeckRefreshState 真正落到工程里,优先改服务和模型,再接页面。这样 回到前台刷新 的多个入口能复用同一套规则,文章里的验证点也能直接转成开发时的回归清单。
ervice 负责持久化牌组。 只要这个边界不变,后续加设置项、恢复入口、批量操作或发布校验,都能沿着同一条链路扩展。
如果要把 ForegroundDeckRefreshState 真正落到工程里,优先改服务和模型,再接页面。这样 回到前台刷新 的多个入口能复用同一套规则,文章里的验证点也能直接转成开发时的回归清单。
更多推荐

所有评论(0)