Skip to content

fix(applist): 修复自动刷新导致应用列表跳动 - #90

Merged
qiin2333 merged 2 commits into
masterfrom
fix/applist-refresh-flicker
Jul 28, 2026
Merged

fix(applist): 修复自动刷新导致应用列表跳动#90
qiin2333 merged 2 commits into
masterfrom
fix/applist-refresh-flicker

Conversation

@qiin2333

@qiin2333 qiin2333 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

问题

用户反馈:应用列表页放在那不动,自动刷新会一直跳动。

原因

三个原因叠加:

  1. setApps() 全量重建对象 — 每次刷新都 new ObservableApp(...),把 localIconPath / iconLoaded 一起清空。于是每轮刷新整列先闪回加载占位符,再串行把 9 个图标重新拉一遍。
  2. ForEach key 含易变状态 — key 是 ${id}_${isRunning}_${iconLoaded},任何一个图标加载完成都会让 ArkUI 销毁并重建那个列表项,而不是通过 @ObjectLink 就地更新。
  3. 静默刷新驱动了 Refresh 组件onPageShow 和退游戏后的收敛轮询都走 refreshApps(silent=true),但仍然置 isRefreshing = true,下拉指示条每轮展开又收起,把列表顶下去再弹回来。

改动

  • AppViewModel.setApps() 改为按 id 增量合并:复用已有的 ObservableApp 实例,只更新变化字段,图标状态得以保留。
  • ForEach 的 key 收敛到只含 id(网格行 key 同理去掉 isRunning)。
  • 静默刷新不再置 isRefreshing
  • loadAllIcons 只处理 !iconLoaded 的应用;取消标志从布尔改为代际计数,修掉了旧写法下多个加载循环可能并发跑的隐患。
  • refreshApps 增加 in-flight 合并:拆成外层守卫 + doRefreshApps 实现,同一时刻只发一次请求。此前单击刷新按钮会打出两个相隔 14ms 的 getAppList。撞上静默刷新的手动刷新仍会显示下拉指示条并等到结束。
  • 静默刷新拿到空列表时保留现有数据,避免服务端瞬时故障把整页闪成"没有可用的应用"。
  • 新增 AppListViewModel.syncRunningState():上面那个"保留旧数据"分支在 return 前按刚拿到的 computer.runningGameId 同步运行状态。否则 hasRunningApp 会停在旧值,而退游戏后的收敛轮询正是靠它判断,一次瞬时空 applist 就会把卡片钉在"运行中"并耗光全部 12 次轮询。
  • AppCard@Watch:实例现在会跨刷新复用,图标路径变化时需要清掉 imageLoadFailed,否则一次加载失败会把卡片永久钉在占位图上。

验证

在 MatePad Pro 13 模拟器(HarmonyOS 6.1.1)上,连接真实 Sunshine host(9 个应用)实测:

场景 结果
首次进入列表 9 个图标正常加载
连续 4 轮静默刷新 每轮日志都有 getAppList: HTTPS 请求成功 + 加载 9 个应用,但零条图标加载日志
刷新前后截图 逐字节相同(MD5 一致)
单击刷新按钮 只发 1 次 getAppList(修复前为 2 次)
真起一局再退出 角标 1.04s、第一次收敛轮询即清除

关键证据是那条消失的日志:修复前每轮刷新都会打 开始加载 9 个应用图标 加 9 条 图标加载成功,修复后四轮刷新一条都没有。

运行状态这条通路单独跑了一遍(去掉 key 里的 isRunning 后是否还能及时响应):在真机 host 上启动「桌面」再退出,

11:45:34.988  AppListViewModel: 加载 9 个应用, 正在运行: 桌面(334004416)
11:45:36.024  AppListViewModel: 加载 9 个应用, 正在运行: 无
11:45:36.025  AppListPageV2: 运行状态收敛(第 1 次轮询后)

截图确认「运行中」角标与绿色边框同时消失。订阅挂在 @Observed 对象上,与数组重建、ForEach 节点复用无关,所以 key 去掉 isRunning 不影响时效;页面级的 hasRunningApp / runningApp 也全是命令式使用,build() 里一处都没有。

Reviewer 需要知道

未覆盖:smallIconMode 的列表布局(AppListItem 没有 @State imageLoadFailed,不需要 @Watch,但布局层面未实测)。

🤖 Generated with Claude Code

应用列表页放着不动、后台自动刷新时整页会反复跳动。三个叠加的原因:

1. setApps() 每次全量重建 ObservableApp,把 localIconPath/iconLoaded
   一起清掉,于是每轮刷新整列先闪回占位符再逐个把图标拉回来。改成按 id
   增量合并,复用已有实例,变化通过 @ObjectLink 就地推给组件。
2. ForEach 的 key 里带了 isRunning/iconLoaded,任一状态变化都会销毁重建
   列表项而不是原地更新。key 收敛到只含 id。
3. 静默刷新(onPageShow、退游戏后的收敛轮询)也置 isRefreshing=true,
   Refresh 的下拉指示条每轮展开又收起,把列表顶下去再弹回来。静默刷新
   不再驱动该组件。

顺带:
- refreshApps 加 in-flight 合并,连点刷新按钮或后台轮询与手动刷新撞车时
  只发一次请求(此前单击会打出两个相隔 14ms 的 getAppList)。
