这次做的不是“把页面分成左右两块”,而是处理平行视界更容易留下后遗症的一件事:主从布局切换之后,详情页路由和当前选中项还能不能保持同一份状态。Demo 名为 ParallelRouteLab,本轮固定使用会话 pv_route_20261001_05,选中内容 article_042。

一、窗口一变,路由栈先乱了

单窗状态下,用户从文章列表点进详情,路径很自然:

ArticleList -> ArticleDetail(article_042)

切到双窗以后,左边重新出现列表,右边继续显示详情。如果仍然沿用“点击一次就 push 一次”的思路,短时间内可能叠出两份甚至三份 ArticleDetail。UI 不一定立刻报错,但返回键、再次切换窗口形态、后台恢复时就会暴露:要么返回两次才到列表,要么右侧短暂闪回上一次文章。

我把问题拆成两层:一层是 SINGLE / DUAL_PANE 这样的布局状态,另一层是 master、detail、selectedId 和栈深度组成的内容路由状态。两者不能互相代替。

这次验收口径固定为:

  • Session:pv_route_20261001_05
  • Master:/pages/ArticleList
  • Detail:/pages/ArticleDetail
  • Selected:article_042
  • 双窗稳定后 Stack Depth:2
  • 本轮检测并处理的重复详情路由:2
  • 最终残留重复详情路由:0
  • Restore Count:1
  • Window Width:1280 vp
  • 最终 State:STABLE

二、先把“选中项”从页面里拿出来

最开始的实现里,列表页自己记 selectedId,详情页又从路由参数里拿一份 articleId。单窗时问题不明显,一到双窗就容易出现两份数据互相追赶。

所以我先把“当前选中哪篇文章”提升到独立快照里。这段代码解决的不是持久化,而是让列表和详情不要各自猜状态。

export type PaneMode = 'SINGLE' | 'DUAL_PANE'

export interface RouteSnapshot {
  session: string
  mode: PaneMode
  articleId: string
  restoreCount: number
}

export class RouteSnapshotStore {
  private snapshot: RouteSnapshot = {
    session: 'pv_route_20261001_05',
    mode: 'SINGLE',
    articleId: '',
    restoreCount: 0
  }

  saveSelected(articleId: string): void {
    this.snapshot.articleId = articleId
  }

  saveMode(mode: PaneMode): void {
    this.snapshot.mode = mode
  }

  markRestored(): void {
    this.snapshot.restoreCount += 1
  }

  current(): RouteSnapshot {
    return { ...this.snapshot }
  }
}

列表点击只负责写入 articleId,窗口变化只负责写入 mode,真正修改 Navigation 栈的动作统一交给协调器。Demo 阶段我没有急着落盘,因为当前要解决的是窗口形态变化,而不是进程被杀后的冷启动恢复。正式项目如果需要跨进程恢复,再把 Store 接到 Preferences 或数据库更合适。

三、不是“补一个详情页”,而是把路由归一化

后来我发现,遇到重复详情页时继续 pop() 很危险。你不知道栈里重复了几次,也不知道中间有没有其他业务页。与其修补,不如先定义平行视界下合法路由的标准形态。

对这个 Demo 来说,双窗稳定态只有一种:

ArticleList -> ArticleDetail(article_042)

于是我把修复写成归一化。

import { NavPathStack } from '@kit.ArkUI'

export class RouteCoordinator {
  private stack: NavPathStack = new NavPathStack()
  private store: RouteSnapshotStore = new RouteSnapshotStore()
  private droppedDetailRoutes: number = 0

  selectArticle(id: string): void {
    this.store.saveSelected(id)
    this.normalizeDetailRoute(id)
  }

  private normalizeDetailRoute(id: string): void {
    const names = this.stack.getAllPathName()
    this.droppedDetailRoutes +=
      names.filter(name => name === 'ArticleDetail').length

    this.stack.clear()
    this.stack.pushPath({ name: 'ArticleList' })
    this.stack.pushPath({
      name: 'ArticleDetail',
      param: { articleId: id }
    })
  }
}

本次调试里检测到两条重复 ArticleDetail,所以 DevEco 图里的 Duplicate Detail Routes = 2 表示本轮被处理掉的重复路由计数;最终手机图中残留重复详情路由为 0。一个是处理量,一个是最终存量。

四、布局变化只发一个恢复动作

另一个坑来自窗口变化本身。一次形态切换可能伴随尺寸变化、组件重建和页面可见性变化。如果多个回调都直接调用归一化,反而会制造重复恢复。

我最终把入口收敛到 onPaneModeChanged():

onPaneModeChanged(mode: PaneMode, widthVp: number): void {
  this.store.saveMode(mode)

  const snapshot = this.store.current()
  if (!snapshot.articleId) {
    return
  }

  if (mode === 'DUAL_PANE' && widthVp === 1280) {
    this.state = 'NORMALIZING'
    this.normalizeDetailRoute(snapshot.articleId)
    this.store.markRestored()
    this.state = 'STABLE'
  }
}

执行以后状态按 SINGLE -> NORMALIZING -> DUAL_PANE -> STABLE 收敛,Restore Count = 1,说明同一次切换只恢复了一次。

DevEco 里我重点看两处:normalizeDetailRoute() 和底部 HiLog。日志为:

mode=SINGLE -> DUAL_PANE
selected=article_042
duplicate detail dropped=2
restore count=1
State: NORMALIZING -> STABLE

五、路由恢复不等于重新拉一遍数据

路由恢复以后还有一个细节:ArticleDetail 被重新挂回右侧时,要不要重新请求全文?

我的判断是,路由恢复和数据刷新应该分开。当前 article_042 的详情缓存仍然有效时,只重建展示关系;缓存过期、版本变化或服务端明确要求刷新时,才重新请求。

六、最终结果要看四份状态是不是同一件事

最终运行图里,Parallel Route Lab 处于:

  • Session:pv_route_20261001_05
  • Mode:DUAL_PANE
  • Selected:article_042
  • Stack Depth:2
  • Duplicate Detail Routes:0
  • Restore Count:1
  • Window Width:1280 vp
  • State:STABLE

我更在意的不是 UI 上已经有两栏,而是选中项、路由栈、布局模式、详情内容四者已经对齐。HarmonyOS 7 的多设备适配资料把平行视界作为典型的应用内分屏场景,但工程里不能把它理解成单纯改 Row/Column;返回行为、选中态、路由恢复同样要一起验收。

七、三个判断

第一,布局状态不要直接充当业务路由状态。DUAL_PANE 只说明怎么展示,不说明该展示哪篇详情。

第二,路由修复优先做归一化。能定义合法标准形态时,标准形态比“pop 两次再 push 一次”更容易测试。

第三,恢复次数本身就是指标。窗口切换一次、恢复一次才是稳定实现。只看最终页面有没有显示,很容易把重复回调带到线上。

Demo 最后稳定在 Stack Depth = 2、Duplicate Detail Routes = 0、Restore Count = 1。从外观看只是平行视界一左一右两块内容,决定体验的是:切过去以后能否准确回来,以及路由栈有没有多出一层。

Logo

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

更多推荐