HarmonyOS 7 DualCart 平行视界适配实录 01:Navigation × EasyGo:主页关联页与列表详情双栏链路【鸿蒙心迹】
前一条 InsightBoard 连载已经把手机、折叠屏、平板、PC 的响应式布局做完,这次我换了一条明显不同的主线:不再由应用自己画两栏,而是把 Navigation 路由关系整理成平行视界可以识别和利用的页面关系。
Demo 叫 DualCart,是一个购物类应用。手机上仍然按普通页面栈运行;折叠屏展开态和平板大屏场景下,希望用户点击商品以后,左侧继续保留列表,右侧打开详情。
这件事看起来和 Navigation(mode: Split) 很像,但工程目标不一样。
普通 Split 是应用自己决定“现在画成两栏”;平行视界更强调系统级的应用内分屏体验,当前 HarmonyOS 官方示例也采用 Navigation 路由购物应用 + 配置 的方式实现,并覆盖主页关联页、路由模式、过渡页、请求全屏、横屏、虚拟容器以及平行世界下应用内分屏等典型场景。
所以这一期不追求把所有配置一次写完,只先把最短链路固定住:
Home
→ Category
→ ProductDetail
展开大屏后:
左侧:ProductList
右侧:ProductDetail
本轮统一数据:
taskId: parallel_20261002_01
device:
Foldable / Unfolded
viewport:
824vp
leftPane:
304vp
rightPane:
520vp
splitRatio:
37 : 63
routeStack:
Home → Category → ProductDetail
pairedRoutes:
ProductList | ProductDetail
selectedProduct:
product_1042 / Watch 5
status:
PAIRED_READY

