HarmonyOS 应用实战 71:切后台回来别显示旧题库:用 onPageShow 触发服务层刷新

用户从后台回到应用时,如果只依赖页面首次出现,选择器可能仍停留在旧牌组名上。这一篇不从界面现象兜圈子,先盯住 currentDeckIdlastForegroundAtlibraryHSP/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 没有写前台刷新信号 这类链路断点。

落地顺序:回到前台刷新 从最小闭环开始

  1. 先在模型或服务层补 ForegroundDeckRefreshState,不要先改 UI。
  2. 再把现有 libraryHSP/src/main/ets/components/DeckPicker.ets 的调用点接到新状态。
  3. 最后补页面反馈和异常分支,避免用户看到无响应。
// 回到前台刷新 的最小回归清单
const checklist: string[] = [
  '后台编辑牌组后返回首页,选择器文字应该随 lastDeckUpdateAt 更新。',
  '仅前后台切换、不改牌组时,列表刷新不应该改变 currentDeckId。',
  '删除当前牌组后返回首页,选择器应该落到默认牌组。',
  '模拟 Preferences 为空时,currentDeckId 仍有 DEFAULT_DECK_ID 兜底。'
];

收口:回到前台刷新 的交付边界

这篇的结论可以压成一句话:EntryAbility 负责前后台事件,DeckPicker 负责选项和选中项重算,DeckService 负责持久化牌组。 只要这个边界不变,后续加设置项、恢复入口、批量操作或发布校验,都能沿着同一条链路扩展。

如果要把 ForegroundDeckRefreshState 真正落到工程里,优先改服务和模型,再接页面。这样 回到前台刷新 的多个入口能复用同一套规则,文章里的验证点也能直接转成开发时的回归清单。
ervice 负责持久化牌组。 只要这个边界不变,后续加设置项、恢复入口、批量操作或发布校验,都能沿着同一条链路扩展。

如果要把 ForegroundDeckRefreshState 真正落到工程里,优先改服务和模型,再接页面。这样 回到前台刷新 的多个入口能复用同一套规则,文章里的验证点也能直接转成开发时的回归清单。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