一、基本概念
我们构建的页面多为静态界面。如果希望构建一个动态的、有交互的界面,就需要引入“状态”的概念。
在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。
二、@State 装饰器
class Person {
name: string
age: number
girlfriend: Person
// ?可选参数
constructor(name: string, age: number, girlfriend?: Person) {
this.name = name
this.age = age
this.girlfriend = girlfriend
}
}
@Entry
@Component
struct StatePage {
// 注意1:状态变量(必须初始化,不能为空值)
@State name: string = 'Jack'
@State age: number = 30
// 改变对象属性的值,会触发页面重新渲染
@State person1: Person = new Person('Tom', 20)
// 注意2:对象嵌套对象,改变嵌套对象属性的值,不会触发页面重新渲染
@State person2: Person = new Person('David', 26, new Person('Lily', 22))
// 注意3:添加/删除/修改数组元素会触发页面重新渲染;修改数组中对象属性的值,不会触发页面重新渲染
@State personList: Person[] = [
new Person('张三', 18),
new Person('李四', 19)
]
build() {
Row() {
Column() {
// 字符串模版语法
Text(`${this.name} : ${this.age}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.age++
})
Text(`${this.person1.name} , ${this.person1.age}`)
.fontSize(30)
.fontColor(Color.Blue)
.onClick(value => {
this.person1.age += 1
})
}
.width('100%')
}
.height('100%')
}
}
三、@Prop、@Link 装饰器
用途:用于父子组件数据同步传值
-
@Prop
装饰属性(单向数据同步):父组件向子组件传值,传的是变量的值 -
@Link
装饰属性(双向数据同步),父组件向子组件传值,必须带上$
,表示传的是变量的引用
四、@Provide、@Consume 装饰器
用途:一般用于跨组件双向数据同步传值。相比
@State
、@Link
装饰器,使用更加方便简洁,如果使用@Link
,会传递多次,使用@Provide
、@Consume
,不需要我们做任何事(即不需要我们调用子组件手动传参),由系统内部自动帮我们做数据同步和传值。系统帮我们维护,会消耗系统一些资源,一般是在@Link
装饰器不能满足使用需求时,会使用这个。-
使用:父组件使用
@Provide
装饰属性,子组件/其它后代组件使用@Consume
装饰属性,注意属性名及类型和父组件保持一致,系统内部会自动帮我们做数据同步和传值。
五、@Observed、@ObjectLink 装饰器
六、任务统计案例
-
效果图
- 实现代码
// 任务模型
@Observed // @Observed装饰器,用于监听嵌套对象的属性,或数组元素对象的属性变化;与@ObjectLink配套使用(即子组件Task类型属性前加@ObjectLink)
class Task {
// static 静态变量:这个类的所有对象共享(通过类访问)
static id: number = 1
// 任务名称(每次创建对象会加1)
name: string = `任务${Task.id++}`
// 任务状态:是否完成
finished: boolean = false
// 属性在定义的时候初始化,就不需要再写构造方法,new对象时候会自动初始化,也就不需要调用构造方法去创建对象
}
// 任务统计信息
class StatInfo {
// 总任务数量
totalTask: number = 0
// 已完成任务数量
finishTask: number = 0
}
// 统一的卡片样式
@Styles function cardStyle() {
.width('90%')
.padding(20)
.backgroundColor(Color.White)
.borderRadius(15)
.shadow({ radius: 6, color: '#ccc', offsetX: 2, offsetY: 4 })
}
// 任务完成样式
@Extend(Text) function finishedTaskStyle() {
.fontColor('#b1b2b1')
// 中划线
.decoration({ type: TextDecorationType.LineThrough })
}
@Entry
@Component
struct TaskPage {
// 统计信息
@State statInfo: StatInfo = new StatInfo()
// 跨组件双向数据同步传值
//@Provide statInfo: StatInfo = new StatInfo()
build() {
Column({ space: 20 }) {
// 1.任务进度卡片
// @Prop装饰属性(单向数据同步):父组件向子组件传值,传的是变量的值
TaskStatView({finishTask: this.statInfo.finishTask, totalTask: this.statInfo.totalTask}) // 不能传对象
// 2.任务列表组件
// @Link装饰属性(双向数据同步),父组件向子组件传值,必须带上$,表示传的是变量的引用
TaskListView({statInfo: $statInfo}) // 可以传对象
// 布局权重调高,剩下高度都给我(弹性布局),解决滚动时高度自适应屏幕边缘
.layoutWeight(1)
// 3.如果使用 @Provide、@Consume 装饰属性,如:
// 父组件属性:@Provide statInfo: StatInfo = new StatInfo()
// 子组件属性:@Consume statInfo: StatInfo
//TaskStatView() // 调用组件时不需要传参,由系统内部自动帮我们做数组同步。注意:属性类型和属性名要保持一致
}
.width('100%')
.height('100%')
.backgroundColor('#f1f2f3')
}
}
// ======== 1.任务统计组件 ========
@Component
struct TaskStatView {
// @Prop:单向数据同步(相当于传的拷贝,深拷贝),接受父组件传来的值,注意不能初始化
@Prop finishTask: number
@Prop totalTask: 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)
}
}
}
.cardStyle()
.margin({ top: 20 })
.justifyContent(FlexAlign.SpaceEvenly)
}
}
// ======== 2.任务列表组件 ========
@Component
struct TaskListView {
// @Link:双向数据同步(相当于传的引用,浅拷贝),接受父组件传来的值,注意不能初始化
@Link statInfo: StatInfo
// 任务数组
@State tasks: Task[] = []
// 处理任务变更时,更新数据
handleTaskChange() {
this.statInfo.totalTask = this.tasks.length
// filter:遍历数组,筛选出满足指定条件的元素
this.statInfo.finishTask = this.tasks.filter(task => task.finished).length
}
build() {
// 注意:build下只能有一个根组件
Column({ space: 20 }) {
// 新增任务按钮
Button('新增任务')
.width(200)
.fontSize(20)
.onClick(() => {
// 向数组中添加元素
this.tasks.push(new Task())
// 处理任务变更
this.handleTaskChange()
})
// 任务列表
List() {
ForEach(this.tasks, (item: Task, index) => {
ListItem() {
// 传递方法,相当于iOS中的block
// 注意:方法传递时,要绑定当前this,不然会导致this变化,方法内部调用报错!
TaskItemView({item: item, onTaskChange: this.handleTaskChange.bind(this)})
}
// 通过构建函数传入自定义删除按钮
.swipeAction({ end: this.DeleteButton(index) })
})
}
.width('100%')
.height('100%')
// List中元素居中对齐
.alignListItem(ListItemAlign.Center)
// 布局权重调高,剩下高度都给我(弹性布局),解决滚动时高度自适应屏幕边缘
.layoutWeight(1)
}
}
// 构建函数:自定义删除按钮
@Builder DeleteButton(index: number) {
Button() {
Image($r('app.media.app_icon'))
.fillColor(Color.White)
.width(20)
}
.width(40)
.height(40)
.type(ButtonType.Circle)
.backgroundColor(Color.Red)
.margin({left: 10})
.onClick(() => {
// 删除数组元素(从index开始,删除1个)
this.tasks.splice(index, 1)
// 处理任务变更
this.handleTaskChange()
})
}
}
// ======== 3.任务卡片组件 ========
@Component
struct TaskItemView {
// Task类也需要加 @Observed 进行装饰
// 使用 @Observed 和 @ObjectLink,item对象属性发生变化,触发视图重新渲染
@ObjectLink item: Task
// 接收父组件传过来的方法(变量是函数类型,相当于iOS中的block回调)
onTaskChange: () => void
build() {
Row() {
// 这里
if (this.item.finished) {
Text(this.item.name)
.fontSize(20)
.finishedTaskStyle()
} else {
Text(this.item.name)
.fontSize(20)
}
// 多选框
Checkbox()
.select(this.item.finished)
.onChange(value => {
this.item.finished = value
// 注意:这里相当于是调用了 handleTaskChange 方法
this.onTaskChange()
})
}
.cardStyle()
.justifyContent(FlexAlign.SpaceBetween)
.margin({bottom: 10})
}
}