一、先把“分栏”理解成路由关系,而不是 Row
最开始我确实想过最简单的方案:
Row() {
ProductList()
ProductDetail()
}
页面马上能做出来,而且截图也很好看。
问题出在手机。
手机仍然需要:
列表
→ 点击
→ 跳详情
如果大屏再维护一套 Row 逻辑,就会很快出现两条路由体系:
手机:
Navigation / NavDestination
大屏:
Row / 手动组件切换
同一个商品详情要维护两份进入方式,返回、Deep Link、状态恢复都容易分叉。
所以 DualCart 这一系列把 Navigation 作为唯一业务路由。
大屏只是让系统和配置知道:
哪些页面可以形成主从关系
哪些页面应该落到另一侧
业务本身仍然是一条路由树。
二、Navigation 路由名必须先稳定下来
第一篇我先把路由名字从页面文件名里抽出来。
这段代码解决的是“后面配置写了 ProductDetail,代码却改名成 ProductInfoPage”的问题:
export enum ShopRoute {
HOME = 'Home',
CATEGORY = 'Category',
PRODUCT_LIST = 'ProductList',
PRODUCT_DETAIL = 'ProductDetail'
}
export interface ProductRouteParam {
productId: string
categoryId: string
}
export class ShopRouteNames {
static all(): string[] {
return [
ShopRoute.HOME,
ShopRoute.CATEGORY,
ShopRoute.PRODUCT_LIST,
ShopRoute.PRODUCT_DETAIL
]
}
}
路由名一旦进入平行视界配置,就不应该继续被当作“页面内部字符串”随便修改。
我现在把它看成一种对外契约。
文件名可以从 ProductDetailPage.ets 改成别的,组件可以重构,真正进入配置和 Navigation 的 route name 尽量稳定。
三、系统路由表和业务参数先保持一致
当前 HarmonyOS 的 Navigation 支持系统路由表,通过 router_map.json 配置后可以按需加载页面;官方当前文档也明确推荐 Navigation 作为主路由能力。
DualCart 当前的路由表概念保持简单:
{
"routerMap": [
{
"name": "Home",
"pageSourceFile": "src/main/ets/pages/HomePage.ets",
"buildFunction": "HomeBuilder"
},
{
"name": "ProductList",
"pageSourceFile": "src/main/ets/pages/ProductListPage.ets",
"buildFunction": "ProductListBuilder"
},
{
"name": "ProductDetail",
"pageSourceFile": "src/main/ets/pages/ProductDetailPage.ets",
"buildFunction": "ProductDetailBuilder"
}
]
}
具体字段仍以当前 SDK 和工程路由表要求为准,这里真正想强调的是:
Navigation route name
平行视界配置里的页面语义
日志里的 route name
测试用例里的 route name
要保持同一套名字。
否则调试时会出现“页面明明打开了,但配置没有命中”的错觉。
四、主页和关联页,不等于把所有详情都固定到右侧
第一版策略里我把 Home 看成主页面,把 ProductDetail 看成关联页。
但商品业务还有:
Home
→ Category
→ ProductList
→ ProductDetail
如果简单理解成“详情永远右侧”,Category 和 ProductList 的关系就会变得模糊。
所以应用内部先增加一层 ParallelPolicy,只描述业务角色,不替代系统配置:
export type ParallelRole =
'PRIMARY' |
'ASSOCIATED' |
'NORMAL'
export class ParallelPolicy {
role(route: string): ParallelRole {
if (
route === ShopRoute.HOME ||
route === ShopRoute.CATEGORY ||
route === ShopRoute.PRODUCT_LIST
) {
return 'PRIMARY'
}
if (route === ShopRoute.PRODUCT_DETAIL) {
return 'ASSOCIATED'
}
return 'NORMAL'
}
canPair(
leftRoute: string,
rightRoute: string
): boolean {
return (
this.role(leftRoute) === 'PRIMARY' &&
this.role(rightRoute) === 'ASSOCIATED'
)
}
}
这个 Policy 不是 HarmonyOS 平行视界 API。
它只是 DualCart 自己的业务镜像,用来保证“配置语义”和“业务代码理解”一致。
后面测试时,如果系统显示和 Policy 预期不一致,就可以优先检查配置,而不是把问题全部推给页面。
五、选中商品不能由右侧详情自己拥有
做成双栏以后,我遇到的第一个状态问题不是路由,而是选中态。
左侧点击 product_1042,右侧详情打开。
如果 selectedProductId 只存在 ProductDetail 页里,左侧列表不知道哪一项应该高亮。
反过来,如果列表自己保存,右侧被系统恢复时又可能拿不到。
所以状态上移到 Store:
export class ProductSelectionStore {
private selectedProductId: string =
'product_1042'
get(): string {
return this.selectedProductId
}
select(id: string): void {
this.selectedProductId = id
AppStorage.setOrCreate(
'selectedProductId',
id
)
}
}
左侧和右侧都消费同一个 ID。
这也解释了为什么本轮截图里:
selectedProduct:
product_1042
不属于某一栏,而属于整个购物上下文。
六、点击商品仍然走 Navigation,不写“大屏特例”
列表页点击:
private openProduct(
productId: string
): void {
ProductSelectionStore.shared()
.select(productId)
this.pathStack.pushPathByName(
ShopRoute.PRODUCT_DETAIL,
{
productId,
categoryId: this.categoryId
} as ProductRouteParam
)
}
没有:
if 平行视界
→ 更新右侧组件
else
→ push 页面
这是第一篇我最在意的地方。
业务动作只有一次。
系统是否把关联页放到右侧,是适配层和配置决定的。
这样手机模式、平板模式、折叠屏模式之间不会产生两套购买链路。
七、分栏比例先记录,不把它写成业务条件
当前运行图里:
left=304vp
right=520vp
split=37:63
这组值是当前 DualCart 在 824vp 视口下的结果,用于调试和视觉验收。
它不是:
if ratio == 37:63
业务才能运行
商品列表只要满足最小可用宽度即可。
详情区域更宽,是因为电商详情需要:
商品图
价格
规格
操作按钮
列表只需要:
缩略图
标题
价格
选中态
比例是体验参数,不应该渗进业务逻辑。
八、DevEco 里重点看“路由对”,不是看两栏是否存在
本轮 DevEco 图:

HiLog 固定记录:
taskId=parallel_20261002_01
route:
Home → Category
paired route:
ProductList | ProductDetail
selected:
product_1042
viewport:
824vp
left:
304vp
right:
520vp
split:
37:63
status:
PAIRED_READY
只看“两栏出来了”是不够的。
如果左边实际是 Category,右边是 ProductDetail,但业务预期要求 ProductList 保持,就说明主从关系还没整理好。
所以日志必须同时记录 route role 和 selected state。
九、运行图第一次证明“一次点击、两种体验”
最终运行图:

当前大屏:
左:
ProductList
右:
ProductDetail(product_1042)
路由栈:
Home → Category → ProductDetail
手机上同一条点击代码仍然进入普通详情页面。
展开设备上,同一条路由在平行视界配置下形成应用内分屏。
这才是我想要的“一次开发”——不是一套页面在不同设备上强行复制布局,而是同一条业务动作被不同设备能力以更合适的方式呈现。
十、第一篇我故意没做“所有页面都并排”
平行视界很容易陷入一个误区:大屏就是能同时放两个页面,于是每个跳转都想占右栏。
实际产品里有很多页面并不适合:
登录
支付
隐私确认
图片沉浸预览
全屏视频
一次性中间处理页
这些会在后面几篇分别处理。
第一篇只建立最稳定的一对:
ProductList
+
ProductDetail
主页、分类、选中态和路由名固定以后,再继续加路由模式才不会越配越乱。
十一、当前官方示例为什么值得作为这条系列的基线
HarmonyOS 官方样例页在 2026-08-29 更新的“平行视界”示例里,明确使用 Navigation 路由的购物类应用 作为载体,并列出七个典型场景:
主页与关联页
路由模式
过渡页面
请求全屏
请求横屏
虚拟容器
平行世界下应用内分屏
这和 DualCart 的业务非常接近。
所以这个系列不沿用旧 Android Activity Codelab 去硬套,而是以当前 HarmonyOS Navigation 示例作为工程基线,再用 EasyGo / 平行视界的配置思路去组织页面关系。
十二、下一篇真正的问题是“中间页到底占不占一栏”
第一篇结束以后,最自然的下一步不是换商品。
而是下面这条路由:
ProductList
→ ProductBridge
→ ProductDetail
ProductBridge 是一个中间过渡页面:
参数补全
埋点
权限检查
跳转整形
它存在,但不应该长期占据右栏。
如果配置和业务都把它当正常关联页,用户会看到:
左边商品列表
右边一闪而过过渡页
再切到商品详情
体验很差。
所以下一篇会专门做路由模式、过渡页语义、右栏替换、左栏滚动与选中态连续性,并继续使用同一个 DualCart,不重建 Demo。
十三、配置层和业务层必须做一次“对表”检查
平行视界适配有一个很实际的风险:代码改了,配置没改;或者配置改了,测试工程还在跑旧路由。
DualCart 现在启动时会做一次轻量校验,把业务侧知道的路由角色输出出来:
export class ParallelConfigValidator {
validate(
expectedPairs: Array<[string, string]>
): string[] {
const errors: string[] = []
expectedPairs.forEach(([left, right]) => {
if (!ParallelPolicy.shared()
.canPair(left, right)) {
errors.push(`${left} -> ${right}`)
}
})
return errors
}
}
它当然不能替系统验证真正的平行视界配置是否生效,但至少能提前发现应用内部对路由角色的理解已经漂移。
例如业务重构以后把 ProductList 拆成 SearchResult 和 CategoryResult,如果仍然只把旧路由当 PRIMARY,就应该在进入真机联调前先补齐业务角色和测试用例。
我现在把这类问题当成“配置漂移”,而不是页面 Bug。
十四、手机回退路径必须始终可用
适配大屏时最容易发生的一件事,是为了让双栏成立,反而把手机单栏流程改坏。
所以 01 的验收不是只在展开折叠屏上看一遍。
同一个 product_1042 至少跑两条路径:
Phone / COMPACT
ProductList
→ ProductDetail
→ Back
→ ProductList
以及:
Foldable / Unfolded
ProductList | ProductDetail
→ Back from detail
→ ProductList remains
两条路径共用同一个 openProduct()。
如果为了平行视界新增了 openProductForLargeScreen(),我会直接认为架构开始分叉。
这也是为什么第一篇一直把“业务动作只有一次”放在前面说。
十五、右栏首屏不能依赖用户先点击一次
另一个容易忽略的问题是:进入大屏页面以后,左侧有 24 个商品,右侧却是空白。
如果用户必须先点一条,双栏才有内容,第一屏会显得很浪费。
DualCart 当前策略是:
如果已有 selectedProductId
→ 恢复该商品
否则
→ 使用列表首个可用商品
但“默认第一条”只发生在没有历史选中的情况下。
代码示意:
ensureInitialSelection(
products: Product[]
): string | null {
const saved = ProductSelectionStore.shared().get()
if (saved.length > 0 &&
products.some((item) => item.id === saved)) {
return saved
}
if (products.length === 0) {
return null
}
ProductSelectionStore.shared()
.select(products[0].id)
return products[0].id
}
这样列表数据刷新以后,也不会因为第一条商品换了就强制覆盖用户原来的选择。
十六、这一篇的验收清单最终固定成 8 条
为了避免“看着像对了”就结束,我把 01 验收固定成:
1. 手机单栏路由正常
2. 折叠屏展开能形成列表 / 详情组合
3. selectedProduct 左右一致
4. 页面路由名与配置语义一致
5. ProductDetail 不重复创建业务 Store
6. 列表最小宽度下仍可操作
7. 回退后左栏仍保持
8. 不支持平行视界时自动退回普通 Navigation
最后一条很重要。
平行视界是增强体验,不应该成为应用可用性的前置条件。
如果设备形态、系统状态或配置没有进入平行视界,DualCart 必须仍然是一套完整的普通购物应用。
十七、最后再补一条异常路径:列表为空时不要强行建立配对
购物类应用并不是永远有商品。网络失败、筛选结果为空、商品下架,都可能让左侧暂时没有可选项。
如果这时仍然按照“进入大屏就一定要有右栏详情”的逻辑硬推一个 ProductDetail,页面会出现无效 productId,甚至把上一次残留商品继续显示出来。
所以当前策略是:
列表为空
→ 左侧展示空状态
→ 右侧展示占位说明
→ 不创建 ProductDetail 路由
列表恢复
→ 再执行 ensureInitialSelection()
这条边界看起来和 EasyGo 无关,却直接决定平行视界在异常数据场景下会不会留下“左右不一致”的假状态。
真正稳定的双栏不是任何时候都塞满两个页面,而是每一栏都能解释自己为什么存在。
十八、配置生效也要留一个可见的降级标记
DualCart 最后还保留 presentationMode 日志。进入平行视界时记录 PARALLEL_VIEW,普通单栏记录 SINGLE_STACK。这样遇到“大屏上为什么没有双栏”时,可以先确认系统能力和配置是否真的生效,再去查 ProductList 页面。
这能避免把配置问题误判成 ArkUI 渲染问题,也让后续 03、04 的全屏和虚拟容器测试有统一入口。
参考资料
- HarmonyOS 7 官方示例:平行视界:https://developer.huawei.com/consumer/cn/samples/
- 2026 年 7 月开发者月刊:平行视界一键适配:https://developer.huawei.com/consumer/cn/monthly/202607
- HarmonyOS 多设备适配指南:https://developer.huawei.com/consumer/cn/multidevice/adaptive-apps/
- Navigation 分栏开发:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkts-navigation-split-mode
更多推荐




所有评论(0)