鸿蒙开发5--鸿蒙页面导航(声明式导航Navigation组件)
上文中我们用@ohos.router做了一个商品信息的列表和详情间的跳转页面。
但是我们发现@ohos.router是一种编程式导航。你必须在代码的某个地方调用函数router.pushUrl()来命令应用跳转到下一个页面。
而声明式导航就不一样了,核心思想是导航本身也是UI状态的一部分。我们不再发出“跳转”的命令,只需要通过改变一个状态变量来声明我们希望看到哪个页面。
一、Navigation框架的核心三要素
声明式导航主要由三个部分协同工作:
<Navigation>容器:
这是一个特殊的根组件,包裹了所有参与导航的页面视图。
定义了一个导航的上下文或作用域。
NavPathStack栈:
这是一个我们需要自己定义和维护的状态变量,通常是一个数组。
这个数组的内容直接决定了当前的导航路径。
数组为空,显示根页面(首页)。
数组中有一个元素,显示栈顶元素对应的页面。
数组中有多个元素,形成一个导航层级。
通过对这个数组进行.push()、.pop()等操作,来驱动UI进行页面的推入和弹出。
<NavDestination>目标页:
这是一个用来“注册”或“定义”可跳转目标页面的组件。
我们给每个目标页面创建一个NavDestination,通过他的builder来指定该页面具体长什么样。
二、重构商品应用页面
我们把之前的商品应用页面重构一下。
2.1. 创建模块
同样的,我们先创建一个模块day5_navigation。
在src/main/ets/pages目录下,创建两个UI Page文件:ProductListPage.ets、ProductDetailPage.ets(Index.ets可以删除)
记得把Day5_navigationAbility.ets中UIAbility的入口页面修改了。
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@Componentstruct ProductListPage {@State path: NavPathStack = new NavPathStack();@BuilderdestinationBuilder(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@Componentexport 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. 改造后界面
列表
详情