
折叠屏应用的分栏适配最容易被忽略的并不是左右两列的宽度而是窗口形态变化后用户原来选中的那条业务记录还在不在。界面从单栏切回双栏列表可以重新排布详情却不应该被重新压栈。本文用一个工单工作台说明这层边界布局模式交给 Navigation业务选择交给状态对象页面栈只表达导航历史。一、先把可复现的问题界定清楚示例工程叫CaseSplit入口页为Workbench.ets详情页为CaseDetail.ets。列表里有C-201至C-204等模拟工单本篇固定观察C-204名称是“设备验收”状态“待交付”校验项5/5。图片使用 10:24 的静态演示时刻所有数字都是为说明逻辑而设定的样例不是折叠屏性能测量值。期望的交互非常具体窄窗口点击C-204后进入详情展开窗口时变成导航栏加详情区再折叠回窄窗口选中 ID 不能丢也不能多出第二个相同的CaseDetail。如果用户只是重复点同一项业务上应视为重复选择而不是一次新的导航动作。这与此前按 GridRow/GridCol 调整工单栅格的工作不同。栅格解决“控件如何占位”这里讨论“路由栈和选中态是不是同一回事”。列表排布完全正确也可能因多次pushPath导致返回按钮要按两遍或者切换一条工单后看到了上一条的旧数据。二、让 Navigation 管显示让业务对象管选择HarmonyOS ArkUI 的Navigation支持单栏、分栏和自适应模式配合NavPathStack管理NavDestination。官方文档写明自适应由 Navigation 宽度驱动不能据此推断铰链状态也不能将其当作“设备是否展开”的传感器。此处选择NavigationMode.Auto不另写一套设备型号判断。设计里只有两种稳定条件没有选中工单时栈可以为空有选中工单时当前业务详情只保留一条。切换不同工单采用替换栈顶而非不断压入同名目的页。为避免组件重建导致本地字段重复初始化控制对象由宿主入口持有业务状态是否跨 Ability、跨进程恢复则属于另一个持久化问题不由本篇自动承诺。下面的状态门卫处理的是同一工单的重复进入以及不同工单的替换。它只依赖已核对的NavPathStack.size()、pushPath()和replacePath()。返回false表示不需要进行导航但不等同于“数据已经同步到服务端”。// entry/src/main/ets/model/CaseSelectionGate.ets export class CaseSelectionGate { private stack: NavPathStack; private selectedId: string ; constructor(stack: NavPathStack) { this.stack stack; } open(id: string): boolean { if (id.length 0) { return false; } if (id this.selectedId this.stack.size() 1) { console.info(CaseSplit duplicate${id} ignored); return false; } const next { name: CaseDetail, param: id }; if (this.stack.size() 0) { this.stack.replacePath(next, false); } else { this.stack.pushPath(next, false); } this.selectedId id; console.info(CaseSplit select${id} stack${this.stack.size()} modeAUTO); return true; } }selectedId是业务标识stack.size()是路由数量两者刻意不混为一个字段。replacePath处理的是路由实例替换不保证详情里的网络请求自动取消。使用false关闭本次转场动画只是为了容易观察状态并非适配所必需。在更复杂的应用中已有多个合法业务详情页时不能直接套用“总是替换栈顶”的规则应按页面所有权和用户的返回预期调整。三、在入口处连上列表、路由与详情Workbench.ets需要解决第二个问题业务对象的open()返回后ArkUI 的选中高亮也要改变反过来页面只是从双栏变单栏时不能再次主动调用open()。下面保留与问题直接相关的片段列表数据在演示里写为静态项生产工程应交给仓储层加载。// entry/src/main/ets/pages/Workbench.ets import { CaseSelectionGate } from ../model/CaseSelectionGate; import { CaseDetail } from ./CaseDetail; Entry Component struct Workbench { private stack: NavPathStack new NavPathStack(); private gate: CaseSelectionGate new CaseSelectionGate(this.stack); State selectedId: string ; private selectCase(id: string): void { if (this.gate.open(id)) { this.selectedId id; } } Builder private destination(name: string, param: Object) { if (name CaseDetail) { NavDestination() { CaseDetail({ caseId: param as string }) }.title(工单详情) } } build() { Navigation(this.stack) { Column({ space: 12 }) { Text(工单工作台).fontSize(23).fontWeight(FontWeight.Bold) Button(C-201 网络设备巡检).onClick(() this.selectCase(C-201)) Button(C-204 设备验收).onClick(() this.selectCase(C-204)) Text(当前选中${this.selectedId || 无}) }.padding(16) } .mode(NavigationMode.Auto) .navDestination(this.destination) .title(CaseSplit) } }实际工程需要把CaseDetail组件按项目结构正确导入此片段着眼于导航组合省略全量样式、列表仓储和资源文件。API 枚举应写NavigationMode.Auto不是大写AUTO日志里的modeAUTO是开发者自定义的展示字符串并非 SDK 枚举常量。即使开发画面里放了红色标注也不能拿示意截图代替这项核对。四、详情组件只消费参数不掌握窗口切换第三个关注点是详情销毁与数据回流。NavDestination负责承载子页用户点击列表时传入 ID详情组件只把 ID 显示出来不在aboutToAppear里重复入栈。官方明确提示不要在该时机贸然进行栈操作否则容易出现构建时序问题。// entry/src/main/ets/pages/CaseDetail.ets Component export struct CaseDetail { Prop caseId: string ; State detailState: string 待交付; aboutToAppear(): void { console.info(CaseSplit detail${this.caseId} onAppear); } aboutToDisappear(): void { // 此处释放属于详情页的计时器、订阅或异步回调句柄。 console.info(CaseSplit detail${this.caseId} release); } build() { Column({ space: 12 }) { Text(工单 ${this.caseId}).fontSize(22) Text(设备验收) Text(状态${this.detailState}) Text(校验项5/5) }.padding(20) } }演示刻意没有发起网络访问如果后来加入远端详情请求应通过请求序号或取消令牌阻止旧响应覆盖新 ID。aboutToDisappear的日志也只是生命周期观察点不能在此修改即将销毁组件的响应式状态更不能断言应用切后台就一定触发组件销毁。五、用状态序列验收而不是只看界面这次约定的演示序列是进入工作台选择C-204重复选择一次再从窄窗口切换到较宽布局。期望日志出现CaseSplit selectC-204 stack1 modeAUTO随后出现CaseSplit duplicateC-204 ignored窗口重排不会新增select日志。C-204仍指向“设备验收”校验项保持5/5。这些是供实际测试验证的断言不是声称已经在真机复现的结果。验收时不要只录一张截图。至少记录进入前后的getAllPathName()或size()并同时检查返回行为单栏退回列表是否一步完成、双栏点击第二个 ID 是否替换原来的详情、迅速连续点击时会不会出现错乱。建议在窄屏、展开态、横屏窗口和可自由缩放的窗口各走一遍而且把模拟器和真机的表现分开归档。对于C-204这张手机图诊断条上的“NavigationMode.Auto | stack1”属于样例可视化信息。屏幕状态栏 10:24、5G、Wi-Fi 和 100% 电量只是视觉演示要素与实际运行环境没有因果关系。图里没有展示真实折叠铰链传感器也不证明设备宽度已经跨过某个阈值。还有一组更容易遗漏的逆向用例先选择C-201马上改选C-204在窗口切换的动画尚未结束时迅速点击返回。此时真正需要核对的不是动画有没有闪一下而是导航栈是否仍由一个活动详情构成、返回后是否能看到列表、业务选中态是否同步清空。本文的门卫尚未监听系统返回事件所以完整产品仍需把返回结果和selectedId的回收连接起来否则可能出现“栈已经为空但高亮仍保留”的残留状态。这个补充需求应该通过 Navigation 的页面生命周期或路由回调处理而不是把清理动作塞进窗口宽度回调。分栏还会改变焦点路径双栏时焦点可以从列表移动到右侧详情单栏时详情成为当前可见内容。对于键盘导航、读屏和焦点恢复界面测试要检查焦点是否跟随可见区域而不能仅凭stack1推断无障碍路径已经正确。布局是展示层的职责但业务可达性和操作完成反馈仍需要由产品自己验收。六、把适用边界写在交付前这个实现适合“列表只维护一个活动详情”的轻量工单场景。如果产品需要多级详情、转派流程、草稿页或深链恢复单详情栈模型就不够了。应该把selectedId、草稿状态和页面历史明确拆开前者决定现在关注谁中间项说明用户输入了什么后者才决定返回去哪里。还要留意宽度触发的分栏阈值属于组件行为不是固定适配机型名单布局外层的约束、导航栏宽度以及最小内容宽度都会影响最终显示。解决方法不是继续写“宽于某个机型就是双栏”而是让窗口真的变化后观察Navigation是否满足预期。最终的判断并不复杂形态变化不等于业务再进入重复点击不等于新的详情历史。只要把这两件事分清楚折叠屏适配就不需要用越来越多的条件分支去掩盖导航状态问题。参考文档HarmonyOS Navigation 组件 APIhttps://developer.huawei.com/consumer/cn/doc/doccenter-references/api/ts-basic-components-navigationNavigation 分栏开发https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkts-navigation-split-modeNavDestination 生命周期https://developer.huawei.com/consumer/en/doc/harmonyos-guides/arkts-navigation-navdestination图片均为本批次独立制作的界面演示不构成真实 IDE 截图、真机调试日志或性能测试证据。