鸿蒙5:组件状态共享

目录

1. 组件状态共享

1.1 状态共享-父子传值:Local、Param、Event

1.2 状态共享-父子双向绑定!!

1.3 跨代共享:Provider和Consumer

1.3.1 aliasName和属性名

1.3.2 实现跨代共享

1.3.3 装饰复杂类型,配合@Trace一起使用

1.3.4 支持共享方法

1.4 @Monitor装饰器:状态变量修改监听

1.5 综合案例 - 相册图片选取

1.5.1 页面布局,准备一个选择图片的按钮并展示

1.5.2 准备弹层,点击时展示弹层

1.5.3 添加点击事件,设置选中状态

1.5.4 点击确定同步给页面

1.5.5 关闭弹层


1. 组件状态共享

1.1 状态共享-父子传值:Local、Param、Event

  • @Param表示组件从外部传入的状态,使得父子组件之间的数据能够进行同步
  • @Param装饰的变量支持本地初始化,但是不允许在组件内部直接修改变量本身
  • 若不在本地初始化,则需要和@Require装饰器一起使用,要求必须从外部传入初始化
  • 如果要修改值需使用@Event装饰器的能力。

父传子强化:

@Entry
  @ComponentV2
  struct ComponentQuestionCase {
    @Local money: number = 9999;

    build() {
      Column() {
        Text('father:' + this.money)
        Button('父亲存100块')
          .onClick(()=>{
            this.money += 100
          })
        CompQsChild({
          money:this.money
        })
          .margin({ bottom: 30 })

        CompQsChild({
          money:this.money
        })
      }
      .padding(20)
        .width('80%')
        .margin(30)
        .border({ width: 2 })
    }
  }

@ComponentV2
  struct CompQsChild {
    // 若不在本地初始化,则需和@Require装饰器一起使用,要求必须从外部传入初始化
    @Require @Param money: number

    build() {
      Column() {
        Text('child:' + this.money)
        Button('花100块')
          .onClick(()=>{
            // @Param表示组件从外部传入的状态
            // 1. 父子组件之间的数据能够进行同步
            // 2. 不允许在组件内部直接修改变量本身
            //    修改值需使用@Event装饰器的能力
            // this.money -= 100
          })
      }
      .padding(20)
        .backgroundColor(Color.Pink)
    }
  }

  • 为了实现子组件向父组件要求更新@Param装饰变量的能力,开发者可以使用@Event装饰器。
  • 使用@Event装饰回调方法是一种规范,表明子组件需要传入更新数据源的回调。
  • @Event主要配合@Param实现数据的双向同步。

子传父强化:

@Entry
  @ComponentV2
  struct ComponentQuestionCase {
    @Local money: number = 9999;

    build() {
      Column() {
        Text('father:' + this.money)
        Button('父亲存100块')
          .onClick(()=>{
            this.money += 100
          })
        CompQsChild({
          money:this.money,
          payMoney: () => {
            this.money -= 10
          }
        })
          .margin({ bottom: 30 })

        CompQsChild({
          money:this.money,
          payMoney: () => {
            this.money -= 10
          }
        })
      }
      .padding(20)
        .width('80%')
        .margin(30)
        .border({ width: 2 })
    }
  }

@ComponentV2
  struct CompQsChild {
    // 若不在本地初始化,则需和@Require装饰器一起使用,要求必须从外部传入初始化
    @Require @Param money: number
    @Event payMoney: () => void = () => {}
    build() {
      Column() {
        Text('child:' + this.money)
        Button('花10块')
          .onClick(()=>{
            // @Param表示组件从外部传入的状态
            // 1. 父子组件之间的数据能够进行同步
            // 2. 不允许在组件内部直接修改变量本身
            //    修改值需使用@Event装饰器的能力
            // this.money -= 100
            this.payMoney()
          })
      }
      .padding(20)
        .backgroundColor(Color.Pink)
    }
  }

1.2 状态共享-父子双向绑定!!

场景:组件二次封装 (输入框、下拉菜单....)

!!双向绑定语法,是一个语法糖方便实现数据双向绑定

