diff --git a/docs/plans/2026-10-21-K95-feed-switch-loading.md b/docs/plans/2026-10-21-K95-feed-switch-loading.md new file mode 100644 index 0000000..da1abda --- /dev/null +++ b/docs/plans/2026-10-21-K95-feed-switch-loading.md @@ -0,0 +1,100 @@ +# K95 修复首页频道快速切换卡 Loading + +## 目标 + +- 频道请求完成后始终写回发起请求的频道缓存,不再因用户切走而丢失结果。 +- 每频道明确区分 loaded / loading / in-flight / error,无请求时绝不显示首屏转圈。 +- 首屏和刷新请求增加可注入的 10 秒超时兜底。 +- PlayerLogic 保证连续 feed 更新串行应用,只让最后一次 feed/session 生效。 + +## 分支 + +- ReelShort:`fix/k95-feed-switch-loading`,从 `origin/develop` 创建。 +- FruitServer 仅提交计划与 PROGRESS,不改功能代码。 + +## A. HomeFeedController + +每个频道状态增加: + +- `List dramas` +- `String? cursor` +- `bool hasMore` +- `bool loaded` +- `bool loading` +- `bool loadingMore` +- `String? lastError` +- `int requestSeq` +- `Future? inFlight` + +请求规则: + +1. 响应成功/失败都写入发起请求的频道状态。 +2. 只有频道仍为当前频道时才 `notifyListeners`。 +3. `requestSeq` 只比较同一频道的新旧请求,不参与跨频道过滤。 +4. 超时时间通过构造参数注入,默认 10 秒。 +5. `loadingFirstPage = !loaded && loading`。 +6. `selectChannel`: + - 有缓存:同步显示,未在飞时后台静默刷新 + - 无缓存且正在飞:等待现有请求,不重复发 + - 无缓存且有历史错误:自动重试 + - 从未加载:发起首屏请求 +7. `refreshCurrent` 同样复用 in-flight,防止并发重复刷新。 +8. `loadMore` 失败保留 cursor/hasMore,允许下次继续重试。 + +## B. PlayerLogic / PlayerPage + +### B1 Feed revision + +- PlayerLogic 增加 `int feedRevision`。 +- 每次 `updateFeed` 自增 revision,记录最新目标 feed。 +- `_replaceSession` 与 `selectEpisode` 在 await 后校验 revision,过期流程直接返回。 +- 连续 updateFeed 使用串行队列:当前应用结束后再应用最新待处理 feed。 + +### B2 空队列与频道切换 + +- 空 feed 不创建新 session,只清空 queue/visibleIndices 并停播。 +- 空 → 非空必须恢复 route visible,并 select 第 0 个。 +- 频道变化通过额外 reset 标志传入 `updateFeed`,即使 drama id 列表相同, + 也重置到第 1 个并开始播放。 + +### B3 PlayerPage 更新判定 + +- `didUpdateWidget` 用 `feedDramas` 的 drama id 列表签名判断内容变化。 +- 同一列表的新 List 实例不重复 updateFeed。 +- 频道变化始终触发 updateFeed,并传 `resetToFirst=true`。 + +## C. 测试 + +### HomeFeedController + +- A 挂起 → B 成功 → A 成功:A/B loaded 均为 true,切回 A 立即有数据。 +- 同频道旧失败响应不得覆盖新成功响应。 +- 无在飞请求时 `loadingFirstPage=false`。 +- 永不返回的请求在注入超时后结束 loading、写 lastError、loaded=true。 +- 失败频道再次 select 自动重试。 +- loadMore 失败保留原有 hasMore/cursor。 + +### PlayerLogic + +- 连续 5 次 updateFeed,最终 queue/session 对应最后一次列表。 +- 空 → 非空会创建 session 并 select 第 0 个。 +- 频道变化时相同 id 列表也回到第 1 个。 + +门禁: + +- `dart analyze lib test` +- `flutter test --no-pub` +- `flutter build apk --debug` + +## 风险与控制 + +1. 跨频道响应写回缓存必须与同频道 seq 校验同时存在;前者解决缓存丢失,后者解决同频道旧请求覆盖新结果。 +2. Player session 替换以 revision 作为唯一有效性判断,避免旧 close/select 回调干扰新 session。 +3. 空队列期间不创建 PlaybackSession;恢复非空时重新创建并起播。 +4. 不改后端、ReelShortWeb、specs;不安装或操作真机。 + +## Git + +- 提交:`fix(home): 修复频道快速切换卡 loading` +- `--no-ff` 合回 ReelShort `develop` 并 push。 +- 作者:`Zhou Weidong <502333560@qq.com>`;禁止 `git add -A`。 diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 6ce421a..a385fcf 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -24072,6 +24072,47 @@ Build successful. Please see dist-dev directory - 已先写计划 `docs/plans/2026-10-21-K94-home-type-feed.md`,记录数据层、频道缓存、 首页无感切换、播放器队列更新、分页触发和测试矩阵。 - 不改后端、ReelShortWeb、specs;不安装或操作真机。 +- 已完成 HomeFeedController 状态机:每频道保存 loaded/loading/inFlight/lastError; + 响应始终写回发起频道,仅当前频道 notify;requestSeq 仅处理同频道新旧请求。 +- 首屏/刷新加入默认 10 秒可注入超时;无在飞请求时 `loadingFirstPage=false`; + 失败频道再次选中会自动重试,loadMore 失败保留游标与 hasMore。 +- 已完成 PlayerLogic feedRevision 与串行更新:连续 updateFeed 只应用最新 feed; + session 替换和 selectEpisode await 后校验 revision;空→非空恢复播放并 select(0)。 +- PlayerPage.didUpdateWidget 改用 drama id 列表签名判断变化,频道变化传 resetToFirst。 + +门禁原始输出: + +```text +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test --no-pub +00:51 +539: All tests passed! + +$ flutter build apk --debug +Running Gradle task 'assembleDebug'... 24.8s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +``` + +解读:K95 静态分析零告警,539 项全量测试通过,debug APK 构建成功; +控制器/播放器定向测试 39 项也已单独复核通过。未安装或操作真机。 + +### K95 交付结果 + +- ReelShort 功能提交:`3c4d65a fix(home): 修复频道快速切换卡 loading`。 +- ReelShort develop 合并提交:`dfa79be merge: 合并 K95 频道切换加载修复`, + 已推送 `c8392b2..dfa79be`。 +- HomeFeedController 已按发起频道写回请求结果;每频道独立维护 + loaded/loading/inFlight/lastError,跨频道响应不再丢失,无在飞请求时不会转圈。 +- 首屏/刷新加入默认 10 秒可注入超时;失败频道再次选中会自动重试; + 同频道 requestSeq 保留,loadMore 失败保留 hasMore/cursor。 +- PlayerLogic 已加入 feedRevision 和串行 feed 更新;旧 session/select 异步流程会被 revision 丢弃, + 连续更新只应用最后一份 feed;空→非空恢复播放并从第 1 个开始。 +- PlayerPage 使用 drama id 列表签名判断变化,频道变化通过 resetToFirst 强制回到第 1 个。 +- 门禁:`dart analyze lib test` 零告警;`flutter test --no-pub` 539 项全过; + `flutter build apk --debug` 成功生成 `build/app/outputs/flutter-apk/app-debug.apk`。 +- 未改后端、ReelShortWeb、specs;未安装或操作真机,等待用户验收。 - 已完成数据层:新增 `FeedPage`,`CatalogApi.feed` 与 `CatalogRepository.fetchFeed` 支持 types/cursor/size,并补齐所有测试仓库实现。 - 已完成 `HomeFeedChannel` 与 `HomeFeedController`:推荐 seed、三频道独立缓存、 @@ -24114,3 +24155,11 @@ Running Gradle task 'assembleDebug'... 27.9s - 门禁:`dart analyze lib test` 零告警;`flutter test --no-pub` 532 项全过; `flutter build apk --debug` 成功生成 `build/app/outputs/flutter-apk/app-debug.apk`。 - 未改后端、ReelShortWeb、specs;未安装或操作真机,等待用户验收。 + +## 2026-10-21 K95 修复首页频道快速切换卡 Loading + +- 已完整读取 K95 `docs/plans/INBOX.md`;本批次只改 ReelShort Flutter 客户端。 +- 已创建 ReelShort `fix/k95-feed-switch-loading` worktree,基于最新 `origin/develop`。 +- 已先写计划 `docs/plans/2026-10-21-K95-feed-switch-loading.md`,明确频道请求归属、 + loading/in-flight 状态、超时、Player feedRevision 与串行化方案。 +- 不改后端、ReelShortWeb、specs;不安装或操作真机。