HarmonyOS 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)
  }
}

鸿蒙全栈开发全新学习指南

也为了积极培养鸿蒙生态人才,让大家都能学习到鸿蒙开发最新的技术,针对一些在职人员、0基础小白、应届生/计算机专业、鸿蒙爱好者等人群,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线[包含了大APP实战项目开发]。

本路线共分为四个阶段:

第一阶段:鸿蒙初中级开发必备技能

第二阶段:鸿蒙南北双向高工技能基础:https://qr21.cn/Bm8gyp

第三阶段:应用开发中高级就业技术

第四阶段:全网首发-工业级南向设备开发就业技术:https://qr21.cn/Bm8gyp

《鸿蒙 (Harmony OS)开发学习手册》(共计892页)

如何快速入门?

1.基本概念
2.构建第一个ArkTS应用
3.……

开发基础知识:https://qr21.cn/Bm8gyp

1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……

基于ArkTS 开发

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

鸿蒙开发面试真题(含参考答案):https://qr21.cn/Bm8gyp

鸿蒙入门教学视频:

美团APP实战开发教学:https://qr21.cn/Bm8gyp

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:https://qr21.cn/FV7h05

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/577267.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

店匠科技技术产品闪耀,引领新质生产力发展

在科技飞速发展的今天,新质生产力正成为推动社会进步和经济高质量发展的核心力量。店匠科技,作为一家致力于为全球B2C电商提供产品和技术解决方案的领先企业,其技术产品不仅体现了新质生产力的创新特质,更在推动电商行业转型升级中发挥了重要作用。 新质生产力,以创新为主导,摆…

嵌入式开发一:初识Stm32

目录 一、嵌入式简介 1.1 嵌入式概念 1.2 嵌入式系统的组成 1.3 嵌入式的分类 1.3.1 嵌入式系统的分类 1.3.2 嵌入式处理器的分类 二、单片机简介(MCU嵌入式微控制器) 2.1 单片机是什么 2.2 单片机的作用是什么 2.3 单片机的发展历程 2.4 单片机发展趋势 2.5 复杂指…

将图片添加描述批量写入excel

原始图片 写入excel的效果 代码 # by zengxy chatgpt # from https://blog.csdn.net/imwatersimport os import xlsxwriter from PIL import Imageclass Image2Xlsx():def __init__(self,xls_path,head_list[编号, 图片, 名称, "描述",备注],set_default_y112,se…

Java毕业设计 基于SpringBoot vue城镇保障性住房管理系统

Java毕业设计 基于SpringBoot vue城镇保障性住房管理系统 SpringBoot 城镇保障性住房管理系统 功能介绍 首页 图片轮播 房源信息 房源详情 申请房源 公示信息 公示详情 登录注册 个人中心 留言反馈 后台管理 登录 个人中心 修改密码 个人信息 用户管理 房屋类型 房源信息管理…

Eudic欧路词典for Mac:专业英语学习工具

Eudic欧路词典for Mac,作为专为Mac用户设计的英语学习工具,凭借其简捷高效的特点,成为众多英语学习者不可或缺的助手。 Eudic欧路词典for Mac v4.6.4激活版下载 这款词典整合了多个权威词典资源,如牛津、柯林斯、朗文等&#xff0…

2024 java easyexcel poi word模板填充数据,多个word合成一个word

先看效果 一、准备工作 1.word模版 2.文件路径 二、pom依赖 <!-- easyexcel --><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>2.1.7</version></dependency><depe…

基于微信小程序云开发实现考研题库小程序V2.0

不久之前&#xff0c;基于云开发的微信答题小程序搭建题库小程序V1.0&#xff0c;软件架构是微信原生小程序云开发。现在来回顾一下&#xff0c;已经实现的功能。 一、V1.0项目预览 1、页面结构 首页 答题页 结果页 我的页 排行榜页 答题历史页 登录页 使用指引页 2…

制造型企业 如何实现便捷的机台文件统一管理?

机台文件统一管理&#xff0c;这是生产制造型企业都需要去做的&#xff0c;机台文件需要统一管理的原因主要包括以下几点&#xff1a; 1、提高效率&#xff1a;统一管理可以简化文件的访问和使用过程&#xff0c;提高工作效率&#xff0c;尤其是在需要频繁访问或更新机台文件的…

【AIGC调研系列】大型语言模型如何减少幻觉生成

在解读大型语言模型&#xff08;LLMs&#xff09;中的长格式事实性问题时&#xff0c;我们首先需要认识到这些模型在生成内容时可能会产生与既定事实不一致的情况&#xff0c;这种情况通常被称为“幻觉”[2][3]。这种现象不仅可能导致信息的误传&#xff0c;还可能对社会造成误…