其中@Event方法名需要声明为“$”+ @Param属性名。

@Entry
  @ComponentV2
  struct Test {
    @Local words: string = 'admin'
    build() {
      Column({ space: 20 }) {
        CustomizeInput({
          text: this.words!!
        })
        Text('输入框内容: ' + this.words)
      }
      .padding(20)
    }
  }


@ComponentV2
  struct CustomizeInput {
    @Param text: string = ''
    @Event $text: (val: string) => void = (val: string) => {};
    build() {
      Column() {
        TextInput({
          text: this.text
        })
          .border({ width: 2, color: Color.Blue })
          .onChange((value) => {
            this.$text(value)
          })
      }
    }
  }

简化逻辑:

CustomizeInput({
  text: this.words!!
})
CustomizeInput({ 
  text: this.words, 
  $text: (val: number) => { 
    this.words = val 
  }
})

1.3 跨代共享:Provider和Consumer

如果组件层级特别多,ArkTS支持跨组件传递状态数据来实现双向同步@Provider 和 @Consumer

  • @Provider,即数据提供方
    • 其所有的子组件都可以通过@Consumer绑定相同的key来获取@Provider提供的数据
  • @Consumer,即数据消费方
    • 可以通过绑定同样的key获取其最近父节点的@Provider的数据
    • 当查找不到@Provider的数据时,使用本地默认值。

@Provider和@Consumer装饰数据类型需要一致。

开发者在使用@Provider和@Consumer时要注意:

  • @Provider和@Consumer强依赖自定义组件层级,@Consumer会因为所在组件的父组件不同,而被初始化为不同的值。
  • @Provider和@Consumer相当于把组件粘合在一起了,从组件独立角度,要减少使用@Provider和@Consumer。

1.3.1 aliasName和属性名

@Provider和@Consumer接受可选参数aliasName,没有配置参数时,使用属性名作为默认的aliasName。

说明

aliasName是用于@Provider和@Consumer进行匹配的唯一指定key。

@ComponentV2
struct Parent {
  // 未定义aliasName, 使用属性名'str'作为aliasName
  @Provider() str: string = 'hello';
}

@ComponentV2
struct Child {
  // 定义aliasName为'str',使用aliasName去寻找
  // 能够在Parent组件上找到, 使用@Provider的值'hello'
  @Consumer('str') str: string = 'world';
}

@ComponentV2
struct Parent {
  // 定义aliasName为'alias'
  @Provider('alias') str: string = 'hello';
}

@ComponentV2 struct Child {
  // 定义aliasName为 'alias',找到@Provider并获得值'hello'
  @Consumer('alias') str: string = 'world';
}

1.3.2 实现跨代共享

  • 静态结构:
@Entry
  @ComponentV2
  struct ComponentQuestionCase1 {
    build() {
      Column() {
        Text('father: 20000')
        Button('存100块')
          .onClick(() => {

          })
        CompQsChild1()
          .margin({ top: 20 })
      }
      .padding(20)
        .margin(30)
        .border({ width: 2 })
        .width('80%')
    }
  }

@ComponentV2
  struct CompQsChild1 {
    build() {
      Column() {
        Text('child儿子: xxx')
        Button('花100块')
          .onClick(() => {

          })
        ChildChild1()
          .margin({ top: 20 })
      }
      .padding(20)
        .backgroundColor(Color.Pink)
    }
  }

@ComponentV2
  struct ChildChild1 {
    build() {
      Column() {
        Text('ChildChild孙子: xxx')
        Button('花100块')
          .onClick(() => {

          })
      }
      .padding(20)
        .backgroundColor(Color.Orange)
    }
  }
  • 实现共享:
@Entry
  @ComponentV2
  struct ComponentQuestionCase1 {
    @Provider('totalMoney') money: number = 50000
    build() {
      Column() {
        Text('father: ' + this.money)
        Button('存100块')
          .onClick(() => {
            this.money += 100
          })
        CompQsChild1()
          .margin({ top: 20 })
      }
      .padding(20)
        .margin(30)
        .border({ width: 2 })
        .width('80%')
    }
  }

