HarmonyOS 7 ArkUI + Navigation:平行视界主从切换中的路由栈归一化与详情状态恢复【鸿蒙心迹】
这次做的不是“把页面分成左右两块”,而是处理平行视界更容易留下后遗症的一件事:主从布局切换之后,详情页路由和当前选中项还能不能保持同一份状态。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。从外观看只是平行视界一左一右两块内容,决定体验的是:切过去以后能否准确回来,以及路由栈有没有多出一层。
更多推荐




所有评论(0)