ArkUI状态管理
一、状态管理
在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。
自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。下图展示了State和View(UI)之间的关系。

说明如下:
- View(UI):UI渲染,一般指自定义组件的build方法和@Builder装饰的方法内的UI描述。
- State:状态,一般指的是装饰器装饰的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。
二、@State修饰符
@State 装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的 build() 方法刷新UI。 @State 状态数据具有以下特征:
- @State装饰器标记的变量必须初始化,不能为空值
- @state支持object、class、string、number、boolean、enum类型以及这些类型的数组
- 嵌套类型以及数组中的对象属性无法触发视图更新
- 标记为
@State的属性是私有变量,只能在组件内访问。
2.1.@State修饰符案例
创建StateExample01.ets,代码如下:
@Entry @Component struct StateExample01 { @State message:string = "Hello World" build() { Column(){ Text(this.message) .fontSize(50) .onClick(()=>{ //变量通过@State修饰,点击修改私有变量,然后会自动修改刷新UI this.message = "Hi Augus" }) } .width("100%").height("100%") .justifyContent(FlexAlign.Center)//主轴方向对齐 } }
预览效果如下:

2.2.@state修饰的私有变量类型
@state支持object、class、string、number、boolean、enum类型以及这些类型的数组,下面演示,点击修改Sutdent对象的年龄属性,点击一次,页面重新渲染一次:
class Student{ sid:number name:string age:number constructor(sid:number,name:string,age:number) { this.sid = sid this.name = name this.age = age } } @Entry @Component struct StateExample02{ //私有变量的值是一个对象 @State s:Student = new Student(2301,"马保国", 73) //@State必须初始化。否则会报错 //@State s:Student build() { Column(){ Text(`${this.s.sid}:${this.s.name}:${this.s.age}`) .fontSize(30) .onClick(()=>{ //变量通过@State修饰,点击修改私有变量(点击一次自增1),然后会自动修改刷新UI this.s.age++ }) } .width("100%").height("100%") .justifyContent(FlexAlign.Center)//主轴方向对齐 } }
预览效果如下:

2.3.嵌套类型的对象属性无法触发视图更新
下面的案例中Student对象嵌套了一个Pet对象,当修改Pet对象属性的时候,是无法触发视图的更新,下面的代码中,点击的时候虽然数据修改了,点击界面并没有修改,代码如下:
class Student{ sid:number name:string age:number //宠物 pet:Pet constructor(sid:number,name:string,age:number,pet:Pet) { this.sid = sid this.name = name this.age = age this.pet = pet } } //宠物 class Pet{ petName:string petAge:number constructor(petName:string,petAge:number) { this.petName = petName this.petAge = petAge } } @Entry @Component struct StateExample03{ //私有变量的值是一个对象 @State s:Student = new Student(2301,"马保国", 73, new Pet("大黄",3)) //@State必须初始化。否则会报错 //@State s:Student build() { Column(){ //修改Student的属性是可以的 Text(`${this.s.sid}:${this.s.name}:${this.s.age}`) .fontSize(30) .onClick(()=>{ //变量通过@State修饰,点击修改私有变量(点击一次自增1),然后会自动修改刷新UI this.s.age++ }) //修改Student的中包含的pet对象的属性值,@State装饰器是做不到的 Text(`${this.s.pet.petName}:${this.s.pet.petAge}`) .fontSize(30) .onClick(()=>{ //点击修改变属性的值 this.s.pet.petAge++ }) } .width("100%").height("100%") .justifyContent(FlexAlign.Center)//主轴方向对齐 } }
预览效果如下:

2.4.数组中的对象属性无法触发视图更新
class Student{ sid:number name:string age:number //宠物 pet:Pet constructor(sid:number,name:string,age:number,pet:Pet) { this.sid = sid this.name = name this.age = age this.pet = pet } } //宠物 class Pet{ petName:string petAge:number constructor(petName:string,petAge:number) { this.petName = petName this.petAge = petAge } } @Entry @Component struct StateExample03{ //私有变量的值是一个对象 @State s:Student = new Student(2301,"马保国", 73, new Pet("大黄",3)) //准备一个数组 @State pets:Pet[] = [new Pet("小白",2300), new Pet("小痴", 1100)] build() { Column({space:20}){ //修改Student的属性是可以的 Text(`${this.s.sid}:${this.s.name}:${this.s.age}`) .fontSize(30) .onClick(()=>{ //变量通过@State修饰,点击修改私有变量(点击一次自增1),然后会自动修改刷新UI this.s.age++ }) //添加宠物 Button("添加").onClick(()=>{ this.pets.push(new Pet("小灰"+1, 10)) }) Text("---------宠物列表------").fontSize(30).width("100%") ForEach(this.pets,(pet:Pet, index)=>{ Row(){ Text(`${pet.petName}:${pet.petAge}`).fontSize(20) Button("修改年龄").onClick(()=>{ //点击后发现修改了数据,但是由于属性属于数组的对象,@State无法让修改后自动渲染 pet.petAge++ }) }.width("100%").justifyContent(FlexAlign.SpaceAround) }) } .width("100%").height("100%") .justifyContent(FlexAlign.Center)//主轴方向对齐 } }
点击修改的年龄是属于,pets数组中对象的属性,使用@State装饰器无法触发视图的渲染,点击页面无法更新,预览效果如下:

三、案例练习
这里实现如下效果,作为后续装饰器讲解的案例代码。

代码如下:
//任务类 class Task{ static id:number = 1; //任务名称,id每次增加1 name:string = `任务${Task.id++}` //任务状态,是否完成 taskStatus:boolean = false } //统一的卡片样式 @Styles function card(){ .width("90%") .padding(20) .backgroundColor(Color.White) .borderRadius(15) //为当前组件添加阴影效果 .shadow({radius:6, color:"1F000000",offsetX:2,offsetY:4}) } @Entry @Component struct StatusManagement { //总任务数量 @State totalTask:number = 0 //已完成数量 @State finishTask:number = 0 //保存添加任务的数组 @State tasks: Task[] = [] //将跟新数据的操作进一步抽取 DataUpdate(){ //需要跟新一下任务总量(就是任务数组的长度) this.totalTask = this.tasks.length //跟新已完成任务总数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length } //自定义删除删除 @Builder DeleteTaskButton(index:number){ Button(){ Image($r("app.media.icon_remove_button")) .width(20) .fillColor("#B0E0E6") } .width(40) .height(40) .type(ButtonType.Circle) .onClick(()=>{ //去数组中删除 this.tasks.splice(index, 1) /*//需要跟新一下任务总量(就是任务数组的长度) this.totalTask = this.tasks.length //跟新已完成任务总数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length*/ //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) .backgroundColor(Color.Red) .margin(10) } build() { Column({space:20}){ //1.任务进度 Row(){ Text("任务进度:") .fontSize(30) //字体大小 .fontWeight(FontWeight.Bold)//字体加粗 //环形和数字要使用堆叠容器, Stack(){ //环形组件: 进度、总量、样式 Progress({value:this.finishTask, total:this.totalTask,type:ProgressType.Ring}) .width(90) Row(){//让数字显示在一起,放在一个容器中 //任务完成量 Text(`${this.finishTask}`) .fontSize(25) //字体大小 .fontColor("#0000CD") //任务总量 Text(` / ${this.totalTask}`) .fontSize(25) //字体大小 } } } .width("100%") .margin({top:20,bottom:20}) .justifyContent(FlexAlign.SpaceAround) //主轴方向布局 .card() //2.添加任务按钮 Button("添加任务") .width(200) .onClick(()=>{ //1.添加任务,就是给任务数组中添加一个值 this.tasks.push(new Task()) //2.新增任务后,需要跟新一下任务总量(就是任务数组的长度) this.totalTask = this.tasks.length }) //3.任务列表 List({space:5}){ ForEach(this.tasks,(item:Task, index:number)=>{ ListItem(){ Row(){ //文本 Text(item.name).fontColor(20) //单选框,select决定是否选中,类型布尔值,取Task对象属性taskStatus Checkbox() .select(item.taskStatus) .onChange((value:boolean)=>{ //1.更新当前已完成任务状态,勾选后修改状态为true item.taskStatus = value /*//2.统计已完成的数量,就是统计数组中状态为true的元素个数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length*/ //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) } .width("100%") .card() .justifyContent(FlexAlign.SpaceBetween) } /** * 用于设置ListItem的划出组件。 * - start: ListItem向右划动时item左边的组件(List垂直布局时)或ListItem向下划动时item上方的组件(List水平布局时)。 * - end: ListItem向左划动时item右边的组件(List垂直布局时)或ListItem向上划动时item下方的组件(List水平布局时)。 * - edgeEffect: 滑动效果。 */ .swipeAction({end: this.DeleteTaskButton(index)}) }) } .width("100%") .layoutWeight(1) //忽略元素本身尺寸设置,表示自适应占满剩余空间。 .alignListItem(ListItemAlign.Center) //ListItem在List交叉轴方向的布局方式(这里就是水平方向居中对齐),默认为首部对齐。 } .size({width:"100%",height:"100%"}) .backgroundColor("#F0F8FF") } }
四、@Prop和@Link
之前章节的时候,我们吧所有的代码都写在一起,这样会导致代码的可读性很差,所以我们会把功能封装成不同的组件

这时候在父子组件需要进行数据同步的时候,可以通过@Prop和@Link装饰器来做到。在父组件中用@State装饰,在自组件中用@Prop或@Link装饰。

说明:
- @Prop用于子组件只监听父组件的数据改变而改变,自己不对数据改变,俗称单项同步
- @Link用于子组件与父组件都会对数据改变,都需要在数据改变的时候发生相应的更新。俗称双向同步
4.1.@Prop装饰器
将章节二中的代码,数据统计和展示分别抽取成两个子组件,这里先抽取出来数据统计部分,代码如下:
//任务类 class Task{ static id:number = 1; //任务名称,id每次增加1 name:string = `任务${Task.id++}` //任务状态,是否完成 taskStatus:boolean = false } //统一的卡片样式 @Styles function card(){ .width("90%") .padding(20) .backgroundColor(Color.White) .borderRadius(15) //为当前组件添加阴影效果 .shadow({radius:6, color:"1F000000",offsetX:2,offsetY:4}) } @Entry @Component struct StatusManagement { //总任务数量 @State totalTask:number = 0 //已完成数量 @State finishTask:number = 0 //保存添加任务的数组 @State tasks: Task[] = [] //将跟新数据的操作进一步抽取 DataUpdate(){ //需要跟新一下任务总量(就是任务数组的长度) this.totalTask = this.tasks.length //跟新已完成任务总数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length } //自定义删除删除 @Builder DeleteTaskButton(index:number){ Button(){ Image($r("app.media.icon_remove_button")) .width(20) .fillColor("#B0E0E6") } .width(40) .height(40) .type(ButtonType.Circle) .onClick(()=>{ //去数组中删除 this.tasks.splice(index, 1) /*//需要跟新一下任务总量(就是任务数组的长度) this.totalTask = this.tasks.length //跟新已完成任务总数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length*/ //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) .backgroundColor(Color.Red) .margin(10) } build() { Column({space:20}){ //1.任务进度 这里直接调用自定义的组件 TaskStatusProgress({totalTask:this.totalTask, finishTask: this.finishTask}) //2.添加任务按钮 Button("添加任务") .width(200) .onClick(()=>{ //1.添加任务,就是给任务数组中添加一个值 this.tasks.push(new Task()) //2.新增任务后,需要跟新一下任务总量(就是任务数组的长度) this.totalTask = this.tasks.length }) //3.任务列表 List({space:5}){ ForEach(this.tasks,(item:Task, index:number)=>{ ListItem(){ Row(){ //文本 Text(item.name).fontColor(20) //单选框,select决定是否选中,类型布尔值,取Task对象属性taskStatus Checkbox() .select(item.taskStatus) .onChange((value:boolean)=>{ //1.更新当前已完成任务状态,勾选后修改状态为true item.taskStatus = value /*//2.统计已完成的数量,就是统计数组中状态为true的元素个数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length*/ //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) } .width("100%") .card() .justifyContent(FlexAlign.SpaceBetween) } /** * 用于设置ListItem的划出组件。 * - start: ListItem向右划动时item左边的组件(List垂直布局时)或ListItem向下划动时item上方的组件(List水平布局时)。 * - end: ListItem向左划动时item右边的组件(List垂直布局时)或ListItem向上划动时item下方的组件(List水平布局时)。 * - edgeEffect: 滑动效果。 */ .swipeAction({end: this.DeleteTaskButton(index)}) }) } .width("100%") .layoutWeight(1) //忽略元素本身尺寸设置,表示自适应占满剩余空间。 .alignListItem(ListItemAlign.Center) //ListItem在List交叉轴方向的布局方式(这里就是水平方向居中对齐),默认为首部对齐。 } .size({width:"100%",height:"100%"}) .backgroundColor("#F0F8FF") } } /** * 定义任务进度组件 * 使用@Prop装饰器,监控父组件的数据状态,而改变自身的数据 */ @Component struct TaskStatusProgress { //TODO “@Prop”、“@Link”修饰的变量不允许在本地初始化 //总任务数量 @Prop totalTask:number //已完成数量 @Prop finishTask:number build() { //1.任务进度 Row(){ Text("任务进度:") .fontSize(30) //字体大小 .fontWeight(FontWeight.Bold)//字体加粗 //环形和数字要使用堆叠容器, Stack(){ //环形组件: 进度、总量、样式 Progress({value:this.finishTask, total:this.totalTask,type:ProgressType.Ring}) .width(90) Row(){//让数字显示在一起,放在一个容器中 //任务完成量 Text(`${this.finishTask}`) .fontSize(25) //字体大小 .fontColor("#0000CD") //任务总量 Text(` / ${this.totalTask}`) .fontSize(25) //字体大小 } } } .width("100%") .margin({top:20,bottom:20}) .justifyContent(FlexAlign.SpaceAround) //主轴方向布局 .card() } }
上面的代码将任务进度抽取成组件TaskStatusProgress ,然后在调用即可,但是需要注意的是,作为子组件TaskStatusProgress ,只需要监控父组件的任务总量和已完成任务的值,然后自己进行渲染即可,并不需要改变数据,所以在TaskStatusProgress 子组件中定义任务总量和任务进度变量的时候,使用@Prop装饰器。
4.2.@Link装饰器
将新增任务按钮和任务列表抽取成第二个子组件TaskList,由于TaskList子组件本身需要修改数据(任务总量和已完成任务进度),同时父组件需要感知到子组件的修改,将数据传入到上一章节定义TaskStatusProgress子组件中,进行数据展示,所以这是一个双向的数据同步,需要在子组件中定义变量任务总量和已完成任务的时候使用@Link装饰器实现双向的数据同步。但是需要注意的是,在父组件调用TaskLink子组件的时候,传入参数的时候需要使用$,同时不能使用this,才可以如下:
//2.任务列表 TaskList({totalTask: $totalTask, finishTask:$finishTask})
子组件TaskList如下:
/** * 定义任务列表子组件 */ @Component struct TaskList { //总任务数量 @Link totalTask:number //已完成数量 @Link finishTask:number //保存添加任务的数组 @State tasks: Task[] = [] //将跟新数据的操作进一步抽取 DataUpdate(){ //需要跟新一下任务总量(就是任务数组的长度) this.totalTask = this.tasks.length //跟新已完成任务总数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length } //自定义删除删除 @Builder DeleteTaskButton(index:number){ Button(){ Image($r("app.media.icon_remove_button")) .width(20) .fillColor("#B0E0E6") } .width(40) .height(40) .type(ButtonType.Circle) .onClick(()=>{ //去数组中删除 this.tasks.splice(index, 1) /*//需要跟新一下任务总量(就是任务数组的长度) this.totalTask = this.tasks.length //跟新已完成任务总数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length*/ //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) .backgroundColor(Color.Red) .margin(10) } build() { Column(){ //2.添加任务按钮 Button("添加任务") .width(200) .onClick(()=>{ //1.添加任务,就是给任务数组中添加一个值 this.tasks.push(new Task()) //2.新增任务后,需要跟新一下任务总量(就是任务数组的长度) this.totalTask = this.tasks.length }) //3.任务列表 List({space:5}){ ForEach(this.tasks,(item:Task, index:number)=>{ ListItem(){ Row(){ //文本 Text(item.name).fontColor(20) //单选框,select决定是否选中,类型布尔值,取Task对象属性taskStatus Checkbox() .select(item.taskStatus) .onChange((value:boolean)=>{ //1.更新当前已完成任务状态,勾选后修改状态为true item.taskStatus = value /*//2.统计已完成的数量,就是统计数组中状态为true的元素个数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length*/ //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) } .width("100%") .card() .justifyContent(FlexAlign.SpaceBetween) } /** * 用于设置ListItem的划出组件。 * - start: ListItem向右划动时item左边的组件(List垂直布局时)或ListItem向下划动时item上方的组件(List水平布局时)。 * - end: ListItem向左划动时item右边的组件(List垂直布局时)或ListItem向上划动时item下方的组件(List水平布局时)。 * - edgeEffect: 滑动效果。 */ .swipeAction({end: this.DeleteTaskButton(index)}) }) } .width("100%") .layoutWeight(1) //忽略元素本身尺寸设置,表示自适应占满剩余空间。 .alignListItem(ListItemAlign.Center) //ListItem在List交叉轴方向的布局方式(这里就是水平方向居中对齐),默认为首部对齐。 }.width("100%").height("100%") } }
完整的代码如下:
//任务类 class Task{ static id:number = 1; //任务名称,id每次增加1 name:string = `任务${Task.id++}` //任务状态,是否完成 taskStatus:boolean = false } //统一的卡片样式 @Styles function card(){ .width("90%") .padding(20) .backgroundColor(Color.White) .borderRadius(15) //为当前组件添加阴影效果 .shadow({radius:6, color:"1F000000",offsetX:2,offsetY:4}) } @Entry @Component struct StatusManagement { //总任务数量 @State totalTask:number = 0 //已完成数量 @State finishTask:number = 0 //保存添加任务的数组 //@State tasks: Task[] = [] build() { Column({space:20}){ //1.任务进度 这里直接调用自定义的组件 TaskStatusProgress({totalTask:this.totalTask, finishTask: this.finishTask}) //2.任务列表 TaskList({totalTask: $totalTask, finishTask:$finishTask}) } .size({width:"100%",height:"100%"}) .backgroundColor("#F0F8FF") } } /** * 定义任务进度组件 * 使用@Prop装饰器,监控父组件的数据状态,而改变自身的数据 */ @Component struct TaskStatusProgress { //TODO “@Prop”、“@Link”修饰的变量不允许在本地初始化 //总任务数量 @Prop totalTask:number //已完成数量 @Prop finishTask:number build() { //1.任务进度 Row(){ Text("任务进度:") .fontSize(30) //字体大小 .fontWeight(FontWeight.Bold)//字体加粗 //环形和数字要使用堆叠容器, Stack(){ //环形组件: 进度、总量、样式 Progress({value:this.finishTask, total:this.totalTask,type:ProgressType.Ring}) .width(90) Row(){//让数字显示在一起,放在一个容器中 //任务完成量 Text(`${this.finishTask}`) .fontSize(25) //字体大小 .fontColor("#0000CD") //任务总量 Text(` / ${this.totalTask}`) .fontSize(25) //字体大小 } } } .width("100%") .margin({top:20,bottom:20}) .justifyContent(FlexAlign.SpaceAround) //主轴方向布局 .card() } } /** * 定义任务列表子组件 */ @Component struct TaskList { //总任务数量 @Link totalTask:number //已完成数量 @Link finishTask:number //保存添加任务的数组 @State tasks: Task[] = [] //将跟新数据的操作进一步抽取 DataUpdate(){ //需要跟新一下任务总量(就是任务数组的长度) this.totalTask = this.tasks.length //跟新已完成任务总数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length } //自定义删除删除 @Builder DeleteTaskButton(index:number){ Button(){ Image($r("app.media.icon_remove_button")) .width(20) .fillColor("#B0E0E6") } .width(40) .height(40) .type(ButtonType.Circle) .onClick(()=>{ //去数组中删除 this.tasks.splice(index, 1) /*//需要跟新一下任务总量(就是任务数组的长度) this.totalTask = this.tasks.length //跟新已完成任务总数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length*/ //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) .backgroundColor(Color.Red) .margin(10) } build() { Column(){ //2.添加任务按钮 Button("添加任务") .width(200) .onClick(()=>{ //1.添加任务,就是给任务数组中添加一个值 this.tasks.push(new Task()) //2.新增任务后,需要跟新一下任务总量(就是任务数组的长度) this.totalTask = this.tasks.length }) //3.任务列表 List({space:5}){ ForEach(this.tasks,(item:Task, index:number)=>{ ListItem(){ Row(){ //文本 Text(item.name).fontColor(20) //单选框,select决定是否选中,类型布尔值,取Task对象属性taskStatus Checkbox() .select(item.taskStatus) .onChange((value:boolean)=>{ //1.更新当前已完成任务状态,勾选后修改状态为true item.taskStatus = value /*//2.统计已完成的数量,就是统计数组中状态为true的元素个数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length*/ //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) } .width("100%") .card() .justifyContent(FlexAlign.SpaceBetween) } /** * 用于设置ListItem的划出组件。 * - start: ListItem向右划动时item左边的组件(List垂直布局时)或ListItem向下划动时item上方的组件(List水平布局时)。 * - end: ListItem向左划动时item右边的组件(List垂直布局时)或ListItem向上划动时item下方的组件(List水平布局时)。 * - edgeEffect: 滑动效果。 */ .swipeAction({end: this.DeleteTaskButton(index)}) }) } .width("100%") .layoutWeight(1) //忽略元素本身尺寸设置,表示自适应占满剩余空间。 .alignListItem(ListItemAlign.Center) //ListItem在List交叉轴方向的布局方式(这里就是水平方向居中对齐),默认为首部对齐。 }.width("100%").height("100%") } }
4.2.变量数据类型说明
@Prop和@Link变量类型和初始化方式说明如下:

需要注意的是,数据同步的时候:
- @Prop父组件是对象类型,则子组件是对象属性
- @Link父子类型一致
1)Prop父组件变量是对象类型,则子组件是对象属性,这里以TaskStatusProgress任务进度子组件进行演示,因为TaskList必须是双向同步,父组件才可以知道数据变化,必须使用@Link
//任务类 class Task{ static id:number = 1; //任务名称,id每次增加1 name:string = `任务${Task.id++}` //任务状态,是否完成 taskStatus:boolean = false } //统一的卡片样式 @Styles function card(){ .width("90%") .padding(20) .backgroundColor(Color.White) .borderRadius(15) //为当前组件添加阴影效果 .shadow({radius:6, color:"1F000000",offsetX:2,offsetY:4}) } //将统计信息抽取出来形成一个类 class StateInfo{ //总任务数量 totalTask:number //已完成数量 finishTask:number constructor( totalTask:number = 0,finishTask:number = 0 ) { this.totalTask = totalTask this.finishTask = finishTask } } @Entry @Component struct StatusManagement { //TODO 父子组件变量类型是对象, @Prop子组件变量类型是对象的属性 //创建统计信息对象 @State stat: StateInfo = new StateInfo() build() { Column({space:20}){ //1.任务进度 这里直接调用自定义的组件,使用的是@Prop,通过属性传入 TaskStatusProgress({totalTask:this.stat.totalTask, finishTask: this.stat.finishTask}) //2.任务列表 //TODO 子组件使用的@Link, 通过$符的方式传值 TaskList({stat:$stat}) } .size({width:"100%",height:"100%"}) .backgroundColor("#F0F8FF") } } /** * 定义任务进度组件 * 使用@Prop装饰器,监控父组件的数据状态,而改变自身的数据 */ @Component struct TaskStatusProgress { //TODO 父组件是对象,子组件则可以使用“@Prop”作为对象的属性 //总任务数量 @Prop totalTask:number //已完成数量 @Prop finishTask:number build() { //1.任务进度 Row(){ Text("任务进度:") .fontSize(30) //字体大小 .fontWeight(FontWeight.Bold)//字体加粗 //环形和数字要使用堆叠容器, Stack(){ //环形组件: 进度、总量、样式 Progress({value:this.finishTask, total:this.totalTask,type:ProgressType.Ring}) .width(90) Row(){//让数字显示在一起,放在一个容器中 //任务完成量 Text(`${this.finishTask}`) .fontSize(25) //字体大小 .fontColor("#0000CD") //任务总量 Text(` / ${this.totalTask}`) .fontSize(25) //字体大小 } } } .width("100%") .margin({top:20,bottom:20}) .justifyContent(FlexAlign.SpaceAround) //主轴方向布局 .card() } } /** * 定义任务列表子组件 */ @Component struct TaskList { //TODO @Link stat: StateInfo //保存添加任务的数组 @State tasks: Task[] = [] //将跟新数据的操作进一步抽取 DataUpdate(){ //需要跟新一下任务总量(就是任务数组的长度) this.stat.totalTask = this.tasks.length //跟新已完成任务总数 this.stat.finishTask = this.tasks.filter(item=> item.taskStatus).length } //自定义删除删除 @Builder DeleteTaskButton(index:number){ Button(){ Image($r("app.media.icon_remove_button")) .width(20) .fillColor("#B0E0E6") } .width(40) .height(40) .type(ButtonType.Circle) .onClick(()=>{ //去数组中删除 this.tasks.splice(index, 1) //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) .backgroundColor(Color.Red) .margin(10) } build() { Column(){ //2.添加任务按钮 Button("添加任务") .width(200) .onClick(()=>{ //1.添加任务,就是给任务数组中添加一个值 this.tasks.push(new Task()) //2.新增任务后,需要跟新一下任务总量(就是任务数组的长度) this.stat.totalTask = this.tasks.length }) //3.任务列表 List({space:5}){ ForEach(this.tasks,(item:Task, index:number)=>{ ListItem(){ Row(){ //文本 Text(item.name).fontColor(20) //单选框,select决定是否选中,类型布尔值,取Task对象属性taskStatus Checkbox() .select(item.taskStatus) .onChange((value:boolean)=>{ //1.更新当前已完成任务状态,勾选后修改状态为true item.taskStatus = value /*//2.统计已完成的数量,就是统计数组中状态为true的元素个数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length*/ //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) } .width("100%") .card() .justifyContent(FlexAlign.SpaceBetween) } /** * 用于设置ListItem的划出组件。 * - start: ListItem向右划动时item左边的组件(List垂直布局时)或ListItem向下划动时item上方的组件(List水平布局时)。 * - end: ListItem向左划动时item右边的组件(List垂直布局时)或ListItem向上划动时item下方的组件(List水平布局时)。 * - edgeEffect: 滑动效果。 */ .swipeAction({end: this.DeleteTaskButton(index)}) }) } .width("100%") .layoutWeight(1) //忽略元素本身尺寸设置,表示自适应占满剩余空间。 .alignListItem(ListItemAlign.Center) //ListItem在List交叉轴方向的布局方式(这里就是水平方向居中对齐),默认为首部对齐。 }.width("100%").height("100%") } }
2)@Link演示,父子组件变量同为对象
//任务类 class Task{ static id:number = 1; //任务名称,id每次增加1 name:string = `任务${Task.id++}` //任务状态,是否完成 taskStatus:boolean = false } //统一的卡片样式 @Styles function card(){ .width("90%") .padding(20) .backgroundColor(Color.White) .borderRadius(15) //为当前组件添加阴影效果 .shadow({radius:6, color:"1F000000",offsetX:2,offsetY:4}) } //将统计信息抽取出来形成一个类 class StateInfo{ //总任务数量 totalTask:number //已完成数量 finishTask:number constructor( totalTask:number = 0,finishTask:number = 0 ) { this.totalTask = totalTask this.finishTask = finishTask } } @Entry @Component struct StatusManagement { //TODO @Link 父子组件变量类型都可以是对象 //创建统计信息对象 @State stat: StateInfo = new StateInfo() build() { Column({space:20}){ //1.任务进度 这里直接调用自定义的组件 TaskStatusProgress({totalTask:this.stat.totalTask, finishTask: this.stat.finishTask}) //2.任务列表 //TODO 这里任然使用$参数名的形式 TaskList({stat:$stat}) } .size({width:"100%",height:"100%"}) .backgroundColor("#F0F8FF") } } /** * 定义任务进度组件 * 使用@Prop装饰器,监控父组件的数据状态,而改变自身的数据 */ @Component struct TaskStatusProgress { //TODO “@Prop”、“@Link”修饰的变量不允许在本地初始化 //总任务数量 @Prop totalTask:number //已完成数量 @Prop finishTask:number build() { //1.任务进度 Row(){ Text("任务进度:") .fontSize(30) //字体大小 .fontWeight(FontWeight.Bold)//字体加粗 //环形和数字要使用堆叠容器, Stack(){ //环形组件: 进度、总量、样式 Progress({value:this.finishTask, total:this.totalTask,type:ProgressType.Ring}) .width(90) Row(){//让数字显示在一起,放在一个容器中 //任务完成量 Text(`${this.finishTask}`) .fontSize(25) //字体大小 .fontColor("#0000CD") //任务总量 Text(` / ${this.totalTask}`) .fontSize(25) //字体大小 } } } .width("100%") .margin({top:20,bottom:20}) .justifyContent(FlexAlign.SpaceAround) //主轴方向布局 .card() } } /** * 定义任务列表子组件 */ @Component struct TaskList { //TODO @Link 父子组件变量类型都可以是对象 //总任务数量 @Link stat:StateInfo //保存添加任务的数组 @State tasks: Task[] = [] //将跟新数据的操作进一步抽取 DataUpdate(){ //需要跟新一下任务总量(就是任务数组的长度) this.stat.totalTask = this.tasks.length //跟新已完成任务总数 this.stat.finishTask = this.tasks.filter(item=> item.taskStatus).length } //自定义删除删除 @Builder DeleteTaskButton(index:number){ Button(){ Image($r("app.media.icon_remove_button")) .width(20) .fillColor("#B0E0E6") } .width(40) .height(40) .type(ButtonType.Circle) .onClick(()=>{ //去数组中删除 this.tasks.splice(index, 1) /*//需要跟新一下任务总量(就是任务数组的长度) this.totalTask = this.tasks.length //跟新已完成任务总数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length*/ //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) .backgroundColor(Color.Red) .margin(10) } build() { Column(){ //2.添加任务按钮 Button("添加任务") .width(200) .onClick(()=>{ //1.添加任务,就是给任务数组中添加一个值 this.tasks.push(new Task()) //2.新增任务后,需要跟新一下任务总量(就是任务数组的长度) this.stat.totalTask = this.tasks.length }) //3.任务列表 List({space:5}){ ForEach(this.tasks,(item:Task, index:number)=>{ ListItem(){ Row(){ //文本 Text(item.name).fontColor(20) //单选框,select决定是否选中,类型布尔值,取Task对象属性taskStatus Checkbox() .select(item.taskStatus) .onChange((value:boolean)=>{ //1.更新当前已完成任务状态,勾选后修改状态为true item.taskStatus = value /*//2.统计已完成的数量,就是统计数组中状态为true的元素个数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length*/ //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) } .width("100%") .card() .justifyContent(FlexAlign.SpaceBetween) } /** * 用于设置ListItem的划出组件。 * - start: ListItem向右划动时item左边的组件(List垂直布局时)或ListItem向下划动时item上方的组件(List水平布局时)。 * - end: ListItem向左划动时item右边的组件(List垂直布局时)或ListItem向上划动时item下方的组件(List水平布局时)。 * - edgeEffect: 滑动效果。 */ .swipeAction({end: this.DeleteTaskButton(index)}) }) } .width("100%") .layoutWeight(1) //忽略元素本身尺寸设置,表示自适应占满剩余空间。 .alignListItem(ListItemAlign.Center) //ListItem在List交叉轴方向的布局方式(这里就是水平方向居中对齐),默认为首部对齐。 }.width("100%").height("100%") } }
五、@Provide和Consume
@Provide和Consume可以跨组件提供类似于@State和@Link的双向同步。如下图所示:

但是需要注意 :
- @Provide:父组件使用
- @Consume:子组件或者后代组件使用
- 同时在在调用子组件或者后代组件的时候,子组件或者后代组件定义了参数,也是不需要传入,会自动隐式的传入
代码案例如下:
//任务类 class Task{ static id:number = 1; //任务名称,id每次增加1 name:string = `任务${Task.id++}` //任务状态,是否完成 taskStatus:boolean = false } //统一的卡片样式 @Styles function card(){ .width("90%") .padding(20) .backgroundColor(Color.White) .borderRadius(15) //为当前组件添加阴影效果 .shadow({radius:6, color:"1F000000",offsetX:2,offsetY:4}) } //将统计信息抽取出来形成一个类 class StateInfo{ //总任务数量 totalTask:number //已完成数量 finishTask:number constructor( totalTask:number = 0,finishTask:number = 0 ) { this.totalTask = totalTask this.finishTask = finishTask } } @Entry @Component struct StatusManagement { //TODO 父子组件变量类型是对象, @Prop子组件变量类型是对象的属性 //创建统计信息对象 @Provide stat: StateInfo = new StateInfo() build() { Column({space:20}){ //1.任务进度 这里直接调用自定义的组件,使用的是@Prop,通过属性传入 TaskStatusProgress() //2.任务列表 //TODO 子组件使用的@Link, 通过$符的方式传值 TaskList() } .size({width:"100%",height:"100%"}) .backgroundColor("#F0F8FF") } } /** * 定义任务进度组件 * 使用@Prop装饰器,监控父组件的数据状态,而改变自身的数据 */ @Component struct TaskStatusProgress { //TODO 通过@Consume实现双向同步,调用组件的时候不需要传入值,会自动传入 @Consume stat: StateInfo build() { //1.任务进度 Row(){ Text("任务进度:") .fontSize(30) //字体大小 .fontWeight(FontWeight.Bold)//字体加粗 //环形和数字要使用堆叠容器, Stack(){ //环形组件: 进度、总量、样式 Progress({value:this.stat.finishTask, total:this.stat.totalTask,type:ProgressType.Ring}) .width(90) Row(){//让数字显示在一起,放在一个容器中 //任务完成量 Text(`${this.stat.finishTask}`) .fontSize(25) //字体大小 .fontColor("#0000CD") //任务总量 Text(` / ${this.stat.totalTask}`) .fontSize(25) //字体大小 } } } .width("100%") .margin({top:20,bottom:20}) .justifyContent(FlexAlign.SpaceAround) //主轴方向布局 .card() } } /** * 定义任务列表子组件 */ @Component struct TaskList { //TODO 通过@Consume实现双向同步,调用组件的时候不需要传入值,会自动传入 @Consume stat: StateInfo //保存添加任务的数组 @State tasks: Task[] = [] //将跟新数据的操作进一步抽取 DataUpdate(){ //需要跟新一下任务总量(就是任务数组的长度) this.stat.totalTask = this.tasks.length //跟新已完成任务总数 this.stat.finishTask = this.tasks.filter(item=> item.taskStatus).length } //自定义删除删除 @Builder DeleteTaskButton(index:number){ Button(){ Image($r("app.media.icon_remove_button")) .width(20) .fillColor("#B0E0E6") } .width(40) .height(40) .type(ButtonType.Circle) .onClick(()=>{ //去数组中删除 this.tasks.splice(index, 1) //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) .backgroundColor(Color.Red) .margin(10) } build() { Column(){ //2.添加任务按钮 Button("添加任务") .width(200) .onClick(()=>{ //1.添加任务,就是给任务数组中添加一个值 this.tasks.push(new Task()) //2.新增任务后,需要跟新一下任务总量(就是任务数组的长度) this.stat.totalTask = this.tasks.length }) //3.任务列表 List({space:5}){ ForEach(this.tasks,(item:Task, index:number)=>{ ListItem(){ Row(){ //文本 Text(item.name).fontColor(20) //单选框,select决定是否选中,类型布尔值,取Task对象属性taskStatus Checkbox() .select(item.taskStatus) .onChange((value:boolean)=>{ //1.更新当前已完成任务状态,勾选后修改状态为true item.taskStatus = value /*//2.统计已完成的数量,就是统计数组中状态为true的元素个数 this.finishTask = this.tasks.filter(item=> item.taskStatus).length*/ //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) } .width("100%") .card() .justifyContent(FlexAlign.SpaceBetween) } /** * 用于设置ListItem的划出组件。 * - start: ListItem向右划动时item左边的组件(List垂直布局时)或ListItem向下划动时item上方的组件(List水平布局时)。 * - end: ListItem向左划动时item右边的组件(List垂直布局时)或ListItem向上划动时item下方的组件(List水平布局时)。 * - edgeEffect: 滑动效果。 */ .swipeAction({end: this.DeleteTaskButton(index)}) }) } .width("100%") .layoutWeight(1) //忽略元素本身尺寸设置,表示自适应占满剩余空间。 .alignListItem(ListItemAlign.Center) //ListItem在List交叉轴方向的布局方式(这里就是水平方向居中对齐),默认为首部对齐。 }.width("100%").height("100%") } }
预览效果如下:

六、@Observed和@objectLink
@objectLink和@observed装饰器用于在涉及嵌套对象或数组元素为对象的场景中进行双向数据同步
6.1.案例1
以之前的学生信息展示的基础案例中,点击修改学生宠物年龄的功能和修改宠物列表中宠物信息,修改后无法同步为例,原因在于:
- 学生的宠物年龄,是属于对象的嵌套
- 宠物列表是属于数组中有对象
要解决上面的问题,就需要@Observed和@objectLink装饰器来实现
1)需要给嵌套的对象和数组中对象添加@Observed装饰器,Pet对象属于嵌套的所以添加装饰器
class Student{ sid:number name:string age:number //宠物 pet:Pet constructor(sid:number,name:string,age:number,pet:Pet) { this.sid = sid this.name = name this.age = age this.pet = pet } } @Observed //实现双向数据同步 //宠物 class Pet{ petName:string petAge:number constructor(petName:string,petAge:number) { this.petName = petName this.petAge = petAge } }
2)将需要修改重新渲染的功能抽取出来定义子组件,然后给变量添加@objectLink注解
/** * 数组元素为对象,实现数据同步 */ @Component struct PetList { //子组件的变量必须使用@ObjectLink @ObjectLink pet:Pet build() { Row(){ Text(`${this.pet.petName}:${this.pet.petAge}`).fontSize(20) Button("修改年龄").onClick(()=>{ //点击后发现修改了数据,但是由于属性属于数组的对象,@State无法让修改后自动渲染 this.pet.petAge++ }) }.width("100%").justifyContent(FlexAlign.SpaceAround) } } /** * 嵌套对象,实现数据同步 */ @Component struct PetInfo { //子组件的变量必须使用@ObjectLink @ObjectLink pet:Pet build() { //修改Student的属性是可以的 Text(`宠物:${this.pet.petName},${this.pet.petAge}`) .fontSize(30) } }
注意:其中的对象嵌套,学生对象里面有个宠物对象,这里在定义的时候,接受的参数一定是宠物对象
3)调用定义的子组件
@Entry @Component struct StateExample03{ //私有变量的值是一个对象 @State s:Student = new Student(2301,"马保国", 73, new Pet("大黄",3)) //准备一个数组 @State pets:Pet[] = [new Pet("小白",2300), new Pet("小痴", 1100)] build() { Column({space:20}){ /** * 数组元素为对象,实现数据同步 * 调用PetInfo, 这里的this.s.pet是属于student对象的pet属性 */ PetInfo({pet:this.s.pet}) .onClick(()=>{ //变量通过@State修饰,点击修改私有变量(点击一次自增1),然后会自动修改刷新UI this.s.pet.petAge++ }) //添加宠物 Button("添加").onClick(()=>{ this.pets.push(new Pet("小灰"+1, 10)) }) Text("---------宠物列表------").fontSize(30).width("100%") ForEach(this.pets,(pet:Pet, index)=>{ /** * 嵌套对象,实现数据同步 * 调用PetList */ PetList({pet:pet}) .onClick(()=>{ //变量通过@State修饰,点击修改私有变量(点击一次自增1),然后会自动修改刷新UI this.s.pet.petAge++ }) }) } .width("100%").height("100%") .justifyContent(FlexAlign.Center)//主轴方向对齐 } }
6.1.案例2
还是任务进度列表案例,之前的功能还剩余一部分,当任务完成后,任务的名称需要置灰并且出现中划线,效果如下所示:

1)在任务类上添加装饰器@Observed
//任务类 @Observed class Task{ static id:number = 1; //任务名称,id每次增加1 name:string = `任务${Task.id++}` //任务状态,是否完成 taskStatus:boolean = false }
2)在任务列表中渲染任务组件功能抽取出来形成子组件,里面使用@ObjectLink装饰器修饰变量
//任务列表置灰加下划线样式组件 @Extend(Text) function finishedTask(){ .decoration({type:TextDecorationType.LineThrough}) //LineThrough .fontColor("#B1B2B1") } /** * 这个由于任务列表里面存放的对象,所以需要使用@objectLink,实现双向同步,抽取组件 */ @Component struct TaskItem { //双向同步数组中的对象 @ObjectLink item:Task //由于数据更新函数,在父组件TaskList,无法移动到这里,所以需要把父组件中的数据跟新的函数DataUpdate(),当成参数传递给子组件 onChangeTask: ()=>void //表示onChangeTask是一个无参返回值为void的函数 build() { Row(){ //TODO 判断是否是完成状态,如果是完成状态,则修改为置灰加中划线 if(this.item.taskStatus){ Text(this.item.name).finishedTask() //调用定义的样式组件 }else { //文本 Text(this.item.name).fontColor(20) } //单选框,select决定是否选中,类型布尔值,取Task对象属性taskStatus Checkbox() .select(this.item.taskStatus) .onChange((value:boolean)=>{ //1.更新当前已完成任务状态,勾选后修改状态为true this.item.taskStatus = value //2.上面的更新数据进一步封装,然后调用 this.onChangeTask() //更新数据方法在父组件,当成参数传递到这里,然后调用 }) } .width("100%") .card() .justifyContent(FlexAlign.SpaceBetween) } }
3)在任务列表组件中调用上面封装的子组件 TaskItem,代码如下:
/** * 定义任务列表子组件 */ @Component struct TaskList { //TODO 通过@Consume实现双向同步,调用组件的时候不需要传入值,会自动传入 @Consume stat: StateInfo //保存添加任务的数组 @State tasks: Task[] = [] //将跟新数据的操作进一步抽取 DataUpdate(){ //需要跟新一下任务总量(就是任务数组的长度) this.stat.totalTask = this.tasks.length //跟新已完成任务总数 this.stat.finishTask = this.tasks.filter(item=> item.taskStatus).length } //自定义删除删除 @Builder DeleteTaskButton(index:number){ Button(){ Image($r("app.media.icon_remove_button")) .width(20) .fillColor("#B0E0E6") } .width(40) .height(40) .type(ButtonType.Circle) .onClick(()=>{ //去数组中删除 this.tasks.splice(index, 1) //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) .backgroundColor(Color.Red) .margin(10) } build() { Column(){ //2.添加任务按钮 Button("添加任务") .width(200) .onClick(()=>{ //1.添加任务,就是给任务数组中添加一个值 this.tasks.push(new Task()) //2.新增任务后,需要跟新一下任务总量(就是任务数组的长度) this.stat.totalTask = this.tasks.length }) //3.任务列表 List({space:5}){ ForEach(this.tasks,(item:Task, index:number)=>{ ListItem(){ //实现数组中对象数据的同步,调用封装的子组件 //this.DataUpdate.bind(this)将函数当成参数传递过去,bind(this)表示使用父组件TaskList的对象,因为更新的数据在父组件TaskList中 TaskItem({item:item, onChangeTask:this.DataUpdate.bind(this)}) } /** * 用于设置ListItem的划出组件。 * - start: ListItem向右划动时item左边的组件(List垂直布局时)或ListItem向下划动时item上方的组件(List水平布局时)。 * - end: ListItem向左划动时item右边的组件(List垂直布局时)或ListItem向上划动时item下方的组件(List水平布局时)。 * - edgeEffect: 滑动效果。 */ .swipeAction({end: this.DeleteTaskButton(index)}) }) } .width("100%") .layoutWeight(1) //忽略元素本身尺寸设置,表示自适应占满剩余空间。 .alignListItem(ListItemAlign.Center) //ListItem在List交叉轴方向的布局方式(这里就是水平方向居中对齐),默认为首部对齐。 }.width("100%").height("100%") } }
这里有个新的问题,新定义的子组件TaskItem中没有数据更新的方法DataUpdate,这时候无法更新数据,而更新数据的方法在TaskList中,为了能在子组件中调用父组件的函数,就需要在组件中定义一个参数为函数,调用的时候把数据更新方法当做函数传入即可,语法如下:

调用的时候,数据更新的方法DataUpdate,更新的数据也在父组件中,所以需要指定是修改的父组件中的数据(绑定父组件的this),如下:

4)完整的代码如下:
//任务类 @Observed class Task{ static id:number = 1; //任务名称,id每次增加1 name:string = `任务${Task.id++}` //任务状态,是否完成 taskStatus:boolean = false } //统一的卡片样式 @Styles function card(){ .width("90%") .padding(20) .backgroundColor(Color.White) .borderRadius(15) //为当前组件添加阴影效果 .shadow({radius:6, color:"1F000000",offsetX:2,offsetY:4}) } //将统计信息抽取出来形成一个类 class StateInfo{ //总任务数量 totalTask:number //已完成数量 finishTask:number constructor( totalTask:number = 0,finishTask:number = 0 ) { this.totalTask = totalTask this.finishTask = finishTask } } @Entry @Component struct StatusManagement { //TODO 父子组件变量类型是对象, @Prop子组件变量类型是对象的属性 //创建统计信息对象 @Provide stat: StateInfo = new StateInfo() build() { Column({space:20}){ //1.任务进度 这里直接调用自定义的组件,使用的是@Prop,通过属性传入 TaskStatusProgress() //2.任务列表 //TODO 子组件使用的@Link, 通过$符的方式传值 TaskList() } .size({width:"100%",height:"100%"}) .backgroundColor("#F0F8FF") } } /** * 定义任务进度组件 * 使用@Prop装饰器,监控父组件的数据状态,而改变自身的数据 */ @Component struct TaskStatusProgress { //TODO 通过@Consume实现双向同步,调用组件的时候不需要传入值,会自动传入 @Consume stat: StateInfo build() { //1.任务进度 Row(){ Text("任务进度:") .fontSize(30) //字体大小 .fontWeight(FontWeight.Bold)//字体加粗 //环形和数字要使用堆叠容器, Stack(){ //环形组件: 进度、总量、样式 Progress({value:this.stat.finishTask, total:this.stat.totalTask,type:ProgressType.Ring}) .width(90) Row(){//让数字显示在一起,放在一个容器中 //任务完成量 Text(`${this.stat.finishTask}`) .fontSize(25) //字体大小 .fontColor("#0000CD") //任务总量 Text(` / ${this.stat.totalTask}`) .fontSize(25) //字体大小 } } } .width("100%") .margin({top:20,bottom:20}) .justifyContent(FlexAlign.SpaceAround) //主轴方向布局 .card() } } /** * 定义任务列表子组件 */ @Component struct TaskList { //TODO 通过@Consume实现双向同步,调用组件的时候不需要传入值,会自动传入 @Consume stat: StateInfo //保存添加任务的数组 @State tasks: Task[] = [] //将跟新数据的操作进一步抽取 DataUpdate(){ //需要跟新一下任务总量(就是任务数组的长度) this.stat.totalTask = this.tasks.length //跟新已完成任务总数 this.stat.finishTask = this.tasks.filter(item=> item.taskStatus).length } //自定义删除删除 @Builder DeleteTaskButton(index:number){ Button(){ Image($r("app.media.icon_remove_button")) .width(20) .fillColor("#B0E0E6") } .width(40) .height(40) .type(ButtonType.Circle) .onClick(()=>{ //去数组中删除 this.tasks.splice(index, 1) //上面的更新数据进一步封装,然后调用 this.DataUpdate() }) .backgroundColor(Color.Red) .margin(10) } build() { Column(){ //2.添加任务按钮 Button("添加任务") .width(200) .onClick(()=>{ //1.添加任务,就是给任务数组中添加一个值 this.tasks.push(new Task()) //2.新增任务后,需要跟新一下任务总量(就是任务数组的长度) this.stat.totalTask = this.tasks.length }) //3.任务列表 List({space:5}){ ForEach(this.tasks,(item:Task, index:number)=>{ ListItem(){ //实现数组中对象数据的同步,调用封装的子组件 //this.DataUpdate.bind(this)将函数当成参数传递过去,bind(this)表示使用父组件TaskList的对象,因为更新的数据在父组件TaskList中 TaskItem({item:item, onChangeTask:this.DataUpdate.bind(this)}) } /** * 用于设置ListItem的划出组件。 * - start: ListItem向右划动时item左边的组件(List垂直布局时)或ListItem向下划动时item上方的组件(List水平布局时)。 * - end: ListItem向左划动时item右边的组件(List垂直布局时)或ListItem向上划动时item下方的组件(List水平布局时)。 * - edgeEffect: 滑动效果。 */ .swipeAction({end: this.DeleteTaskButton(index)}) }) } .width("100%") .layoutWeight(1) //忽略元素本身尺寸设置,表示自适应占满剩余空间。 .alignListItem(ListItemAlign.Center) //ListItem在List交叉轴方向的布局方式(这里就是水平方向居中对齐),默认为首部对齐。 }.width("100%").height("100%") } } //任务列表置灰加下划线样式组件 @Extend(Text) function finishedTask(){ .decoration({type:TextDecorationType.LineThrough}) //LineThrough .fontColor("#B1B2B1") } /** * 这个由于任务列表里面存放的对象,所以需要使用@objectLink,实现双向同步,抽取组件 */ @Component struct TaskItem { //双向同步数组中的对象 @ObjectLink item:Task //由于数据更新函数,在父组件TaskList,无法移动到这里,所以需要把父组件中的数据跟新的函数DataUpdate(),当成参数传递给子组件 onChangeTask: ()=>void //表示onChangeTask是一个无参返回值为void的函数 build() { Row(){ //TODO 判断是否是完成状态,如果是完成状态,则修改为置灰加中划线 if(this.item.taskStatus){ Text(this.item.name).finishedTask() //调用定义的样式组件 }else { //文本 Text(this.item.name).fontColor(20) } //单选框,select决定是否选中,类型布尔值,取Task对象属性taskStatus Checkbox() .select(this.item.taskStatus) .onChange((value:boolean)=>{ //1.更新当前已完成任务状态,勾选后修改状态为true this.item.taskStatus = value //2.上面的更新数据进一步封装,然后调用 this.onChangeTask() //更新数据方法在父组件,当成参数传递到这里,然后调用 }) } .width("100%") .card() .justifyContent(FlexAlign.SpaceBetween) } }
七、@StorageLink装饰器
7.1.@StorageLink
@StorageLink(key)是和AppStorage中key对应的属性建立双向数据同步:
- 本地修改发生,该修改会被写回AppStorage中;
- AppStorage中的修改发生后,该修改会被同步到所有绑定AppStorage对应key的属性上,包括单向(@StorageProp和通过Prop创建的单向绑定变量)、双向(@StorageLink和通过Link创建的双向绑定变量)变量和其他实例(比如PersistentStorage)。
这段内容的意思应该是@StorageLink要和AppStorage配合使用,@StorageLink的特点:
- @StorageLink(key) 装饰的变量是组件的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法进行UI刷新。
- 一个@StorageLink装饰的数据被修改后,所有相同key的@StorageLink变量都会随之改变,不管你是哪一个页面。
- @StorageLink的作用域是全局的,在应用程序运行期间,在任何页面的任何组件中都可以通过键值访问它的值,它的生命周期取决于应用程序的生命周期。
- 双向同步:在父组件中修改@StorageLink变量会同步到子组件中相同key的@StorageLink变量,反之亦然。
- 所有@StorageLink变量必须初始化。
- @StorageLink支持的数据类型同@State类似,但是它还支持object
- @StorageLink可通过PersistentStorage接口持久化
- 所有相同key的@StorageLink变量,他们的初始值以第一个初始化的值为准
7.2.装饰器使用规则说明
|
@StorageProp变量装饰器 |
说明 |
|---|---|
|
装饰器参数 |
key:常量字符串,必填(字符串需要有引号)。 |
|
允许装饰的变量类型 |
Object class、string、number、boolean、enum类型,以及这些类型的数组。嵌套类型的场景请参考观察变化和行为表现。 类型必须被指定,且必须和AppStorage中对应属性相同。不支持any,不允许使用undefined和null。 |
|
同步类型 |
单向同步:从AppStorage的对应属性到组件的状态变量。 组件本地的修改是允许的,但是AppStorage中给定的属性一旦发生变化,将覆盖本地的修改。 |
|
被装饰变量的初始值 |
必须指定,如果AppStorage实例中不存在属性,则作为初始化默认值,并存入AppStorage中。 |
7.3.数据绑定
编写案例代码:
@Entry @Component struct EntryComponent { @StorageLink('key1') link1: string = 'hello' @StorageLink('key1') link2: string = 'hello' build() { Column({ space: 20 }) { Text(this.link1).fontSize(30) .onClick(() => { this.link2 = '你好' }) } } }
运行应用,Text的内容显示的是link1的值"hello"

点击Text组件后,link2的值被修改为"你好",此时link1的值也随之变为"你好",build方法被调用,刷新UI,如图:

从上例中可以看出,link1和link2的值是绑定的,不管你修改哪一个值,另一个都会同步修改,同时build方法被调用。
7.4.双向同步
@Component struct ChildComponent { @StorageLink('key1') link: string = '' build() { Text('child:' + this.link).fontSize(30).fontColor(Color.Red) .onClick(() => { this.link = '你好,鸿蒙' }) } } @Entry @Component struct EntryComponent { @StorageLink('key1') link1: string = 'hello' build() { Column({ space: 20 }) { Text('parent:' + this.link1).fontSize(30).fontColor(Color.Blue) .onClick(() => { this.link1 = 'hello HarmonyOS' }) ChildComponent() } .width('100%') .alignItems(HorizontalAlign.Center) } }
在入口组件中绑定了键为"key1"的@StorageLink变量link1,在子组件中绑定了相同key的变量link2,当点击父组件的文本时,link1的值被修改,子组件的link2同步变动,父组件和子组件的build方法被调用,当点击子组件的文本时,link2的值被修改,父组件的link1同步变动,子组件和父组件的build方法被调用。

7.5.页面间通信
编写测试页面Index.ets
// index.ets @Entry @Component struct EntryComponent { @StorageLink('key1') link1: string = 'hello' build() { Column({ space: 20 }) { Text(this.link1).fontSize(50) .onClick(() => { this.link1 = 'hello HarmonyOS' }) // 跳转到page2 Navigator({ target: 'pages/page2'}) { Button('go to page2').fontSize(30) } } .width('100%') .alignItems(HorizontalAlign.Center) } }
右键new -> etsPage,创建一个名为page2的页面,代码如下
// page2.ets @Entry @Component struct Page2 { @StorageLink('key1') link: string = '' build() { Column({ space: 20 }) { Text(this.link).fontSize(30) .onClick(() => { this.link = "你好鸿蒙" }) // 返回index页面 Navigator({ target: 'pages/index', type: NavigationType.Back }) { Button('back').fontSize(30) } } .width('100%') } }
运行效果如图:

7.6.持久化
@StorageLink是可以被PersistentStorage接口持久化的,使用起来也非常简单,只要一行就OK
PersistentStorage.PersistProp("key1", "hello")
上面的第一个参数key1就是我们想要持久化的@StorageLink的key,第二个参数是默认值,案例代码如下:
PersistentStorage.PersistProp("key1", "hello")
@Entry
@Component
struct EntryComponent {
@StorageLink('key1') link1: string = 'hello'
@StorageLink('key1') link2: string = 'hello'
build() {
Column({ space: 20 }) {
Text(this.link1).fontSize(50)
.onClick(() => {
this.link2 = '你好,鸿蒙!'
})
}
}
}
运行效果:

即使应用程序的生命周期结束了,@StorageLink绑定的变量依然是被修改后的值,说明该变量确实被持久化了。@StorageLink装饰器最重要的还是能被PersistentStorage持久化,这一点是@State,@Link,@Prop这些都不能比的。

浙公网安备 33010602011771号