一、双栏看着稳定,导航栈却可能越点越深

在资料库类应用里,平板和展开态折叠屏常常采用“左目录、右详情”的结构。视觉上右边只有一张文档卡片,用户连续点击同一条目录时,页面内容似乎没有变化;但如果每次点击都调用一次pushPath,导航栈可能悄悄叠了好几个同名详情页。用户按一次返回只弹掉最上面一层,屏幕还是同一个标题,于是误以为返回键失灵。这是路由状态与可见状态脱节,跟文件内容是否加载成功并没有直接关系。

本篇以SplitRoute演示:资料库有十二条目录记录,宽度采用预设的740vp双栏场景,当前选择DOC-042《高级实践》。针对这条记录连续发起四次打开请求,只允许一次入栈,其余三次按重复请求忽略。期望页面为DetailPage、栈深度为一。这里的“四次”是模型输入,不是通过DevEco反复点击获得的实测数据;图片也只展示业务希望呈现的状态。

二、区分同一个详情与多个详情实例

最初容易想到的做法,是用一个isLoading布尔值挡住连点。它对网络请求有用,却不能代表导航栈中是否已经存在详情。加载完成后isLoading归零,用户再次点击会继续入栈;屏幕虽没变,返回路径已经延长。另一种方案是在每次点击后调用clear(),这样能阻止重复,却会把用户本应保留的目录状态一并清空,在窗口形态切换时尤其生硬。

更可靠的判断应围绕导航对象本身。HarmonyOS ArkUI的Navigation是路由容器,NavPathStack管理子页,NavDestination是可进入栈的目标组件。开发者需要明确自己的业务目标:是允许多个不同文档逐层访问,还是在双栏场景始终只保留一层详情?此处选择后者,不强行覆盖其它业务类型。左边的目录常驻,右边的DetailPage负责唯一详情,用户连续点击相同文档只改变点击统计,不制造新的历史页面。

这也不是EasyGo平行视界配置文件的实现教程。Navigation的Split模式属于应用内部的导航布局;系统级平行视界、窗口协同和设备形态策略有额外条件。本文只拿双栏资料库解释导航栈治理,不声称已经完成EasyGo适配,也不把模拟740vp等同于某款实际折叠屏可用宽度。

三、栈跟布局模式分开管理

先给出界面骨架。核心是Navigation(this.pathStack)与mode(NavigationMode.Split),前者明确路由栈对象,后者表达宽布局下的分栏策略。在真实跨设备工程中,应根据容器尺寸在Auto、Stack、Split间做取舍,并检查NavBar区域最小宽度,不能只写一个Split常量就宣称所有手机都适配。下面是展示职责关系的ArkTS关键片段,目录项的真实数据和样式略去。

@Entry
@Component
struct SplitHome {
  private pathStack: NavPathStack = new NavPathStack();
  @State selectedId: string = 'DOC-042';
  @State ignored: number = 0;
  @State accepted: number = 0;

  build() {
    Navigation(this.pathStack) {
      Column() {
        Text('SplitRoute 资料库');
        Button('打开 DOC-042').onClick(() => this.openDetail('DOC-042'));
      }
    }
    .mode(NavigationMode.Split)
    .navDestination(this.detailBuilder)
  }
}

这里没有把NavPathStack设成@State,因为它本身是导航容器的控制对象,不能在每次状态刷新时重新new一个栈。selectedId只表达用户当前选中的业务对象,accepted和ignored用于演示统计。导航栏里的真实十二条资料记录可以通过List或其它布局渲染,本文不重复讨论上一轮书架重排的滚动锚点问题,而是只处理“点同一个详情,栈是否变深”。

四、入栈之前先核对已有目标

下一段解决重复入栈。getAllPathName()来自NavPathStack,可读取栈内页面名称;pushPathByName()负责入栈。为让案例可观察,四次请求中第一次计为accepted=1,其后在DetailPage已经存在时将ignored累加到3。实际产品应决定当用户点的是另一个docId时如何处理:替换现有详情、复用同一详情页参数,还是允许新页叠加;这三种行为不能混写在一个无条件push里。

private openDetail(docId: string): void {
  if (!docId.startsWith('DOC-')) { return; }
  const names: string[] = this.pathStack.getAllPathName();
  if (names.includes('DetailPage')) {
    this.ignored += 1;
    console.info(`[SplitRoute] SR-205 ignored=${this.ignored}`);
    return;
  }
  this.pathStack.pushPathByName('DetailPage', { docId: docId });
  this.selectedId = docId;
  this.accepted += 1;
}

这里只允许一个名为DetailPage的目标,因此无需等待网络数据加载完成才做重复判断。要注意,getAllPathName()只能说明页面名称,不保证参数合法,更无法代替业务权限检查。startsWith('DOC-')仅作为示例形式校验,不能作为正式白名单;真实应用还应核对条目是否存在、是否已删除、是否属于当前账号。打开请求如果跨线程或跨异步回调汇聚,还应在上层串行化,不把“同一UI事件队列通常顺序执行”当成永久并发保证。

