HarmonyOS 7 DualCart 平行视界适配实录 04:EasyGo × 虚拟容器:商品比价双详情与分栏比例策略【鸿蒙心迹】

03 刚刚解决了商品视频和高清大图的沉浸路由:短时间离开 ProductList | ProductDetail,进入全屏或横屏,退出以后再把原来的双栏上下文恢复回来。

第四篇做的是另一种完全相反的交互。

用户不是想"离开购物流",而是想在原来的购物过程中临时比较两件商品:

text 复制代码
MateBook Air
VS
MatePad Pro

如果我直接再 push 一层详情,主路由会变成:

text 复制代码
ProductList
→ ProductDetail(product_1047)
→ ProductDetail(product_1051)

关闭比价以后还要倒退页面栈,而且原来的右栏语义也变得不清楚。

当前 HarmonyOS 官方"平行视界"示例把"开启虚拟容器能力"明确列为七个典型场景之一。2026 年 6 月开发者月刊还提到,HarmonyOS 7 的平行视界在大屏分栏基础上新增了 1:2、2:1 等比例配置,目的就是让不同业务能用更合适的分栏比例承载内容。

所以 04 的工程目标不是"再造两个 Detail",而是:

text 复制代码
保存当前主路由上下文
→ 打开一个独立比较容器
→ 容器里同时承载两个商品详情
→ 关闭容器
→ 原 ProductList | ProductDetail 原样恢复

本轮固定数据:

text 复制代码
taskId:
parallel_20261002_04

viewport:
824vp

mode:
VIRTUAL_CONTAINER

containerId:
compare_vc_01

ratio:
50:50

primaryProduct:
product_1047 / MateBook Air

compareProduct:
product_1051 / MatePad Pro

category:
电脑办公

mainRoute:
ProductList | ProductDetail

leftScrollOffset:
960vp

closeCompareCost:
36ms

status:
VIRTUAL_READY

双栏浏览阶段,主路由非常简单:

text 复制代码
左:
ProductList

右:
ProductDetail(product_1047)

比价如果直接使用同一个 rightPathStack:

text 复制代码
ProductDetail(1047)
→ ProductDetail(1051)

用户视觉上虽然看到了第二个商品,但系统和业务上已经失去了"主商品 / 对比商品"的区别。

更糟糕的是关闭比价时:

text 复制代码
到底 pop 一层
还是 replace 回 1047
还是重新加载 ProductList

每一种做法都在主路由上继续打补丁。

所以第四篇第一条规则是:

比价属于一个临时工作上下文,不应该改写原来的主购物上下文。

这和 03 的全屏快照很像,但方向相反:03 是单个沉浸页独占画面;04 是临时容器内部再并行两个详情。

二、应用侧只维护 VirtualCompareContext,不伪造系统配置字段

我没有在 ArkTS 页面里直接写某个 EasyGo 配置字段。

当前官方示例确认了"虚拟容器能力"这个场景,但具体配置应以当前示例工程和 SDK 配套规则为准。

DualCart 业务侧只维护一个稳定的数据模型:

ts 复制代码
export interface VirtualCompareContext {
  taskId: string
  containerId: string

  primaryProductId: string
  compareProductId: string

  categoryId: string
  ratio: '50:50' | '1:2' | '2:1'

  mainLeftRoute: string
  mainRightRoute: string

  leftScrollOffsetVp: number
}

export const compareContext:
  VirtualCompareContext = {
    taskId:
      'parallel_20261002_04',

    containerId:
      'compare_vc_01',

    primaryProductId:
      'product_1047',

    compareProductId:
      'product_1051',

    categoryId:
      'computer_office',

    ratio:
      '50:50',

    mainLeftRoute:
      'ProductList',

    mainRightRoute:
      'ProductDetail',

    leftScrollOffsetVp:
      960
  }

业务层只描述"我要比较什么"。

系统级虚拟容器配置由适配层完成。

这样后面官方配置字段变化,不会迫使 ProductDetail 改业务模型。

三、进入比价以前,主路由必须冻结成一份快照

第四篇新增 VirtualCompareCoordinator.ets。

第一段核心逻辑:

ts 复制代码
export class VirtualCompareCoordinator {
  private mainSnapshot:
    PairedStateSnapshot | null = null

  async open(
    primaryId: string,
    compareId: string
  ): Promise<void> {
    this.mainSnapshot =
      ParallelStateStore.shared()
        .snapshot()

    CompareSessionStore.shared()
      .open({
        containerId:
          'compare_vc_01',

        primaryProductId:
          primaryId,

        compareProductId:
          compareId,

        categoryId:
          'computer_office',

        ratio:
          '50:50'
      })

    AppStorage.setOrCreate(
      'virtualCompareMode',
      true
    )
  }

  async close(): Promise<void> {
    CompareSessionStore.shared()
      .close()

    AppStorage.setOrCreate(
      'virtualCompareMode',
      false
    )

    if (this.mainSnapshot !== null) {
      await ParallelRouteManager.shared()
        .restorePairedState(
          this.mainSnapshot
        )
    }

    this.mainSnapshot = null
  }
}

这段代码解决的是"比价关闭以后主页面到底恢复到哪"的问题。

恢复来源只有一个:

text 复制代码
mainSnapshot

而不是根据当前 Route 猜。

四、主商品和对比商品必须有明确角色

一开始我只是用:

text 复制代码
leftProduct
rightProduct

后来发现容器比例一变,left/right 语义会误导。

例如未来使用:

text 复制代码
1:2

并不代表右边就是主商品。

所以改成:

text 复制代码
primaryProduct
compareProduct

当前:

text 复制代码
primary:
product_1047
MateBook Air

compare:
product_1051
MatePad Pro

即使以后 2:1 或 1:2,业务语义也不变。

页面位置只是表现层。

五、分栏比例要由内容类型决定,而不是只看屏幕宽度

2026 年 6 月官方月刊已经提到 HarmonyOS 7 平行视界新增 1:2 和 2:1 分栏配置,说明系统适配本身开始支持更多业务比例。

DualCart 里我没有立刻把所有比价都用 50:50。

当前策略:

ts 复制代码
export class CompareRatioPolicy {
  resolve(
    primaryKind: string,
    compareKind: string
  ): '50:50' | '1:2' | '2:1' {
    if (
      primaryKind ===
        'SPEC_DENSE' &&
      compareKind ===
        'SPEC_DENSE'
    ) {
      return '50:50'
    }

    if (
      primaryKind ===
        'VISUAL' &&
      compareKind ===
        'SPEC_DENSE'
    ) {
      return '2:1'
    }

    return '1:2'
  }
}

本轮两件商品都属于参数密集型:

text 复制代码
MateBook Air
MatePad Pro

所以使用:

text 复制代码
50:50

这样两边规格表更容易直接对齐比较。

六、双详情不能共享同一个 selectedProductId

这是这篇最容易踩的状态坑。

主购物流只有一个:

text 复制代码
selectedProductId

比价里却有:

text 复制代码
primaryProductId
compareProductId

如果继续复用 selectedProductId,两边切换会互相覆盖。

所以 CompareSessionStore 独立保存:

ts 复制代码
export class CompareSessionStore {
  @State
  primaryProductId: string =
    'product_1047'

  @State
  compareProductId: string =
    'product_1051'

  @State
  ratio: string =
    '50:50'

  @State
  containerId: string =
    'compare_vc_01'

  open(
    context:
      VirtualCompareContext
  ): void {
    this.primaryProductId =
      context.primaryProductId

    this.compareProductId =
      context.compareProductId

    this.ratio =
      context.ratio

    this.containerId =
      context.containerId
  }

  close(): void {
    this.containerId = ''
  }
}

主流的 selectedProductId=product_1047 保持不动。

关闭比价以后,右栏自然还是 MateBook Air。

七、比价容器里两个详情页可以共享数据仓,但不能共享页面状态

两个商品都来自同一个 ProductRepository,没有必要各自重新请求全部基础数据。

但页面状态不能共享:

text 复制代码
主商品规格 Tab
对比商品规格 Tab

主商品图片索引
对比商品图片索引

主商品数量
对比商品数量

都应该独立。

所以当前 CompareDetailState:

ts 复制代码
export interface CompareDetailState {
  productId: string
  activeTab: string
  galleryIndex: number
  quantity: number
}

容器有两份实例。

这样左边切到"参数",不会把右边也切成"参数"。

八、关闭容器以后,恢复速度比打开速度更重要

比价打开时用户有心理预期:

text 复制代码
我要进入一个新模式

关闭时则希望马上回到原浏览位置。

本轮:

text 复制代码
closeCompareCost=36ms

恢复包括:

text 复制代码
关 CompareSession
恢复 ProductList | ProductDetail
恢复 selected product_1047
恢复 scroll=960vp

我把 36ms 单独记下来。

如果以后虚拟容器里加入更多卡片,关闭时间持续变长,就能从版本基线里看出来。

九、DevEco 图必须同时看到"容器状态"和"主路由快照"

开发图:

本轮 HiLog:

text 复制代码
taskId=parallel_20261002_04

openCompare
container=compare_vc_01

ratio=50:50

primary=
product_1047

compare=
product_1051

main routes preserved:
ProductList | ProductDetail

leftScrollOffset=960vp

closeCompare
cost=36ms

restore selected=
product_1047

status=
VIRTUAL_READY

如果比价页正常,但关闭后回到了 ProductList 首页,那虚拟容器本身可能没问题,错的是主路由快照。

所以调试一定要同时看两层。

十、最终运行图:两个详情并行,但主购物流没有被改写

运行图:

当前比较:

text 复制代码
MateBook Air
product_1047

VS

MatePad Pro
product_1051

容器:

text 复制代码
compare_vc_01
50:50

主上下文仍然是:

text 复制代码
ProductList | ProductDetail
selected=product_1047
category=电脑办公
scroll=960vp

关闭比价以后:

text 复制代码
ProductList | ProductDetail

继续浏览。

这就是虚拟容器对 DualCart 最有价值的地方:临时并行任务不污染主任务路由。

十一、为什么这一篇不直接做 1:2 / 2:1 的所有组合

官方当前已经明确 HarmonyOS 7 平行视界扩展了更多分栏比例。

但 DualCart 没有把这一篇写成"比例大全"。

比例的意义不是配置越多越高级,而是:

text 复制代码
内容有没有主次
两侧信息密度是否相当
是否需要突出视觉内容

当前双参数详情最适合 50:50。

未来如果:

text 复制代码
左边 3D 商品展示
右边规格参数

才更适合测试 2:1。

把比例和场景绑起来,比为了演示功能不停切比例更像真实产品。

十二、下一篇会进入"平行世界里的应用内分屏"

到 04 为止,DualCart 解决的仍然是:

text 复制代码
一个应用
一个主要购物任务
内部双栏和临时虚拟容器

05 会进入官方示例里的最后一类大场景:

text 复制代码
平行世界下触发应用内分屏

这时候会出现两个更复杂的问题:

text 复制代码
主购物任务
+
辅助购物任务

多个窗口 / 容器
+
共享商品状态

也就是说,05 不再只是 route replace,而是要处理任务级状态同步、窗口关系和关闭某一侧以后另一侧怎么继续。

十三、虚拟容器里的数据请求也要避免重复加载两份相同基础信息

比价一打开,最容易写成:

text 复制代码
左详情请求一次 product_1047
右详情请求一次 product_1051

看起来没问题。

但两个 Detail 组件还会分别请求:

text 复制代码
公共规格字典
品牌信息
配送规则
优惠规则

如果这些数据每个 Pane 都重新加载一遍,比价模式会比普通详情明显更重。

所以我把数据拆成两层:

text 复制代码
ProductRepository
→ 商品主体数据,可缓存

CompareDetailState
→ 每个 Pane 独立的 UI 状态

共享的是只读业务数据,独立的是:

text 复制代码
activeTab
galleryIndex
quantity
展开/收起状态

这样能避免"为了状态隔离,把所有网络请求也重复一遍"。

十四、比价商品失效时,容器必须能降级,而不是整个页面报错

还有一个真实场景:

用户选择:

text 复制代码
product_1047
product_1051

进入比价前,第二件商品刚好下架。

如果虚拟容器必须等两个 Detail 都成功,任何一个失败都会让整个 Compare 页面空白。

现在的降级规则是:

text 复制代码
primaryProduct 必须存在

compareProduct 不存在
→ 保留 primary
→ 右侧显示"商品已不可用"
→ 允许重新选择

只有主商品本身失效,才直接关闭比价并回到 ProductList。

这种处理很重要,因为"临时并行任务"不应该破坏主购物流。

即使对比失败,用户仍然应该回到原来的 MateBook Air 详情继续浏览。

十五、分栏比例切换时,两个 Detail 的内部状态不能跟着重置

第四篇提到 50:50、1:2、2:1 只是展示策略。

真正测试时我主动把比例做了来回切换:

text 复制代码
50:50
→ 2:1
→ 1:2
→ 50:50

要求:

text 复制代码
primary activeTab 不变
compare activeTab 不变
两边 galleryIndex 不变
quantity 不变

如果比例变化导致 Detail 组件被完整销毁重建,用户正在看的规格位置会跳。

所以 CompareDetailState 的生命周期高于具体布局组件。

布局只拿状态重新渲染,不重新初始化状态。

这一点和前面平行视界的 selectedProduct 连续性是同一种思路:结构变化不能顺手清业务状态。

十六、关闭比价时,我没有直接 pop 容器页面

关闭按钮最开始就是:

text 复制代码
pop()

问题是 pop 只处理可见路由,不知道主上下文快照是否已经恢复。

最后关闭顺序改成:

text 复制代码
冻结 CompareSession
→ 保存必要的 compare history
→ 关闭系统虚拟容器适配层
→ restore mainSnapshot
→ restore selected product
→ restore left scroll
→ 清 CompareSession

真正的 closeCompareCost=36ms 从"点击关闭"开始,到:

text 复制代码
ProductList | ProductDetail
product_1047
scroll=960vp

全部恢复完成为止。

这比统计一个容器动画结束时间更有业务意义。

十七、04 最后做了五轮循环,专门看主路由有没有被污染

比价不是一次性页面,所以我最后做了:

text 复制代码
打开比价
→ 关闭

连续五轮。

每一轮选不同的 compareProduct,但主商品一直保持:

text 复制代码
product_1047
MateBook Air

结束后检查:

text 复制代码
mainRouteDepth 不增长
selectedProduct 仍为 product_1047
leftScrollOffset 仍为 960vp
category 仍为 电脑办公
CompareSession 已关闭

第一版在第三轮以后,主路径里悄悄多了一个 ProductDetail。

原因就是某次对比商品点击仍然走了普通 rightPathStack。

把所有对比入口收口到 VirtualCompareCoordinator 后,这个问题才消失。

十八、虚拟容器真正的价值,是隔离"临时任务"和"主任务"

做完这一篇以后,我对虚拟容器的理解更明确了。

它不是"可以同时显示两个页面"这么简单。

对于 DualCart 来说,它提供的是一个很清楚的业务边界:

text 复制代码
主购物流:
找商品
看详情
加入购物车

临时任务:
对比两件商品

临时任务可以有自己的:

text 复制代码
两个 Detail
比例策略
UI 状态
错误降级

但关闭以后,主购物流完全不需要知道里面发生过多少次切换。

这种隔离能力比"多一个分栏效果"更有工程价值。

也正因为主任务和临时任务开始清楚分层,下一篇进入应用内分屏时,才有基础继续讨论:

text 复制代码
两个真正并行的购物任务
如何共享商品数据
又如何避免互相覆盖状态

十九、比价入口也要做幂等,避免重复创建同一个容器

比价按钮连续点击两次时,早期版本会创建两个相同的 compare session。

页面看起来只有一层,但关闭一次以后,后台状态还残留一份。

现在 openCompare() 会先检查:

text 复制代码
containerId
primaryProductId
compareProductId
active

如果同一个 compare_vc_01 已经 ACTIVE,就直接返回当前 Session,不再重复初始化。

如果用户换了 compareProduct,则只更新对比商品,不重新创建主容器。

这条保护让 5 轮循环测试稳定很多,也避免后面做应用内分屏时出现"一个临时任务被创建两份"的隐蔽问题。

二十、商品比价关闭后,历史可以保留,但活跃状态必须清空

我还保留一份轻量历史:

text 复制代码
lastCompare:
product_1047 vs product_1051

lastRatio:
50:50

它用于下次快速重新进入比价,不参与当前主路由。

真正活跃的:

text 复制代码
containerId
activeTab
galleryIndex
quantity

关闭后必须清空。

这样"历史记录"和"活跃容器状态"不会混在一起。后面即使用户从收藏页重新发起比价,也不会继承上一轮已经无效的 UI 对象。

参考资料

相关推荐
李游Leo1 小时前
HarmonyOS 7 PixelBridge 原生库适配实录 06:Release 性能基线、资源释放、包体积与工程化验收【鸿蒙心迹】
华为·harmonyos
传奇开心果编程2 小时前
【ArkUI进阶练中学】第11课:安全与合规进阶
学习·ui·华为·harmonyos
熊猫钓鱼>_>3 小时前
Kotlin Multiplatform for OpenHarmony 实战:为 Reaktive 实现响应式原语适配(完整版 · 含摘要目录与技术图表)
华为·kotlin·大模型·ai编程·harmonyos·适配·reaktive
李游Leo6 小时前
HarmonyOS 7 + Spatial Recon Kit-C++:3DGS 高斯参数的非有限值隔离与可渲染性门禁【鸿蒙心迹】
开发语言·c++·3d·harmonyos
熊猫钓鱼>_>7 小时前
Kotlin Multiplatform for OpenHarmony 实战:为 MVIKotlin 实现单向数据流适配
华为·ai编程·鸿蒙·openharmony·适配·数据流·mvkotlin
传奇开心果编程7 小时前
【ArkUI进阶练中学】第12课:应用架构演进与遗留系统迁移
学习·ui·华为·harmonyos
传奇开心果编程9 小时前
【ArkUI进阶练中学】第13课:元服务与卡片开发
学习·ui·华为·harmonyos
李游Leo10 小时前
HarmonyOS 7 InsightBoard 多形态适配实录 06:ArkUI × 多设备回归:布局抖动、监听释放与全形态验收【鸿蒙心迹】
回归·kotlin·harmonyos