@ComponentV2
  struct CompQsChild1 {
    @Consumer('totalMoney') money: number = 0
    build() {
      Column() {
        Text('child儿子: ' + this.money)
        Button('花100块')
          .onClick(() => {
            this.money -= 100
          })
        ChildChild1()
          .margin({ top: 20 })
      }
      .padding(20)
        .backgroundColor(Color.Pink)
    }
  }

@ComponentV2
  struct ChildChild1 {
    @Consumer('totalMoney') money: number = 0
    build() {
      Column() {
        Text('ChildChild孙子: ' + this.money)
        Button('花100块')
          .onClick(() => {
            this.money -= 100
          })
      }
      .padding(20)
        .backgroundColor(Color.Orange)
    }
  }
1.3.3 装饰复杂类型,配合@Trace一起使用

@Provider和@Consumer只能观察到数据本身的变化。

如果当其装饰复杂数据类型,需要观察属性的变化时,需要配合@Trace一起使用。

interface ICar {
  brand: string
  price: number
}

@ObservedV2
  class Car {
    @Trace brand: string = '宝马'
    @Trace price: number = 200000
    constructor(obj: ICar) {
      this.brand = obj.brand
      this.price = obj.price
    }
  }

@Entry
  @ComponentV2
  struct ComponentQuestionCase2 {
    @Provider('totalMoney') money: number = 50000
    @Provider() car: Car = new Car({
      brand: '奔驰',
      price: 150000
    })
    build() {
      Column() {
        Text('father: ' + this.money)
        Text(this.car.brand + "_" + this.car.price)
        Row() {
          Button('存100块')
            .onClick(() => {
              this.money += 100
            })
          Button('换车')
            .onClick(() => {
              this.car.brand = '三轮车'
            })
        }
        CompQsChild2()
          .margin({ top: 20 })
      }
      .padding(20)
        .margin(30)
        .border({ width: 2 })
        .width('80%')
    }
  }

@ComponentV2
  struct CompQsChild2 {
    @Consumer('totalMoney') money: number = 0
    build() {
      Column() {
        Text('child儿子: ' + this.money)
        Button('花100块')
          .onClick(() => {
            this.money -= 100
          })
        ChildChild2()
          .margin({ top: 20 })
      }
      .padding(20)
        .backgroundColor(Color.Pink)
    }
  }

@ComponentV2
  struct ChildChild2 {
    @Consumer('totalMoney') money: number = 0
    @Consumer() car: Car = new Car({} as ICar)
    build() {
      Column() {
        Text('ChildChild孙子: ' + this.money)
        Text(this.car.brand + '_' + this.car.price)
        Row() {
          Button('花100块')
            .onClick(() => {
              this.money -= 100
            })
          Button('换车').onClick(() => {
            this.car.brand = '小黄车'
          })
        }
      }
      .padding(20)
        .backgroundColor(Color.Orange)
    }
  }

1.3.4 支持共享方法

当需要在父组件中向子组件注册回调函数时,可以使用@Provider和@Consumer装饰回调方法来解决。

比如输入提交,当输入提交时,如果希望将子孙组件提交的信息同步给父组件

可以参考下面的例子:

import { promptAction } from '@kit.ArkUI'

@Entry
@ComponentV2
struct Parent {
  @Provider() onSubmit: (txt: string) => void = (txt: string) => {
    promptAction.showDialog({
      message: txt
    })
  }
  build() {
    Column() {
      Child()
    }
  }
}

@ComponentV2
struct Child {
  @Local txt: string = ''
  @Consumer() onSubmit: (txt: string) => void = (txt: string) => {};
  build() {
    Column() {
      TextInput({ text: $$this.txt })
        .onSubmit(() => {
          this.onSubmit(this.txt)
        })
    }
  }
}

注意:@Provider重名时,@Consumer向上查找其最近的@Provider

1.4 @Monitor装饰器:状态变量修改监听

为了增强对状态变量变化的监听能力,开发者可以使用@Monitor装饰器对状态变量进行监听。

@Monitor装饰器用于监听状态变量修改,使得状态变量具有深度监听的能力

  • @Monitor装饰器支持在@ComponentV2装饰的自定义组件中使用,未被状态变量装饰器@Local@Param@Provider@Consumer@Computed装饰的变量无法被@Monitor监听到变化。
  • @Monitor装饰器支持在类中与@ObservedV2、@Trace配合使用,不允许在未被@ObservedV2装饰的类中使用@Monitor装饰器。未被@Trace装饰的属性无法被@Monitor监听到变化。
  • 单个@Monitor装饰器能够同时监听多个属性的变化,当这些属性在一次事件中共同变化时,只会触发一次@Monitor的回调方法。

@Monitor与@Watch对比:

基础语法:

@Entry
@ComponentV2
struct Index {
  @Local username: string = "帅鹏";
  @Local age: number = 24;

  @Monitor('username')
  onNameChange(monitor: IMonitor) {
    console.log('姓名变化了', this.username)
  }

  // 监视多个变量 / 拿到变化前的值
  // @Monitor("username", "age")
  // onInfoChange(monitor: IMonitor) {
  //   // monitor.dirty.forEach((path: string) => {
  //   //   console.log(`${path} changed from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`)
  //   // })
  //   // console.log(JSON.stringify(monitor.dirty))
  //
  //   monitor.dirty.forEach((path: string) => {
  //     console.log(`${path}改变了, 从 ${monitor.value(path)?.before} 改成了 ${monitor.value(path)?.now}`)
  //   })
  // }

  build() {
    Column() {
      Text(this.username)
      Text(this.age.toString())
      Button("修改信息").onClick(() => {
        this.username = '张三'
      })
    }
    .width('100%')
  }
}

在@ObservedV2装饰的类中使用@Monitor:

import { promptAction } from '@kit.ArkUI';

@ObservedV2
class Info {
  @Trace name: string = "吕布";
  age: number = 25;

  // name被@Trace装饰,能够监听变化
  @Monitor("name")
  onNameChange(monitor: IMonitor) {
    promptAction.showDialog({
      message: `姓名修改, 从 ${monitor.value()?.before} 改成了 ${monitor.value()?.now}`
    })
  }
  // age未被@Trace装饰,不能监听变化
  @Monitor("age")
  onAgeChange(monitor: IMonitor) {
    console.log(`年纪修改, 从 ${monitor.value()?.before} 改成了 ${monitor.value()?.now}`);
  }
}
@Entry
@ComponentV2
struct Index {
  info: Info = new Info();
  @Monitor('info.name')
  onNameChange () {
    promptAction.showDialog({
      message: this.info.name
    })
  }
  
  build() {
    Column() {
      Text(this.info.name + this.info.age)
      Button("修改名字")
        .onClick(() => {
          this.info.name = "张飞"; // 能够触发onNameChange方法
        })
      Button("修改年纪")
        .onClick(() => {
          this.info.age = 26; // 不能够触发onAgeChange方法
        })
    }
  }
}

1.5 综合案例 - 相册图片选取

基于我们已经学习过的父子、跨代、状态监听,我们来做一个综合案例

分析:

1.准备一个用于选择图片的按钮,点击展示弹层

2.准备弹层,渲染所有图片

3.图片添加点击事件,点击时检测选中数量后添加选中状态

4.点击确定,将选中图片同步给页面并关闭弹层

5.取消时,关闭弹层

1.5.1 页面布局,准备一个选择图片的按钮并展示

  • 选择图片Builder

@Builder
function SelectImageIcon() {
  Row() {
    Image($r('sys.media.ohos_ic_public_add'))
      .width('100%')
      .height('100%')
      .fillColor(Color.Gray)
  }
  .width('100%')
  .height('100%')
  .padding(20)
  .backgroundColor('#f5f7f8')
  .border({
    width: 1,
    color: Color.Gray,
    style: BorderStyle.Dashed
  })
}

  • 页面布局,使用Builder

@Entry
@ComponentV2
struct ImageSelectCase {
  build() {
    Grid() {
      GridItem() {
        SelectImageIcon()
      }.aspectRatio(1)
    }
    .padding(20)
    .width('100%')
    .height('100%')
    .rowsGap(10)
    .columnsGap(10)
    .columnsTemplate('1fr 1fr 1fr')
  }
}

1.5.2 准备弹层,点击时展示弹层

准备弹层组件:

@ComponentV2
struct SelectImage {
  @Param imageList:ResourceStr[] = []
  build() {
    Column() {
      Row() {
        Text('取消')
        Text('已选中 0/9 张')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        Text('确定')
      }.width('100%').padding(20)
      Grid() {
        ForEach(this.imageList, (item: ResourceStr) => {
          GridItem() {
            Image(item)
          }.aspectRatio(1)
        })
      }
      .padding(20)
      .layoutWeight(1)
      .rowsGap(10)
      .columnsGap(10)
      .columnsTemplate('1fr 1fr 1fr')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f5f7f8')
  }
}
export { SelectImage }

控制显示:

import { SelectImage } from '../components/SelectImage'
@Entry
@ComponentV2
struct ImageSelectCase {
  @Local showDialog: boolean = false
  @Local imageList: ResourceStr[] = [
    "assets/1.webp",
    "assets/2.webp",
    "assets/3.webp",
    "assets/4.webp",
    "assets/5.webp",
    "assets/6.webp",
    "assets/7.webp",
    "assets/8.webp",
    "assets/9.webp",
    "assets/10.webp"
  ]

  @Builder
  ImageListBuilder() {
    // 大坑:最外层必须得是容器组件
    Column(){
      SelectImage({imageList:this.imageList})
    }
  }

  build() {
    Grid() {
      GridItem() {
        SelectImageIcon()
      }.aspectRatio(1)
      .onClick(() => {
        this.showDialog = true
      })
    }
    .padding(20)
    .width('100%')
    .height('100%')
    .rowsGap(10)
    .columnsGap(10)
    .columnsTemplate('1fr 1fr 1fr')
    .bindSheet($$this.showDialog, this.ImageListBuilder(), { 
      showClose: false, 
      height: '60%' 
    })
  }
}

@Builder
function SelectImageIcon() {
  Row() {
    Image($r('sys.media.ohos_ic_public_add'))
      .width('100%')
      .height('100%')
      .fillColor(Color.Gray)
  }
  .width('100%')
  .height('100%')
  .padding(20)
  .backgroundColor('#f5f7f8')
  .border({
    width: 1,
    color: Color.Gray,
    style: BorderStyle.Dashed
  })
}
1.5.3 添加点击事件,设置选中状态

  • 对图片进行改造,统一添加点击事件,并声明一个选中的列表用来收集选中的图片

@ComponentV2
struct SelectImage {
  @Param imageList:ResourceStr[] = []
  @Local selectList: ResourceStr[] = []

  build() {
    Column() {
      Row() {
        Text('取消')
        Text(`已选中${this.selectList.length}/9 张`)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        Text('确定')
      }.width('100%').padding(20)
      Grid() {
        ForEach(this.imageList, (item: ResourceStr) => {
          GridItem() {
            Stack({ alignContent: Alignment.BottomEnd }) {
              Image(item)
              if (this.selectList.includes(item)) {
                Image($r('sys.media.ohos_ic_public_select_all'))
                  .width(30)
                  .aspectRatio(1)
                  .fillColor('#ff397204')
                  .margin(4)
                  .backgroundColor(Color.White)
              }
            }
          }.aspectRatio(1)
          .onClick(() => {
            this.selectList.push(item)
          })
        })
      }
      .padding(20)
      .layoutWeight(1)
      .rowsGap(10)
      .columnsGap(10)
      .columnsTemplate('1fr 1fr 1fr')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f5f7f8')
  }
}
export { SelectImage }
  1. 已经选中, 再点击就是取消
  2. 没有选中, 再点击就是追加

.onClick(() => {
  if (this.selectList.includes(item)) {
    // 已经选中, 再点击就是取消
    this.selectList = this.selectList.filter(filterItem => filterItem !== item)
  }
  else {
    // 没有选中, 再点击就是追加
    this.selectList.push(item)
  }
})

1.5.4 点击确定同步给页面

  • 父组件传递数据下来,利用!!双向绑定

import { SelectImage } from '../components/SelectImage'

@Entry
@ComponentV2
struct ImageSelectCase {
  @Local showDialog: boolean = false
  @Local imageList: ResourceStr[] = [
    "assets/1.webp",
    "assets/2.webp",
    "assets/3.webp",
    "assets/4.webp",
    "assets/5.webp",
    "assets/6.webp",
    "assets/7.webp",
    "assets/8.webp",
    "assets/9.webp",
    "assets/10.webp"
  ]
  @Local selectedList: ResourceStr[] = []
  @Builder
  ImageListBuilder() {
    // 大坑:最外层必须得是容器组件
    Column(){
      SelectImage({
        imageList: this.imageList,
        selectedList: this.selectedList!!
      })
    }
  }

  build() {
    Grid() {
      ForEach(this.selectedList,(item:ResourceStr)=>{
        GridItem() {
          Image(item)
        }.aspectRatio(1)
      })

      GridItem() {
        SelectImageIcon()
      }.aspectRatio(1)
      .onClick(() => {
        this.showDialog = true
      })
    }
    .padding(20)
    .width('100%')
    .height('100%')
    .rowsGap(10)
    .columnsGap(10)
    .columnsTemplate('1fr 1fr 1fr')
    .bindSheet($$this.showDialog, this.ImageListBuilder(), { showClose: false, height: '60%' })
  }
}

@Builder
function SelectImageIcon() {
  Row() {
    Image($r('sys.media.ohos_ic_public_add'))
      .width('100%')
      .height('100%')
      .fillColor(Color.Gray)
  }
  .width('100%')
  .height('100%')
  .padding(20)
  .backgroundColor('#f5f7f8')
  .border({
    width: 1,
    color: Color.Gray,
    style: BorderStyle.Dashed
  })
}
  • 子组件接受处理

@ComponentV2
struct SelectImage {
  @Param imageList:ResourceStr[] = []
  @Local selectList: ResourceStr[] = []

  @Param selectedList: ResourceStr[] = []
  @Event $selectedList: (val: ResourceStr[]) => void = (val: ResourceStr[]) => {}
  build() {
    Column() {
      Row() {
        Text('取消')
        Text(`已选中${this.selectList.length}/9 张`)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        Text('确定')
          .onClick(() => {
            this.$selectedList([...this.selectList])
          })
      }.width('100%').padding(20)
      Grid() {
        ForEach(this.imageList, (item: ResourceStr) => {
          GridItem() {
            Stack({ alignContent: Alignment.BottomEnd }) {
              Image(item)
              if (this.selectList.includes(item)) {
                Image($r('sys.media.ohos_ic_public_select_all'))
                  .width(30)
                  .aspectRatio(1)
                  .fillColor('#ff397204')
                  .margin(4)
                  .backgroundColor(Color.White)
              }
            }
          }.aspectRatio(1)
          .onClick(() => {
            if (this.selectList.includes(item)) {
              // 已经选中, 再点击就是取消
              this.selectList = this.selectList.filter(filterItem => filterItem !== item)
            }
            else {
              // 没有选中, 再点击就是追加
              this.selectList.push(item)
            }
          })
        })
      }
      .padding(20)
      .layoutWeight(1)
      .rowsGap(10)
      .columnsGap(10)
      .columnsTemplate('1fr 1fr 1fr')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f5f7f8')
  }
}
export { SelectImage }

到这效果基本就完成了,最后一个关闭弹层,你能想到怎么做了吗?

1.5.5 关闭弹层

  • 父组件

@Builder
ImageListBuilder() {
  // 大坑:最外层必须得是容器组件
  Column(){
    SelectImage({
      showDialog: this.showDialog!!,
      imageList: this.imageList,
      selectedList: this.selectedList!!
    })
  }
}
  • 子组件

@Param showDialog: boolean = false
@Event $showDialog: (val: boolean) => void = (val: boolean) => {}

Text('取消').onClick(() => {
  this.$showDialog(false)
})
  • 子组件渲染时,同步一下数据

aboutToAppear(): void {
  this.selectList = [...this.selectedList]
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值