对路由失败也不能只加计数。pushPathByName通常是栈操作入口,但目标构建还涉及路由表或builder。页面名拼错、NavDestination构建失败时,accepted统计需要根据产品定义修正;生产版可使用带结果反馈的路由方案核对最终到达状态。本文的计数仅为设计预期,不能当作DevEco实际跳转成功证明。

五、返回不是清空全部,而是收口当前详情

第三段要处理“返回目录”按钮。设计里目录仍在左栏,右侧DetailPage是唯一的可出栈实例。显式返回应该只pop当前详情,不要直接clear整条栈。对系统返回手势,也要让栈管理统一驱动界面,而不是另存一个isDetailVisible并靠猜测更新。页面隐藏或销毁后,从栈得到的结果才是下一次点击能否再次打开的依据。

@Builder
private detailBuilder(name: string, param: Object) {
  if (name === 'DetailPage') {
    NavDestination() {
      Column() {
        Text('DOC-042 高级实践');
        Button('返回目录').onClick(() => this.backToCatalog());
      }
    }
  }
}

private backToCatalog(): void {
  if (this.pathStack.getAllPathName().includes('DetailPage')) {
    this.pathStack.pop();
  }
}

在这个最小模型里,param没有被展开使用。正式详情页应从传入参数读取docId,并在目标页打开时重新校验,而不是硬编码DOC-042。此处把标题写死,是为了把关注点放在路由栈收敛,并不代表NavDestination传参应该被忽略。拆成独立组件后,还应处理参数更新、对象被删除以及从外部深链进入时目录未准备好等边界。

返回后再次点击同一条目录,栈没有DetailPage名称,允许重新push;只要用户在旧页面还没退出完毕就连续触发返回操作,应用要防止额外pop影响上一级内容。转场动画、系统手势和设备返回键应一起纳入验收,不要只验证按钮。这里没有手动订阅全局窗口事件,避免把生命周期监听变成一个新的重复入栈来源。

六、日志要证明栈深,而不只是按钮点了几次

本轮设计的演示日志分别为:13:41:10 [SplitRoute] SR-205 doc=DOC-042 click=4、13:41:10 [SplitRoute] SR-205 accepted=1 ignored=3、13:41:11 [SplitRoute] SR-205 stack=DetailPage depth=1。它们描述的是四次相同请求汇总后的目标结果,非真实HiLog采样。界面同时显示十二条目录、740vp、SPLIT、当前DetailPage和栈深一,便于从同一套数据审查“可见页面没有叠加,逻辑栈也不叠加”。

对调试来说,只看到“点击了四次”还不足以证明修复成立;至少要记录入口请求、去重决策、实际栈名称和返回后的栈变化。若真实日志出现accepted=1但stack仍有两个DetailPage,应优先检查其他入口是否绕过这段方法,例如通知点击、深链打开或恢复历史页面。别靠把按钮禁用一秒来掩盖所有来源。

七、形态变化不应重播点击事件

从窄屏Stack切换到宽屏Split时,Navigation展示形态发生变化,但不应该因此主动再次调用openDetail。重新布局与新建业务历史是两回事。开发者可以选择Auto让框架参与布局决策,也可以用明确的产品阈值切换模式;无论哪条路径,都要保证pathStack实例不随着build()重建。若窗口关闭重新进入,历史是否恢复则属于产品策略,需要单独设计存储和校验规则。

多窗口场景还需要区分全局单例与窗口实例。两个独立窗口可能各有自己的Navigation栈,不能简单用进程级布尔值阻止第二个窗口打开详情。反过来,若业务真的只允许一个编辑会话,则须在业务层管理锁,而不是假设NavPathStack可以跨所有窗口统一去重。本文的防重复约束仅作用于一个Navigation容器实例,边界明确之后才容易写出可复现的测试。

八、把验收落到三个可复现的场景

首先检查同一目录项的连续点击。预期请求数四、入栈数一、拦截数三;其次检查点击返回后再次选择,确认原DetailPage已出栈且新一次点击可以重新进入;最后检查窗口宽度变化前后选中ID仍是DOC-042,路径栈没有凭空增加。三个场景都应在受支持设备上分别记录真实的入口和出口日志,不把生成的图片充当验收证据。

这个方案不追求把所有页面都变成单例,而是根据双栏资料库的交互原则,避免视觉上已经是同一个详情时继续堆积返回层级。面对深链、嵌套Navigation、多文档编辑或需要保留历史详情的业务,应修改规则,不能机械复制“只允许一个DetailPage”。文章参考华为Navigation分栏开发及NavPathStack API一手资料,尚未在本轮运行实际项目或测试系统手势。

资料:华为开发者文档《Navigation分栏开发》 https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkts-navigation-split-mode ;《Navigation接口参考》 https://developer.huawei.com/consumer/cn/doc/doccenter-references/api/ts-basic-components-navigation 。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