目录
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@ComponentV2struct 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 })}}@ComponentV2struct CompQsChild {// 若不在本地初始化,则需和@Require装饰器一起使用,要求必须从外部传入初始化@Require @Param money: numberbuild() {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@ComponentV2struct 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 })}}@ComponentV2struct CompQsChild {// 若不在本地初始化,则需和@Require装饰器一起使用,要求必须从外部传入初始化@Require @Param money: number@Event payMoney: () => void = () => {}build() {Column() {Text('child:' + this.money)Button('花10块').onClick(()=>{// @Param表示组件从外部传入的状态// 1. 父子组件之间的数据能够进行同步// 2. 不允许在组件内部直接修改变量本身// 修改值需使用@Event装饰器的能力// this.money -= 100this.payMoney()})}.padding(20).backgroundColor(Color.Pink)}}
1.2 状态共享-父子双向绑定!!
场景:组件二次封装 (输入框、下拉菜单....)
!!双向绑定语法,是一个语法糖方便实现数据双向绑定
其中@Event方法名需要声明为“$”+ @Param属性名。
@Entry@ComponentV2struct Test {@Local words: string = 'admin'build() {Column({ space: 20 }) {CustomizeInput({text: this.words!!})Text('输入框内容: ' + this.words)}.padding(20)}}@ComponentV2struct 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@ComponentV2struct ComponentQuestionCase1 {build() {Column() {Text('father: 20000')Button('存100块').onClick(() => {})CompQsChild1().margin({ top: 20 })}.padding(20).margin(30).border({ width: 2 }).width('80%')}}@ComponentV2struct CompQsChild1 {build() {Column() {Text('child儿子: xxx')Button('花100块').onClick(() => {})ChildChild1().margin({ top: 20 })}.padding(20).backgroundColor(Color.Pink)}}@ComponentV2struct ChildChild1 {build() {Column() {Text('ChildChild孙子: xxx')Button('花100块').onClick(() => {})}.padding(20).backgroundColor(Color.Orange)}}
- 实现共享:
@Entry@ComponentV2struct ComponentQuestionCase1 {@Provider('totalMoney') money: number = 50000build() {Column() {Text('father: ' + this.money)Button('存100块').onClick(() => {this.money += 100})CompQsChild1().margin({ top: 20 })}.padding(20).margin(30).border({ width: 2 }).width('80%')}}@ComponentV2struct CompQsChild1 {@Consumer('totalMoney') money: number = 0build() {Column() {Text('child儿子: ' + this.money)Button('花100块').onClick(() => {this.money -= 100})ChildChild1().margin({ top: 20 })}.padding(20).backgroundColor(Color.Pink)}}@ComponentV2struct ChildChild1 {@Consumer('totalMoney') money: number = 0build() {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: stringprice: number
}@ObservedV2class Car {@Trace brand: string = '宝马'@Trace price: number = 200000constructor(obj: ICar) {this.brand = obj.brandthis.price = obj.price}}@Entry@ComponentV2struct 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%')}}@ComponentV2struct CompQsChild2 {@Consumer('totalMoney') money: number = 0build() {Column() {Text('child儿子: ' + this.money)Button('花100块').onClick(() => {this.money -= 100})ChildChild2().margin({ top: 20 })}.padding(20).backgroundColor(Color.Pink)}}@ComponentV2struct 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"]@BuilderImageListBuilder() {// 大坑:最外层必须得是容器组件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 }
- 已经选中, 再点击就是取消
- 没有选中, 再点击就是追加
.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[] = []@BuilderImageListBuilder() {// 大坑:最外层必须得是容器组件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]
}