上文中我们用@ohos.router做了一个商品信息的列表和详情间的跳转页面。

但是我们发现@ohos.router是一种编程式导航。你必须在代码的某个地方调用函数router.pushUrl()来命令应用跳转到下一个页面。

而声明式导航就不一样了,核心思想是导航本身也是UI状态的一部分。我们不再发出“跳转”的命令,只需要通过改变一个状态变量来声明我们希望看到哪个页面。

一、Navigation框架的核心三要素

声明式导航主要由三个部分协同工作:

  1. <Navigation>容器:

    • 这是一个特殊的根组件,包裹了所有参与导航的页面视图。

    • 定义了一个导航的上下文或作用域。

  2. NavPathStack栈:

    • 这是一个我们需要自己定义和维护的状态变量,通常是一个数组。

    • 这个数组的内容直接决定了当前的导航路径。

      • 数组为空,显示根页面(首页)。

      • 数组中有一个元素,显示栈顶元素对应的页面。

      • 数组中有多个元素,形成一个导航层级。

    • 通过对这个数组进行.push()、.pop()等操作,来驱动UI进行页面的推入和弹出。

  3. <NavDestination>目标页:

    • 这是一个用来“注册”或“定义”可跳转目标页面的组件。

    • 我们给每个目标页面创建一个NavDestination,通过他的builder来指定该页面具体长什么样。

二、重构商品应用页面

我们把之前的商品应用页面重构一下。

2.1. 创建模块

同样的,我们先创建一个模块day5_navigation。

image-20251009094810270

在src/main/ets/pages目录下,创建两个UI Page文件:ProductListPage.ets、ProductDetailPage.ets(Index.ets可以删除)

image-20251009095008895

记得把Day5_navigationAbility.ets中UIAbility的入口页面修改了。

image-20251009095251387

2.2. 定义数据模型和导航栈

为了在页面间传递数据,并让代码更规范,我们先定义好数据类型。

创建一个新文件src/main/ets/models/ProductModels.ets

 // 商品的数据结构
 export interface Product {
   id: string;
   name: string;
   description: string;
 }

2.3. 改造ProductListPage.ets(列表页)

把列表页改造成我们的导航主容器。

 
import { Product } from '../models/ProductModels';
 import { ProductDetailPage } from './ProductDetailPage';
 ​
 const products: Product[] = [
   { id: 'p001', name: '鸿蒙智能手表', description: '最新款,搭载HarmonyOS NEXT。' },
   { id: 'p002', name: 'ArkUI T恤', description: '开发者信仰充值,100%纯棉。' },
   { id: 'p003', name: 'DevEco Studio贴纸', description: '让你的笔记本充满极客范。' },
   { id: 'p004', name: '手机', description: '拍个月球给你看。' }
 ];
 ​
 @Entry
 @Component
 struct ProductListPage {
   @State path: NavPathStack = new NavPathStack();
 ​
   @Builder
   destinationBuilder(name: string, param: object) {
     if (name === 'ProductDetail') {
       NavDestination() {
         ProductDetailPage({ product: param as Product })
       }
       .title('产品详情')
     } else {
       NavDestination() {
         Text('Invalid destination')
           .fontSize(16)
           .fontColor(Color.Red)
       }
       .title('Error')
     }
   }
 ​
   build() {
     Navigation(this.path) {
       Column() {
         List({ space: 12 }) {
           ForEach(products, (item: Product) => {
             ListItem() {
               Text(item.name)
                 .width('100%').fontSize(20).padding(20)
                 .backgroundColor(Color.White).borderRadius(10)
             }
             .onClick(() => {
               this.path.pushPathByName('ProductDetail', item);
             })
           })
         }
         .width('95%').layoutWeight(1)
       }
       .width('100%').height('100%').backgroundColor('#f1f3f5')
       .padding({ top: 10 })
     }
     .title('商品列表')
     .navDestination(this.destinationBuilder)
   }
 }

使用import导入了Product数据模型和ProductDetailPage组件。

@State path: NavPathStack是整个声明式导航的核心。

定义了一个名叫path的变量,类型是NavPathStack,这是一个专门用来管理导航路径的特殊对象。

@State装饰器就是告诉ArkUI框架,要关注path这个变量,如果发生了变化,就要自动刷新所有依赖他的UI。

@Builder也是一个装饰器,表示destinationBuilder方法不是一个普通的逻辑函数,是一个专门用来构建UI片段的模版。

destinationBuilder(name: string, param: object)构造器接收两个参数,这两个参数都是path.pushPathByName方法在跳转的时候提供的。

name是路由目标的名称,用他来区分跳转到哪个页面。

param是携带的参数,是一个通用的object。

NavDestination()是真正定义目标导航页的容器。所有可以被导航到的页面,都必须被他包裹。

在NavDestination内部,我们创建了ProductDetailPage组件的实例。param as Product是一个类型断言,明确告诉编译器:虽然param是object类型,但确定他实际上是一个Product对象,然后把他传递给详情页的product属性。

.title('产品详情')就是给这个目标页设置导航栏的标题。

Navigation(this.path)是导航的根容器,我们把@State变量this.path传给他,建立双向绑定。当path改变时,Navigation组件会自动更新视图;当用户在UI上进行返回操作时,Navigation组件也会自动更新path变量。

这里的.title('商品列表')是根页面(也就是列表页自身)设置导航栏标题。

.navDestination(this.destinationBuilder)是关键的一步。我们把刚才用@Builder定义的destinationBuilder方法,注册给了Navigation容器。这样Navigation就知道当path发生变化时,应该调用哪个模版去构建目标页面。

当用户点击列表项时,我们不再调用router,而是直接修改状态。

this.path.pushPathByName('ProductDetail', item)调用path对象的pushPathByName方法。

ProductDetail这个字符串会作为name参数传递给我们的destinationBuilder。

item是当前点击的商品对象(Product类型),会作为param参数传递给destinationBuilder。

一旦这行代码执行,@State变量path的内容就变了,ArkUI框架侦测到这个变化,就会自动使用destinationBuilder去构建ProductDetailPage并显示出来。

2.4. 改造ProductDetailPage.ets(详情页)

这个页面比较简单,只负责一件事,就是接收一个Product对象,显示出来。

 import { Product } from '../models/ProductModels';
 @Entry
 @Component
 export struct ProductDetailPage {
   @Prop product: Product;
 ​
   build() {
     Column() {
       Text(this.product.name)
         .fontSize(28).fontWeight(FontWeight.Bold).margin(20)
       Text(this.product.description)
         .fontSize(18).fontColor(Color.Gray).margin({ left: 20, right: 20 })
     }
     .width('100%').height('100%')
     .justifyContent(FlexAlign.Start).padding(15)
   }
 }

@Prop product: Product;是子组件接收父组件传递数据的标准方式。

@Prop装饰器声明product是一个外部属性,他的值由创建ProductDetailPage的地方(也就是destinationBuilder)提供。

这样就建立了一个从父到子的单向数据流。

在build函数中,我们可以像使用普通成员变量一样,直接通过this.product.name和this.product.description来访问由@Prop传入的数据,并把他们渲染成UI。

2.5. 改造后界面

列表

image-20251009120224417

详情

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