目录
1.1 状态共享-父子传值:Local、Param、Event
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 }
- 已经选中, 再点击就是取消
- 没有选中, 再点击就是追加
.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]
}