从今天开始,博主将开设一门新的专栏用来讲解市面上比较热门的技术 "鸿蒙开发",对于刚接触这项技术的小伙伴在学习鸿蒙开发之前,有必要先了解一下鸿蒙,从你的角度来讲,你认为什么是鸿蒙呢?它出现的意义又是什么?鸿蒙仅仅是一个手机操作系统吗?它的出现能够和Android和IOS三分天下吗?它未来的潜力能否制霸整个手机市场呢?
抱着这样的疑问和对鸿蒙开发的好奇,让我们开始今天对ArkUI状态管理的掌握吧!
目录
ArkUI状态管理
在声明式UI中是以状态来驱动视图进行更新的,其中的核心概念就是状态和视图**。** 所谓状态就是驱动视图更新这个数据,或者说是我们自定义组件当中定义好的那些被装饰器标记好的变量;所谓视图就是指GUI描述渲染得到的用户界面;视图渲染好了之后用户就可以对视图中的页面元素产生交互,通过点击、触摸、拖拽等互动事件来改变状态变量的值,在arkui的内部就有一种机制去监控状态变量的值,一旦发现它发生了变更就会去触发视图的重新渲染。所以像这种状态和视图之间的相互作用的机制,我们就称之为状态管理机制。
状态管理需要用到多个不同的装饰器,接下来我们开始学习状态管理的基本概念以及以下几个装饰器的基本用法和注意事项。
@State装饰器
使用@State装饰器有以下注意事项:
1)@State装饰器标记的变量必须初始化,不能为空值
2)@State支持Object、class、string、number、boolean、enum类型以及这些类型的数组
3)嵌套类型(Object里面的某个属性又是一个Object)以及数组中的对象属性无法触发视图更新,以下是演示代码:
javascript
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
@Entry
@Component
struct StatePage {
idx: number = 1
@State p: Person[] = [
new Person('张三', 20)
]
build(){
Column(){
Button('添加')
.onClick(()=>{
this.p.push(new Person('张三'+this.idx++, 20 ))
})
ForEach(
this.p,
(p, index) => {
Row(){
Text(`${p.name}: ${p.age}`)
.fontSize(30)
.onClick(() => {
//数组内的元素变更不会触发数组的重新渲染
// p.age++
//数组重新添加、删除或者赋值的时候才会触发数组的重新渲染
this.p[index] = new Person(p.name, p.age+1)
})
Button('删除')
.onClick(()=>{
this.p.splice(index, 1)
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
}
)
}
.width('100%')
.height('100%')
}
}
@Prop和@Link
Prop和Link这两个装饰器是在父子组件之间数据同步的时候去使用的,以下是两者的使用情况:
装饰器 | @Prop | @Link |
---|---|---|
同步类型 | 单向同步 | 双向同步 |
允许装饰的变量类型 | 1)@Prop只支持string、number、boolean、enum类型 2)父组件是对象类型,子组件是对象属性 3)不可以是数组、any | 1)父子类型一致:string、number、boolean、enum、object、class,以及他们的数组 2)数组中的元素增、删、替换会引起刷新 3)嵌套类型以及数组中的对象属性无法触发视图更新 |
接下来借助Prop和Link完成一个小案例:
我们在父组件中通过prop向子组件传值,子组件通过@Prop装饰器接受到值之后进行页面渲染,这里我们采用了ArkUI提供的堆叠容器和进度条组件实现页面的配置:
javascript
// 统一卡片样式
@Styles function card(){
.width('95%')
.padding(20)
.backgroundColor(Color.White)
.borderRadius(15)
.shadow({radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4})
}
@Component
export struct TaskStatistics {
@Prop totalTask: number // 总任务数量
@Prop finishTask: number // 已完成任务数量
build() {
// 任务进度卡片
Row(){
Text('任务进度')
.fontSize(30)
.fontWeight(FontWeight.Bold)
// 堆叠容器,组件之间可以相互叠加显示
Stack(){
// 环形进度条
Progress({
value: this.finishTask,
total: this.totalTask,
type: ProgressType.Ring // 选择环形进度条
})
.width(100)
Row(){
Text(this.finishTask.toString())
.fontColor('#36D')
.fontSize(24)
Text(' / ' + this.totalTask.toString())
.fontSize(24)
}
}
}
.margin({top: 20, bottom: 10})
.justifyContent(FlexAlign.SpaceEvenly)
.card()
}
}
子组件如果修改父组件的值的话,需要通过装饰器@Link来实现,父组件需要通过$来拿值:
javascript
// 任务类
class Task {
static id: number = 1 // 静态变量,内部共享
name: string = `任务${Task.id++}` // 任务名称
finished: boolean = false // 任务状态,是否已完成
}
// 统一卡片样式
@Styles function card(){
.width('95%')
.padding(20)
.backgroundColor(Color.White)
.borderRadius(15)
.shadow({radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4})
}
@Component
export struct TaskList {
@Link totalTask: number // 总任务数量
@Link finishTask: number // 已完成任务数量
@State tasks: Task[] = [] // 任务数组
// 任务更新触发函数
handleTaskChange(){
this.totalTask = this.tasks.length // 更新任务总数量
this.finishTask = this.tasks.filter(item => item.finished).length // 更新任务数量
}
build() {
Column(){
// 任务新增按钮
Button('新增任务')
.width(200)
.onClick(()=>{
this.tasks.push(new Task()) // 新增任务数组
this.handleTaskChange()
})
// 任务列表
List({space: 10}){
ForEach(
this.tasks,
(item: Task, index)=>{
ListItem(){
Row(){
Text(item.name)
.fontSize(20)
Checkbox()
.select(item.finished)
.onChange(val => {
item.finished = val // 更新当前的任务状态
this.handleTaskChange()
})
}
.card()
.justifyContent(FlexAlign.SpaceBetween)
}
.swipeAction({end: this.DeleteButton(index)})
}
)
}
.width('100%')
.layoutWeight(1)
.alignListItem(ListItemAlign.Center)
}
}
@Builder DeleteButton(index: number){
Button(){
Image($r('app.media.delete'))
.fillColor(Color.Red)
.width(20)
}
.width(40)
.height(40)
.type(ButtonType.Circle)
.backgroundColor(Color.Red)
.margin(5)
.onClick(()=>{
this.tasks.splice(index, 1)
this.handleTaskChange()
})
}
}
接下来就需要在父组件引用这两个子组件了,然后传参来获取和传递相关数值:
javascript
// 任务类
class Task {
static id: number = 1 // 静态变量,内部共享
name: string = `任务${Task.id++}` // 任务名称
finished: boolean = false // 任务状态,是否已完成
}
import { TaskStatistics } from '../components/TaskStatistics'
import { TaskList } from '../components/TaskList'
@Entry
@Component
struct PropPage {
@State totalTask: number = 0 // 总任务数量
@State finishTask: number = 0 // 已完成任务数量
@State tasks: Task[] = [] // 任务数组
build(){
Column({space: 10}){
// 任务进度卡片
TaskStatistics({ totalTask: this.totalTask, finishTask: this.finishTask })
// 任务列表
TaskList({ totalTask: $totalTask, finishTask: $finishTask })
}
.width('100%')
.height('100%')
.backgroundColor('#F1F2F3')
}
}
最终呈现的结果如下:
@Provide和@Consume
这两个装饰器可以跨组件提供类似@State和@Link的双向同步,操作方式很简单,父组件之间使用Provide装饰器,子组件全部使用Consume装饰器,父组件都不需要传递参数了,直接调用子组件函数即可:
最终呈现的结果如下:
虽然相对来说比Prop和Link简便许多,但是使用Provide和Consume还是有代价的,本来需要传递参数的,但是使用Provide不需要传递参数,其内部自动帮助我们去维护,肯定是有一些资源上的浪费,所以说我们能用Prop还是尽量用Prop,实在用不了的可以去考虑Provide。
@Observed和@ObjectLink
这两个装饰器用于在涉及嵌套对象或数组元素为对象的场景中进行双向数据同步:
我们给任务列表中的文本添加一个样式属性,当我们点击勾选的话,文本就会变灰并且加上一个中划线样式:
但是当我们勾选之后,视图并没有发生变化,原因是我们的Task是一个对象类型,数组的元素是对象,对象的属性发生修改是不会触发视图的重新渲染的,所以这里我们需要使用本次讲解的装饰器来进行解决:
我们给class对象设置@Observed装饰器:
然后在要修改对象属性值的位置进行设置@ObjectLink装饰器,因为这里一个任务列表通过ForEach遍历出来的,所以我们需要将这个位置单独抽离出来形成一个函数,然后将要使用的item设置@ObjectLink装饰器,因为还需要调用函数,但是任务列表的函数不能动,所以我们也将调用的函数作为参数传递过去:
javascript
@Component
struct TaskItem {
@ObjectLink item: Task
onTaskChange: () => void
build(){
Row(){
if (this.item.finished){
Text(this.item.name)
.finishedTask()
}else{
Text(this.item.name)
}
Checkbox()
.select(this.item.finished)
.onChange(val => {
this.item.finished = val // 更新当前的任务状态
this.onTaskChange()
})
}
.card()
.justifyContent(FlexAlign.SpaceBetween)
}
}
传递过程中为了确保this指向没有发生改变,我们在传递函数的时候,还需要通过bind函数指定this指向:
最终呈现的结果如下:
页面路由
页面路由是指在应用程序中实现不同页面之间的跳转和数据传递,如果学习过前端vue或react框架的人,可以非常简单的理解页面路由跳转的概念,以下是在鸿蒙开发中进行页面路由跳转所调用的API函数以及相应函数的作用,与前端的vue框架十分类似:
Router有两种页面跳转模式,分别是:
router.pushUrl():目标页不会替换当前页,而是压入页面栈,因此可以用router.back()返回当前页
router.replaceUrl():目标页替换当前页,当前页会被销毁并释放资源,无法返回当前页
Router有两种页面实例模式,分别是:
Standard:标准实例模式,每次跳转都会新建一个目标页并压入栈顶,默认就是这种模式
Single:单实例模式,如果目标页已经在栈中,则离栈顶最近的同url页面会被移动到栈顶并重新加载
了解完页面路由基本概念之后,接下来在案例中开始介绍如何使用页面路由:
首先我们在index首页定义路由信息:
javascript
// 定义路由信息
class RouterInfo {
url: string // 页面路径
title: string // 页面标题
constructor(url: string, title: string) {
this.url = url
this.title = title
}
}
接下在struct结构体里面定义路由相关信息以及页面的静态样式:
javascript
@State message: string = '页面列表'
private routers: RouterInfo[] = [
new RouterInfo('pages/router/test1', '页面1'),
new RouterInfo('pages/router/test2', '页面2'),
new RouterInfo('pages/router/test3', '页面3'),
new RouterInfo('pages/router/test4', '页面4')
]
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.fontColor('#008c8c')
.height(80)
List({space: 15}){
ForEach(
this.routers,
(router, index) => {
ListItem(){
this.RouterItem(router, index + 1)
}
}
)
}
.layoutWeight(1)
.alignListItem(ListItemAlign.Center)
.width('100%')
}
.width('100%')
.height('100%')
}
}
定义RouterItem函数,设置点击函数进行路由跳转:
javascript
@Builder RouterItem(r: RouterInfo, i: number){
Row(){
Text(i+'.')
.fontSize(20)
.fontColor(Color.White)
Blank()
Text(r.title)
.fontSize(20)
.fontColor(Color.White)
}
.width('90%')
.padding(12)
.backgroundColor('#38f')
.shadow({radius: 6, color: '#4f0000', offsetX: 2, offsetY: 4})
.onClick(()=>{
// router跳转,传递3个参数
router.pushUrl(
// 跳转路径及参数
{
url: r.url,
params: {id: i}
},
// 页面实例
router.RouterMode.Single,
// 跳转失败的一个回调
err => {
if (err) {
console.log(`跳转失败,errCode:${err.code} errMsg: ${err.message}`)
}
}
)
})
}
定义3个路由跳转页,设置第四个路由没有跳转页面,作对照:
注意,如果是仅仅是新建一个ArkTS页面的话需要在以下的文件中进行路由配置:
如果觉得每次创建一个页面都要进行一次路由路径的创建比较烦的话,可以采用以下创建方式,会自动帮我们配置好路由路径,而不需在去手动设置:
在子组件中,如果我们想拿到传递过来的参数可以调用getParams函数,返回调用back函数:
想要加个返回的警告可以采用如下的方式:
最终呈现的结果为: