fix(applist): 修复自动刷新导致应用列表跳动 - #90
Conversation
应用列表页放着不动、后台自动刷新时整页会反复跳动。三个叠加的原因: 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]>
📝 WalkthroughWalkthrough本次变更完善应用列表刷新与图标加载流程:增加刷新防重入、静默空列表保留、图标加载代际控制和应用实例复用,并使用稳定 key 及图标路径监听减少组件状态异常。 Changes应用列表稳定性
Estimated code review effort: 4 (Complex) | ~40 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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(本来会把新状态应用到即将传入setApps的apps)。结果是: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
📒 Files selected for processing (3)
entry/src/main/ets/components/AppCard.etsentry/src/main/ets/pages/AppListPageV2.etsentry/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]>
问题
用户反馈:应用列表页放在那不动,自动刷新会一直跳动。
原因
三个原因叠加:
setApps()全量重建对象 — 每次刷新都new ObservableApp(...),把localIconPath/iconLoaded一起清空。于是每轮刷新整列先闪回加载占位符,再串行把 9 个图标重新拉一遍。ForEachkey 含易变状态 — key 是${id}_${isRunning}_${iconLoaded},任何一个图标加载完成都会让 ArkUI 销毁并重建那个列表项,而不是通过@ObjectLink就地更新。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 个应用)实测:
getAppList: HTTPS 请求成功+加载 9 个应用,但零条图标加载日志getAppList(修复前为 2 次)关键证据是那条消失的日志:修复前每轮刷新都会打
开始加载 9 个应用图标加 9 条图标加载成功,修复后四轮刷新一条都没有。运行状态这条通路单独跑了一遍(去掉 key 里的
isRunning后是否还能及时响应):在真机 host 上启动「桌面」再退出,截图确认「运行中」角标与绿色边框同时消失。订阅挂在
@Observed对象上,与数组重建、ForEach节点复用无关,所以 key 去掉isRunning不影响时效;页面级的hasRunningApp/runningApp也全是命令式使用,build()里一处都没有。Reviewer 需要知道
未覆盖:
smallIconMode的列表布局(AppListItem没有@State imageLoadFailed,不需要@Watch,但布局层面未实测)。🤖 Generated with Claude Code