FORM调用标准AP\AR\GL\FA界面

EBS FORM客户化界面有时候数据需要追溯打开AP\AR\GL\FA等界面&#xff1a; 一种打开日记账的方式&#xff1a; PROCEDURE SHOW_JOURNAL ISparent_form_id FormModule;child_form_id FormModule; BEGINclose_jrn;parent_form_id : FIND_FORM(:SYSTEM.CURRENT_FORM);COPY(TO…

HWOD:蛇形矩阵

一、知识点 忽略蛇形矩阵本身的定义1,2,3,4,5,6&#xff0c;&#xff0c;&#xff0c;&#xff0c;&#xff0c;&#xff0c; 而是从它的输出图形上找规律 二、题目 1、描述 蛇形矩阵是由1开始的自然数依次排列成的一个矩阵上三角形。 例如&#xff0c;当输入5时&#xf…

Magnet for Mac:高效窗口管理工具

Magnet for Mac是一款专为Mac用户设计的窗口管理工具&#xff0c;旨在帮助用户更高效地管理和布局多个应用程序窗口&#xff0c;提升工作效率。 Magnet for Mac v2.14.0中文免激活版下载 这款软件拥有直观易用的界面和丰富的功能&#xff0c;支持用户将屏幕分割成多个区域&…

【UE5】蓝图通信方式

目录 1、直接通信 2、getAllActorsOfClass 3、getAllActorsOfClassWithTag 4、通过射线检测 5、接口 6、事件分发器 7、SpawnActor 8、调用控制台命令 9、关卡蓝图中直接调用 创建两个Actor蓝图 1、直接通信 场景中 2、getAllActorsOfClass 3、getAllActorsOfClassWit…

如何实现直播声卡反向给手机充电功能呢?

在数字化时代的浪潮中&#xff0c;声卡作为多媒体系统的核心组件&#xff0c;扮演着声波与数字信号相互转换的关键角色。它不仅能够将来自各类音源的原始声音信号转换为数字信号&#xff0c;进而输出到各类声响设备&#xff0c;更能够通过音乐设备数字接口(MIDI)发出合成乐器的…

MySQL 数据库远程访问问题

在默认的情况下&#xff0c;MySQL 是不能远程访问的&#xff0c;当我们修改了用户名可以接受远程访问后&#xff0c;还是没有办法接受远程访问。 还有一个配置的地方需要验证。 mysqld.cnf 配置文件 mysqld.cnf 配置文件对访问的地址可能会有限制。 配置文件的地址为&#…

杰发科技AC7840——ADC简介(1)_双路ADC同时使用

0. 简介 1. 特性 2. 双路ADC Sample里面没有双路的&#xff0c;以为那个规则组只有一个通道&#xff0c;看了外设寄存器才发现&#xff0c;原来他的通道是双路的。 注意1: ADC硬件引脚的配置 注意2: 规则组长度设置和 RSEQ序列号和CH通道号组合应该就对应了转换顺序&#xff0…

服务器防护哪家好

在当前的网络安全环境中&#xff0c;服务器防护已经成为企业和个人防御网络威胁的重要一环。选择一个高效且可靠的服务器防护方案是至关重要的。今天我们来看一下为什么安全狗的服务器防护哪家好呢&#xff0c;一起来看看安全狗服务器防护的介绍吧。 首先&#xff0c;安全狗提供…

读天才与算法:人脑与AI的数学思维笔记11_算法如何思考

1. 创造力 1.1. 创建一种算法&#xff0c;其首要任务是放弃已知的所有艺术风格&#xff0c;然后判断由算法自己所产生的艺术品是否具有与所有艺术风格都截然不同的特性&#xff0c;即真正独树一帜的艺术风格 1.2. 抗性模型同样适用于人类创造力代码的引导 1.3. 神经科学家的…

网盘——进入文件夹

本文主要讲解网盘的文件操作中进入文件夹的部分&#xff0c;具体实现步骤如下&#xff1a; 1、具体步骤如下&#xff1a; A、客户端发送进入文件夹的请求&#xff08;该请求包含目录信息以及要进入的文件夹名字&#xff09; B、服务器收到该请求之后&#xff0c;服务器首先判…

SpringBoot---------整合Redis

目录 第一步&#xff1a;引入依赖 第二步&#xff1a;配置Redis信息 第三步&#xff1a;选择Spring Data Redis进行操作Redis数据库 ①操作String类型数据&#xff08;用的少&#xff09; ②操作Object类型数据&#xff08;重要&#xff01;&#xff01;&#xff01;&#x…
最新文章