- 静默刷新拿到空列表时保留现有数据,避免瞬时故障把整页闪成"没有可用的应用"。
- 图标加载的取消标志换成代际计数,修掉多个加载循环并发跑的隐患。
- AppCard 实例现在会跨刷新复用,加 @watch 在图标路径变化时清掉
  imageLoadFailed,否则一次加载失败会把卡片永久钉在占位图上。

在 MatePad Pro 13 模拟器 + 真实 Sunshine host(9 个应用)上验证:连续四轮
静默刷新日志里零条图标加载记录,前后截图逐字节相同;单击刷新只发一次
getAppList。未覆盖退游戏后的运行状态收敛轮询路径和 smallIconMode 布局。

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

本次变更完善应用列表刷新与图标加载流程:增加刷新防重入、静默空列表保留、图标加载代际控制和应用实例复用,并使用稳定 key 及图标路径监听减少组件状态异常。

Changes

应用列表稳定性

Layer / File(s) Summary
图标加载状态管理
entry/src/main/ets/viewmodel/AppViewModel.ets
按应用 id 复用 ObservableApp 实例,使用代际计数取消过期的异步图标加载,并校验加载完成状态。
刷新请求协调
entry/src/main/ets/pages/AppListPageV2.ets
缓存进行中的刷新 Promise,统一维护刷新状态,并在静默刷新返回空列表且本地已有数据时保留当前列表。
稳定的列表渲染与图标组件状态
entry/src/main/ets/pages/AppListPageV2.ets, entry/src/main/ets/components/AppCard.ets
列表和网格 key 仅使用应用 id;AppCard 在图标路径变化时重置图标加载失败状态。

Estimated code review effort: 4 (Complex) | ~40 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了本次修复自动刷新导致应用列表跳动的核心变化。
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/applist-refresh-flicker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
entry/src/main/ets/pages/AppListPageV2.ets (1)

511-534: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

静默空列表保留分支未同步最新运行状态。

514-519 行已用最新 serverInfo.currentGame 更新了 computer.runningGameId,但 526-531 行在静默刷新拿到空列表时直接 return,跳过了 534 行的 markRunningApps(本来会把新状态应用到即将传入 setAppsapps)。结果是:viewModel 中保留的旧应用列表 isRunning/hasRunningApp 停留在刷新前的状态,即使这次请求已经明确知道游戏已经退出(currentGame=0)。

这正好会被 scheduleNextRunningPoll(退游戏收敛轮询,PR 中标注为"尚未覆盖")以 silent=true 命中:某次轮询恰好返回空列表时,"运行中"角标不会消失,轮询也不会提前收敛,需等到下一次非空列表刷新才更正。

建议:即使提前返回,也用新的 runningGameId 同步一次已保留列表的运行状态,而不是完全跳过。

🔧 建议修复方向
+        // 即使空列表被丢弃,仍需用最新运行状态同步已保留的应用列表
+        const latestRunningGameId = computer?.runningGameId || 0;
         if (apps.length === 0 && silent && this.viewModel.appCount > 0) {
           console.warn('AppListPageV2: 静默刷新返回空列表,保留现有数据');
+          this.viewModel.syncRunningState(latestRunningGameId);
           return;
         }

AppListViewModel 中新增:

syncRunningState(runningGameId: number): void {
  for (const app of this.apps) {
    app.isRunning = runningGameId > 0 && app.id === runningGameId;
  }
  this.updateRunningApp();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@entry/src/main/ets/pages/AppListPageV2.ets` around lines 511 - 534, Update
the silent empty-list early-return branch in the app refresh flow to synchronize
the retained applications with the latest computer.runningGameId before
returning. Add or reuse AppListViewModel.syncRunningState(runningGameId) to
update each app’s isRunning state and refresh the aggregate running state, while
preserving the existing list-retention behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@entry/src/main/ets/pages/AppListPageV2.ets`:
- Around line 511-534: Update the silent empty-list early-return branch in the
app refresh flow to synchronize the retained applications with the latest
computer.runningGameId before returning. Add or reuse
AppListViewModel.syncRunningState(runningGameId) to update each app’s isRunning
state and refresh the aggregate running state, while preserving the existing
list-retention behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f2e3c219-6b06-45a0-85ad-6de6c0b0316f

📥 Commits

Reviewing files that changed from the base of the PR and between a12c81e and ebe01f3.

📒 Files selected for processing (3)
  • entry/src/main/ets/components/AppCard.ets
  • entry/src/main/ets/pages/AppListPageV2.ets
  • entry/src/main/ets/viewmodel/AppViewModel.ets

静默刷新拿到空列表时保留旧列表,但直接 return 跳过了 markRunningApps,
hasRunningApp 停在旧值。退游戏后的收敛轮询正是靠 hasRunningApp 判断收敛,
于是一次瞬时的空 applist 就会让卡片一直显示"运行中",并把 12 次轮询全部耗光。

加 AppListViewModel.syncRunningState(runningGameId),用刚拿到的 serverInfo
同步现有实例的 isRunning 并刷新聚合状态,列表保留行为不变。

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@qiin2333
qiin2333 merged commit ac6a038 into master Jul 28, 2026
2 checks passed
@qiin2333
qiin2333 deleted the fix/applist-refresh-flicker branch July 28, 2026 03:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant