ArkUI Navigation:平行视界详情页选中态与返回栈去重【鸿蒙心迹】
商品目录放在手机上,列表点进去是详情,返回再选别的商品,通常不会让人多想。但同一套界面适配到平板或折叠屏展开态时,左边列表常驻,右边详情连续切换,问题立刻复杂起来:点中第二个商品,到底是在更新选中态,还是给路由栈又塞进一张详情页?用户连点同一行,系统返回一次后会不会又看到同一个商品?这篇用一个小的双栏商品页,把列表选择、路由跳转和返回行为拆开说明。
## 一、平行视界不是把两个页面简单并排演示项目 SplitCatalog,批次编号 SPLIT_20261017_03。左栏是四条商品记录,右栏固定展示被选中的商品。构造操作依次为点击 SKU_201、点击 SKU_204、再次点击 SKU_204。最终希望看到 selectedId=SKU_204、detailNodes=1、suppressed=1、revision=2。这里的数字来自预设的业务事件序列,不是设备日志里实际采集的指标。首先明确两种状态。selectedId 是产品业务事实,表示列表中哪个商品被用户选中;NavPathStack 是页面结构事实,表示有哪些可返回的目的页。它们并非彼此自动同步。平行视界在不同设备和窗口条件下可有不同布局呈现,但不能由布局模式的变化随意新增一条相同的详情路由,否则从双栏切回单栏后很容易暴露重复返回的问题。我将设计目标收束成一个规则:同名详情页最多保留一个路由节点,重复点击同一商品不再发起导航动作,不同商品在已存在详情页时替换当前详情,而不是继续压栈。 这是一种适合商品浏览的业务取舍,不代表所有页面都应该采用替换。像结算流程、表单向导,保留历史详情可能有另外的产品意义。## 二、选中态必须能独立于页面实例存在先给选中态一个轻量账本。它只回答“当前是谁、是否真的发生选择变化”,不触碰 Navigation 对象,因而可以单独检验点击去重规则。ts// entry/src/main/ets/model/SelectionLedger.etsexport class SelectionLedger { selectedId: string = ''; revision: number = 0; suppressed: number = 0; select(id: string): boolean { if (id.length === 0) { return false; } if (this.selectedId === id) { this.suppressed++; return false; } this.selectedId = id; this.revision++; return true; }}````revision` 是业务事件版本,不是系统路由版本。它帮助开发者识别被旧请求覆盖的 UI 状态,但不提供跨设备自动同步。该类也没有声明响应式能力:把它直接放入页面普通字段,属性变化未必触发 ArkUI 刷新。示例 UI 需要用 `@State` 持有最终的显示值,并在点击处理方法里同步更新,而不是假定类的字段修改会自动重绘。相比把当前行号塞进路由 URL,稳定的业务 ID 更耐用。商品排序改变、分类筛选、插入新记录,都不会让 `SKU_204` 变成另一个商品;如果后台删除了商品,则需要用“内容不可用”的占位页回应,而不是保留一张空白详情。这里的键值仅是样例,不包含真实商品信息。## 三、路由动作需要以“已有详情页”为分岔点按 ArkUI 的 Navigation 机制,`NavPathStack` 用于管理目标页信息;`pushPathByName` 创建导航条目,`replacePathByName` 可替换栈顶条目,`getAllPathName` 可读取路径名称集合。下面演示业务侧如何把这三个动作组合起来。示例要求项目注册好名为 `ProductDetail` 的 `NavDestination` 构造器;代码片段只列核心部分,不把未提供的路由文件伪装成完整可直接运行项目。ts// entry/src/main/ets/pages/CatalogPage.ets 核心片段import { SelectionLedger } from ‘…/model/SelectionLedger’;import { ProductParams } from ‘./ProductDetail’;@Entry@Componentstruct CatalogPage { private stack: NavPathStack = new NavPathStack(); private ledger: SelectionLedger = new SelectionLedger(); @State selectedId: string = ‘’; @State revision: number = 0; @State suppressed: number = 0; openDetail(id: string): void { if (!this.ledger.select(id)) { this.suppressed = this.ledger.suppressed; return; } this.selectedId = id; this.revision = this.ledger.revision; const param: ProductParams = { id, revision: this.revision }; const names: string[] = this.stack.getAllPathName(); if (names.includes(‘ProductDetail’)) { this.stack.replacePathByName(‘ProductDetail’, param); } else { this.stack.pushPathByName(‘ProductDetail’, param); } } // build() 中的 Navigation(this.stack) 注册 ProductDetail,并将列表点击绑定到 openDetail。}这段逻辑包含一个必须说明的假设:`ProductDetail` 是当前导航流程的栈顶详情页。如果应用还允许从详情打开评价、购物车或嵌套详情,就不能简单因为路径里曾出现 `ProductDetail`,就替换当前栈顶;那会覆盖别的页面。真正的工程版本应在页面结构上明确层级,或结合 `NavPathStack` 当前栈顶名称、业务路由策略做更加严格的判断。此外,`replacePathByName` 不是“更新旧组件字段”的代名词。它改变的是路由栈条目,可能触发新组件构建与生命周期变化。对需要保持媒体播放、表单草稿或图片滚动位置的详情页,应该把待保留状态上移到业务仓库,而不是依赖视图实例一定不销毁。当前 Demo 刻意只保留 `selectedId` 和 `revision`,不承诺复用任何具体组件实例。演示 DevEco 图把目录、`openDetail` 核心分支、右侧双栏模拟器和底部日志放在同一画面。日志中的 `PUSH SKU_201`、`REPLACE SKU_204`、`SUPPRESS SKU_204` 是按操作序列写入的预期业务事件,并非真实设备打印。调试时可以为日志补上每条事件之前和之后的 `getAllPathName()` 结果,重点检查“替换前后没有额外增长一个详情节点”。## 四、返回时究竟应该清空哪个状态平行视界的返回行为往往比跳转更容易误写。用户在手机窄屏下看到的是“列表 → 详情”,系统返回一般回到列表;展开态里列表本来就常驻,返回操作如何表现,要与窗口配置、应用导航层级和用户预期统一。不能仅凭一张双栏截图,推断所有 HarmonyOS 设备都采用相同的系统返回策略。详情组件要读取参数,而不是依赖父页面当前列表索引。以下代码展示 `NavDestinationContext` 的参数接收位置,以及在详情内容关闭时把返回动作交给路由栈的方式。ts// entry/src/main/ets/pages/ProductDetail.ets 核心片段export interface ProductParams { id: string; revision: number; }@Componentexport struct ProductDetail { @State productId: string = ‘’; @State sourceRevision: number = 0; private pathStack: NavPathStack = new NavPathStack(); build() { NavDestination() { Column() { Text(商品:${this.productId}) Text(选择版本:${this.sourceRevision}) } } .onReady((ctx: NavDestinationContext) => { this.pathStack = ctx.pathStack; const p: ProductParams = ctx.pathInfo.param as ProductParams; this.productId = p.id; this.sourceRevision = p.revision; }) .onBackPressed(() => { this.pathStack.pop(); return true; }); }}```这里 onReady 负责在目的页创建时读取参数。如果未来改成单例移动到栈顶的路由模式,必须重新评估是否需要 onNewParam 等参数更新机制,不能因为某次导航表现正确就把生命周期当成永恒不变。自定义返回回调也要仔细检查:当页面还有未保存的数据、弹层或正在上传的任务时,返回可能要先发起业务确认,不应该直接 pop()。我更倾向于让选中态在返回后保持 SKU_204,便于用户回到左侧列表看到上次浏览位置;但详情是否仍可见应服从当前布局与 Navigation。换句话说,业务选择可以保留,路由详情节点可以消失。这正是“选中态”和“返回栈”需要分开存放的原因。## 五、把验收场景写成事件,而不是依赖设备印象构造操作顺序很简单:打开 SKU_201 时 PUSH;选中 SKU_204 时 REPLACE;再次点击 SKU_204 时 SUPPRESS。最终选中 SKU_204、版本 2、重复拦截 1、详情节点 1。这组数值适合用于代码审阅、日志样式以及演示图核对,却不能替代在平板、折叠屏和手机上的实际测试。
手机效果图采取纯屏幕设计:显示 SPLIT_20261017_03、已选商品 SKU_204、当前 DUAL 模式、详情节点 1、重复拦截 1、版本 2。其中 DUAL 是本 Demo 的布局标签,不是由 HarmonyOS 返回的系统枚举。图中的左右布局是为了说明产品设计意图,不能作为系统平行视界行为的实测截图。正式验收要至少覆盖四组路径:窄屏推入后返回,宽屏连续切换商品,窗口尺寸变化时从双栏恢复单栏,以及详情存在时来自通知或外部深链的新选择。每组都记录路由节点数与选中 ID;遇到布局切换时,检查导航栈保留与页面重建是否符合产品预期。若应用支持多窗口,不能把全局唯一 selectedId 无条件共享给所有窗口,需要明确每窗口还是每任务维护选择状态。## 六、把可证实的 API 和产品约束分别归档官方的 Navigation 资料支持 NavPathStack、pushPathByName、replacePathByName、getAllPathName,以及 NavDestination 的 onReady 参数接收。华为公开的平行视界示例也说明它涉及导航路由与多设备适配,而不是机械地把两个页面复制到一张大屏上。本文没有声称代码覆盖完整的平行视界配置、动态路由注册或设备分屏策略;这些都需要在对应 SDK 和产品目标设备上继续配置、验证。工程上还应注意两处边界。其一,重复点击屏蔽发生在业务选择层,不是系统替我们去重,因此外部跳转、收藏恢复等入口也应走同一个路由协调器。其二,延迟的数据请求应带上 revision:旧 SKU_201 的请求哪怕最后完成,也不能把 SKU_204 的详情内容覆盖掉。本例没有联网请求,因此不存在可报告的真实竞态发生率。如果要把这个方案投入生产,建议先完成可运行的静态路由注册,再补上页面卸载、返回拦截、异步请求取消和多设备形态回归。能从这轮演示直接得到的结论很有限,但也足够明确:列表的“当前选中”不等于 Navigation 的“当前栈顶”,连续选商品时先确定业务动作,再决定使用压栈还是替换,才不会在返回路径上留下难解释的重复详情页。资料参考: 华为《Navigation基础传参和接收示例》;《Navigation跨包路由》;官方《平行视界》样例目录。
更多推荐


所有评论(0)