HarmonyOS 7 + ArkUI Navigation:平行视界空栈占位与详情回退链【鸿蒙心迹】
一个文档型应用在手机上运行时,列表和详情顺序出现,路径很直观:点开一条笔记,阅读,返回列表。到了展开态折叠屏,开发者通常会把同一套 Navigation 调整成左右分栏。此时最容易被误判的地方,是右侧大片空白。它未必是页面加载失败,也可能只是路由栈本来就没有任何详情页。另一个更隐蔽的问题,是返回动作删掉详情以后,左侧高亮仍留在上一条记录上,让人误以为内容丢失。本文使用一个静态笔记演示工程 AtlasNotes。固定演示记录为 NOTE-204,标题为“路线规划”,打开详情后展示状态 DETAIL_READY;这两项都是应用内的示例数据。讨论的是分栏导航和业务选中态之间的契约,不涉及真实云端同步、性能计时或已完成的真机验证。
## 一、空白区域是栈状态,不等于错误页面ArkUI 的 Navigation 同时承担导航栏和 NavDestination 的组织。手机的单栏布局经常在打开详情后遮住列表;宽屏的分栏布局保留左侧导航栏,把 NavDestination 放在右侧。区别发生在显示层,但数据层应该使用同一份选中记录。倘若业务把“当前文章”直接定义为“路由栈顶页面”,分栏空栈时就没有足够的信息决定左侧选中态。官方分栏开发文档明确说明:NavigationMode.Split 是分栏模式,NavigationMode.Auto 可以随设备状态调整;分栏且空栈时,右侧默认可能留白。splitPlaceholder 用于为这个位置提供展示内容,且占位本身不是 NavDestination,不参与路由栈,也不能把交互能力当作正常详情页。官方 FAQ 指出它是 API 20 起的能力。这个版本边界很重要:不应把新属性原样复制到较低 API 目标工程然后宣称兼容。为了把需求收敛,AtlasNotes 只约定三个条件:初始状态可以没有选中笔记;用户点选某条笔记后,路由参数必须是稳定的笔记标识;从详情返回后,列表中不得维持一个不存在于当前详情视图的“正在阅读”标记。这里把“选中”和“最近阅读”拆开,后者可以持久化,前者跟随页面栈变化。### 1. 先让数据模型不要暗含视图状态下面是最小数据模型。它没有尝试预测设备是否展开,也没有把字体大小或列表宽度塞入业务记录。笔记标识用字符串,便于跨进程恢复时重新查询;正文放在本地演示常量中,以便聚焦导航行为。ts// common/NoteData.ets:只定义可稳定寻址的业务数据export class NoteData { constructor( public id: string, public title: string, public summary: string ) {}}export const NOTES: NoteData[] = [ new NoteData('NOTE-203', '离线地图', '缓存范围与更新说明'), new NoteData('NOTE-204', '路线规划', '折叠设备行程页的阅读草稿'), new NoteData('NOTE-205', '出行清单', '按地点归档的事项')];export function findNote(id: string): NoteData | undefined { return NOTES.find((item: NoteData) => item.id === id);}````findNote` 的作用是把 URI 式的标识映射为实际内容。它的返回值包含 `undefined`,提醒调用方不能把每次路由参数都当作合法对象。演示环境只有三条固定数据;换成真实数据库时,详情页还要处理删除、权限变化和异步加载失败。一个稳定的 ID 并不能自动保证页面内容永远存在。## 二、占位页负责说明状态,真正详情仍由栈管理在折叠大屏上,有两种常见的补空白方式。一种是在启动时直接 push 第一条详情,用户一进来就看到内容;另一种是不代替用户做选择,只用占位页解释右侧应该如何操作。前者适合固定首页,后者适合个人资料、私密笔记和长列表。AtlasNotes 采用后者,不把默认展示误写成一次阅读行为。下面是导航主页的关键部分。它使用官方文档中的 `ComponentContent`、`wrapBuilder`、`Navigation`、`NavPathStack` 和 `splitPlaceholder`;为突出行为,没有包括应用入口主题等工程模板文件。注意,此代码路径要求目标 API 满足 `splitPlaceholder` 的起始版本条件。ts// pages/AtlasHome.ets(目标 API 20+ 的关键实现片段)import { ComponentContent } from ‘@kit.ArkUI’;import { NOTES, NoteData } from ‘…/common/NoteData’;@Builderfunction EmptyDetail() { Column({ space: 16 }) { Text(‘请选择左侧笔记’).fontSize(24).fontWeight(FontWeight.Medium) Text(‘列表与详情共用一条导航路径’).fontSize(14) }.width(‘100%’).height(‘100%’).justifyContent(FlexAlign.Center);}@Entry@Componentstruct AtlasHome { private navPathStack: NavPathStack = new NavPathStack(); private placeholder = new ComponentContent(this.getUIContext(), wrapBuilder(EmptyDetail)); @Builder pageMap(name: string) { if (name === ‘NoteDetail’) { NoteDetail(); // 与下文同模块示例配套 } } build() { Navigation(this.navPathStack) { List() { ForEach(NOTES, (note: NoteData) => { ListItem() { Text(note.title).padding(18) .onClick(() => { this.navPathStack.pushPath({ name: ‘NoteDetail’, param: note.id }); }); } }, (note: NoteData) => note.id) }.width(‘100%’) } .mode(NavigationMode.Auto) .navDestination(this.pageMap) .splitPlaceholder(this.placeholder) .title(‘AtlasNotes’) .width(‘100%’).height(‘100%’); }}这里的 `Auto` 交给系统选择单栏或分栏,不要求业务每次折叠都重置路由。`ComponentContent` 属于 UI 实例相关对象,实际工程要结合页面销毁释放策略管理它,不能把实例塞进全局单例长久保留。`pageMap` 注册的是自定义路由构建函数,示例的 `NoteDetail` 需与实际工程模块导入关系一致;如果改为系统路由表,则应该删除重复的自定义注册,避免出现两套同名路由。DevEco 风格演示图以 `AtlasHome.ets` 为中心,重点标出 `splitPlaceholder` 和 `pushPath`,右侧的模拟器示意选择前后的左右内容区,底部日志仅是预设的流程样本。它并不证明 IDE 已经编译通过。阅读这张图的价值,是核对“空栈占位”和“已选详情”属于两个状态,而不是观看一个看起来真实的工具截图。## 三、返回时撤销的是页面,不是业务记录列表高亮与页面返回最容易发生脱节。一个看似省事的写法,是在点击事件中设置 `selectedId='NOTE-204'`,然后在单栏详情页的关闭按钮里清空它。但系统返回、手势返回和分栏切换并不一定经过自建的关闭按钮。这样一来,右侧返回到占位页,左侧却仍提示“正在阅读”。更稳妥的办法,是把 `selectedId` 看作“阅读中的临时标识”,让 NavDestination 在建立时解析参数,在销毁后再由父视图根据栈状态更新高亮。下面展示详情的入参校验和观测日志。日志中的 `depth` 指当前演示导航栈的名称数组长度,并非设备全局页面数。ts// pages/NoteDetail.ets:细化入参和返回语义import { findNote, NoteData } from ‘…/common/NoteData’;@Componentstruct NoteDetail { @State noteId: string = ‘’; @State noteTitle: string = ‘记录不可用’; build() { NavDestination() { Column({ space: 12 }) { Text(this.noteTitle).fontSize(25) Text(记录编号:${this.noteId}).fontSize(14) Text(‘折叠设备行程页的阅读草稿’).fontSize(16) }.padding(24) } .title(this.noteTitle) .onReady((ctx: NavDestinationContext) => { this.noteId = ctx.pathInfo.param as string; const note: NoteData | undefined = findNote(this.noteId); this.noteTitle = note ? note.title : ‘记录不可用’; console.info(AtlasNotes route ${this.noteId} depth=${ctx.pathStack.getAllPathName().length} DETAIL_READY); }) .onBackPressed(() => false); // 由 Navigation 默认返回,避免重复 pop }}```对于 NOTE-204,示例期望输出是 AtlasNotes route NOTE-204 depth=1 DETAIL_READY。这是静态流程的预期日志,不是实机抓取的 HiLog。假如收到未知 ID,页面要显示“记录不可用”,而不是为了不留白回退到第一条;否则返回链看起来正常,实际打开的却可能是另一份资料。若用户快速连续点击多个条目,pushPath 会按调用追加详情,需要根据产品需求选择显式返回或单例导航策略,不可默认多次点击只会保留一页。### 1. 一张状态表,比临时加三个布尔值有效建议至少覆盖四条操作路径:刚打开宽屏首页时,栈深度为零,显示占位;点开 NOTE-204 后,栈深度为一,详情为“路线规划”;触发系统返回后,栈深度恢复为零,显示占位;折叠到单栏再展开时,当前栈不因布局切换而被清空。第四条是应验证的约束,而不是本文已测量的结论。即便视觉上出现重新布局,也应检查是否由页面重建、Ability 恢复或业务代码误清栈引起。排查时先记录事件时间线,再解释渲染。可以依次打印按钮点击、pushPath、onReady、getAllPathName()、返回触发和目标显示模式。不要只记录“回退成功”,因为它无法区分返回到上一条详情与返回到占位页;也不要把 splitPlaceholder 的文本变化当成路由事件,它本来就不是栈页面。
手机页面演示图展示同一套数据在窄屏下的单栏详情:标题“路线规划”、记录 NOTE-204 和 DETAIL_READY 标签。用户看到的是阅读结果,不是右侧占位结构;这和开发界面图中的双栏逻辑形成互补。状态栏中的时间和信号为设计用占位元素,不参与工程指标。## 四、把边界写清楚才谈得上可复用这套做法依赖 Navigation 容器、NavDestination 路由以及可访问的稳定笔记标识,并不保证所有设备形态下的窗口宽度都对应某一种固定模式。用户可以分屏、调节自由窗口,系统也可能因屏幕比例而选择不同模式。Auto 是布局策略,不等于设备类别检测器。依赖“设备是折叠屏”来决定是否清空业务状态,会把同一窗口中的系统适配和业务选择混为一谈。另一个边界是 API 版本。官方 FAQ 对 splitPlaceholder 明确标注 API 20 起:目标低于该版本时,应使用可用的默认 NavDestination 路由方案,不能直接照搬代码。真实工程需要补充对应 SDK 版本检查、项目资源引用、页面导入和组件生命周期处理。本文给出的三个片段服务于设计和审查,不声称已在特定型号设备上完成编译、折叠切换或稳定性回归。如果要把 AtlasNotes 做成可长期维护的笔记应用,下一步是把高亮的来源改成导航栈与数据仓库的共同推导结果,增加删除记录后的重定向策略,并在自动化 UI 测试中覆盖系统返回、手势返回、连续点选和窗口尺寸变化。到那时,所谓“平行视界适配”才不是把两列排到屏幕上,而是让同一份阅读上下文经得起视图形态变化。官方依据:- Navigation 分栏开发:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkts-navigation-split-mode- Navigation 分栏空白 FAQ(含 splitPlaceholder 起始版本):https://developer.huawei.com/consumer/cn/doc/doccenter-dev-faq/faqs-arkui-1242- Navigation 参数与回退:https://developer.huawei.com/consumer/cn/doc/doccenter-dev-faq/faqs-arkui-1098> 说明:本文的项目、数据、日志及配图均为可审查的合成案例,不是作者声称亲历的发布或设备实测。
更多推荐




所有评论(0)