ArkUI Navigation.Auto:商品页详情栈与双栏空态【鸿蒙心迹】
同一个商品列表放到窄屏和宽屏上,开发者容易先盯着“右边能不能腾出一块详情区”。真正需要保住的是用户的上下文:选中了哪件商品,路由栈里现在有什么,宽度变化后返回按钮会指向哪里。窗口从宽到窄,再从窄到宽,单纯给列表加一个 if (width > 600) 并不能自动管理这些问题。
这篇用 ShelfRoute 做一个可拆解的小样例。它有三件固定商品:SKU-204“轻薄台灯”129元、SKU-319“桌面收纳”79元、SKU-502“无线鼠标”169元;当前选中 SKU-319。约定宽窗口824vp显示双栏,窄窗口412vp显示单栏,当前详情栈包含一个ProductDetail。这组数字是用于设计与配图对应的假设场景,不是实测设备宽度或性能报告。

一、别把双栏排版和路由栈当成一个状态
列表内容属于导航页(NavBar),商品详情属于子页面(NavDestination),NavPathStack 管理详情页面进出。宽度影响的是同一个导航容器的呈现模式,不是让业务另造两套互不相干的路由。若在“展开时”重新 push 一次详情,就可能把一条商品变成两层相同页面;缩回窄屏时连续按返回,用户会看到重复详情。
华为的 NavigationMode.Auto 会根据导航容器宽度选择Stack或Split;对于API version 10及以后,文档描述的常见阈值是600vp。本文演示824vp → Split、412vp → Stack。这里不手动测量折叠角度,不读取设备名称去猜测宽度,也不把这个ArkUI通用布局功能说成应用已接入 EasyGo平行视界。后者涉及独立能力和产品适配规范,需要按自己的官方文档另外实现。
再确认一个“看起来多余”的空态:宽屏时如果NavPathStack里没有任何详情页,右侧究竟显示什么?空白不是必然错误,但业务页面应明确告诉用户“从左侧选择商品”,而不应该把上一次商品截图伪装成仍选中。新版Navigation提供分栏空栈占位能力;为了兼顾不同SDK基线,本文将占位作为设计要求,未在核心片段中使用需要进一步核定版本的接口。
二、首先固定商品ID与导航参数
这里要解决的是“点商品时用列表下标还是稳定ID”。排序、筛选或商品下架都可能改变列表下标,因此路由参数只传SKU,详情页再通过ID查找数据。用展示文本或价格定位页面,迟早遇到同名商品和促销改价。
export class ProductParam {
sku: string;
constructor(sku: string) { this.sku = sku; }
}
export class ProductItem {
sku: string; title: string; price: number;
constructor(sku: string, title: string, price: number) {
this.sku = sku; this.title = title; this.price = price;
}
}
export const PRODUCTS: ProductItem[] = [
new ProductItem('SKU-204', '轻薄台灯', 129),
new ProductItem('SKU-319', '桌面收纳', 79),
new ProductItem('SKU-502', '无线鼠标', 169)
];
ProductParam 的职责是路由参数协议,不是网络数据模型。为了避免JSON类型在ArkTS严格类型检查下出现隐式推断,示例使用显式类。真实详情页需要按SKU查本地索引或者请求服务端,再处理商品已删除、库存变更以及API出错;这些状态不要靠“刚刚列表里能点”来保证。
三、Navigation只负责呈现,选中行为只触发一次
页面布局代码主要展示三件事:使用一个NavPathStack,将导航容器设置为Auto,在宽窄切换回调里只更新显示状态,不重复往栈里塞页面。示例中 openDetail 由商品卡片的点击事件触发;窗口变化不会调用它。这样可以让双栏变化与业务路由解耦。
// ShelfRoutePage 内的核心成员及布局片段
private stack: NavPathStack = new NavPathStack();
@State modeText: string = 'STACK';
@State selectedSku: string = 'SKU-319';
openDetail(sku: string): void {
this.selectedSku = sku;
this.stack.pushPath({ name: 'ProductDetail', param: new ProductParam(sku) });
}
build() {
Navigation(this.stack) {
Column({ space: 12 }) {
Text('ShelfRoute 商品目录')
ForEach(PRODUCTS, (item: ProductItem) => {
Button(item.title + ' ¥' + item.price)
.onClick(() => this.openDetail(item.sku))
}, (item: ProductItem) => item.sku)
}.padding(16)
}
.navDestination(this.buildDestination)
.mode(NavigationMode.Auto)
.onNavigationModeChange((mode: NavigationMode) => {
this.modeText = mode === NavigationMode.Split ? 'SPLIT' : 'STACK';
})
}
这段是页面类内的核心摘录,不是可以独立粘贴运行的全部工程文件:buildDestination 在下一段定义,项目仍需配置入口页和相关资源。onNavigationModeChange 自API version 11开始提供,官方说明适用于Stage模型;初次显示与单双栏变化都可能触发。回调里做日志和轻量状态更新合适,不应该每次都重建整个栈。
还有一个容易出现的业务重复:用户连续点两次SKU-319,pushPath会产生两个同名详情。生产环境可在应用层检测当前路由是否同一SKU,决定忽略点击、替换当前页或允许新实例;不能不加说明地把所有详情强制变成单实例,因为某些对比流程确实需要多实例。本文讨论的是同一商品在形态切换时不重复压栈,不声称点击去重已完全实现。

四、详情页对无效SKU必须有退路
只展示正常商品会让演示太理想。宽屏时详情可能一直停留;如果用户通过其他入口修改了商品集合,旧SKU可能查不到,不能让界面停在一个无意义的空白页。下段通过导航目标页的onReady取得参数,再以稳定ID查找;找不到时明确显示“商品已下架或不可用”。这里不调用价格API,使用本地演示数组。
// ShelfRoutePage 内的目标页构建函数片段
@State detailSku: string = '';
@Builder
buildDestination(name: string) {
if (name === 'ProductDetail') {
NavDestination() {
Column({ space: 16 }) {
Text(this.detailSku.length > 0 ? this.detailSku : '无效商品')
Text(PRODUCTS.some((p: ProductItem) => p.sku === this.detailSku)
? '商品详情已定位' : '商品已下架或不可用')
Button('返回列表').onClick(() => { this.stack.pop(); })
}.padding(20)
}
.title('商品详情')
.onReady((ctx: NavDestinationContext) => {
const param = ctx.pathInfo.param as ProductParam;
this.detailSku = param?.sku ?? '';
})
}
}
onReady 在目标页面可以取得pathInfo.param,但页面反复进入时仍应考虑参数变更、缓存复用和异步详情请求的取消。这里把detailSku放在宿主组件是为了展示流程,复杂项目宜让独立详情组件持有自己的加载状态,避免多个详情页共享一个可变字段。本例没有读真实库存,所以不能把“本地有SKU”当成“商品可购买”。
五、把824vp到412vp的转换拆成两次检查

本轮两张技术配图使用同一业务信息:ShelfRoute,当前SKU-319,商品名桌面收纳,价格79元。IDE图用宽屏824vp的 SPLIT 设计状态说明“左列表/右详情”怎样同时存在;纯手机屏幕图使用412vp的 STACK 设计状态说明“详情单独显示,返回后仍见列表”。日志示例约定19:40:12为Split、19:40:14为Stack,栈里始终只有一个详情路径。数字仅用于演示验收,不是声称系统在两秒内完成了真实形态切换。
第一个检查针对宽屏:选择SKU-319后看详情,确认右侧不为空,并记录当前栈。第二个检查针对缩窄:确认同一路由被单栏展示,返回按钮对应同一个详情实例,没有因为模式变化而额外压栈。再展开时,应检查选中商品是否仍由业务状态确定,而不是碰巧显示上一次布局的视觉残影。
边界测试还要包含快速连续切换宽度、触发返回与切换同时发生、详情加载失败、商品从目录中移除四种情况。不要使用“未出现崩溃”作为唯一验收标准;要同时检查路径栈长度、当前SKU和视觉状态。对于更复杂的导航路径,可在业务层记下路由操作来源,而不是试图从动画完成顺序反推业务状态。
还有一种不是布局引起的异常:从通知或应用链接直接进入某件商品详情,此时左侧列表可能尚未加载。正确做法是先用SKU恢复详情路径,再异步补全目录的选中态,不能假设用户必然从左侧列表点进来。若异步加载期间恰好切换模式,详情栈不应被清空;多窗口实例也应各自维护导航控制器,避免一个窗口的返回操作影响另一窗口。
六、这套方案能做什么,不能替代什么
NavigationMode.Auto适合让导航结构跟着可用宽度变化,特别是列表—详情一类主从关系清楚的页面。它不会为业务自动维护商品库存,不会替应用保存离线详情,也不会处理第三方路由回跳协议。固定600vp阈值属于官方所述版本和默认布局条件下的规则;如果设置了特殊导航栏宽度、最小内容宽度或者切换不同API版本,应重新对照文档和实测行为。
本篇交付的是清楚的工程设计与核心代码片段。DevEco与手机配图只是用于解释状态的合成演示,没经过本轮模拟器编译或设备测试。正式集成时要补齐多页面结构、路由参数隔离、详情请求取消、空态资源、焦点与无障碍返回策略。先让路径和状态归属明确,再讨论过渡动画和视觉质感,能够减少多形态适配中最难发现的“看着切换成功,其实栈乱了”的问题。
官方资料: Navigation组件参考 · Navigation分栏开发 · Navigation组件导航指南。
更多推荐




所有评论(0)