目录
1.创建自定义组件(@Entry、@Component装饰器)
2.自定义组件构建函数创建自定义组件(@Builder装饰器)
8.多层双向传递(@Provider+@Cosume装饰器)
9.嵌套对象或数组元素为对象的双向数据同步(@Observed+@ObjectLink装饰器)(补充)
简介
本系列是windows系统下、采用ArkTS语言、ArkUI框架、deveco studio编译器学习纯鸿蒙软件研发,采用API version 9进行。本小节主要了解鸿蒙开发的各种装饰器(带有@符号具有特定功能的关键字),包括如何自定义组件、自定义组件构建函数、通过变量实现UI刷新、变量更改监听、自定义样式实现样式复用,单向、双向、多层双向参数传递,嵌套对象,多层嵌套对象,对象作为数组元素,嵌套对象作为数组元素,多层嵌套对象作为数组元素的数据同步。纯小白,一步步学习,记录一下过程便于查询。
1.创建自定义组件(@Entry、@Component装饰器)
ArkUI中附带了系统组件,但是很多场景下我们需要自定义组件来满足我们组合使用系统组件的布局、属性、方法的要求,或者复用自定义组件避免冗余代码,或者通过自定义组件实现变量改变驱动UI刷新等。
1.1 创建自定义组件的流程
明确需自定义的组件布局和逻辑(可以先用系统组件结合起来布局一下,UI预览查看一下效果是否符合要求)>声明组件(含参需要声明接收参数的变量)>调用组件(可能需要传参)
1.2 声明+使用自定义组件的写法
- 组件要成为组件,必须使用Component修饰,并且必须具备build函数,才能呈现界面。
- 组件的build函数中必须具备一个组件,没有被Entry修饰的组件,build中可以是一个根或者非根组件,例如Text()或Row(),但是不可以多个并列,根和非根组件都不可以。
- 一个ets文件中必须有且仅有一个Entry装饰器,作为该文件的入口。
- ets文件中被entry装饰的组件会被显示在界面,并且被装饰的组件的build函数中必须有且仅有一个根组件。
其中自定义组件包括含参和不含参两种,含参的自定义组件可以通过组件内定义私有变量接收传参,调用时使用键值对方式键为私有变量名传递参数的方式,进行参数的定义和传递。
1.3 自定义组件的属性和事件直接编写
自定义组件的属性和事件可以在声明自定义组件时直接写在自定义组件中(一般这种属性和方法时是需要复用的),如上述的写法,也可以在调用自定义组件时进行链式编程(这种可以写除了复用外的属性和方法以外的)。
1.4 通过变量值改变驱动UI刷新
被state修饰的变量,变量值改变会驱动UI刷新,可以通过该方式实现一些效果。
实现效果
2.自定义组件构建函数创建自定义组件(@Builder装饰器)
自定义组件构建函数->@Builder装饰器:使用函数实现自定义组件的效果。编写方式为通过Builder装饰器装饰函数,函数就可以编写组件形式的代码。
2.1 内部函数系定义组件
(1)无传参
(2)有传参:和函数传参方式相同,直接在小括号中加入参数即可。
(3)通过变量值改变驱动UI刷新和前面是相同的。
2.2 外部函数自定义组件
上述的方式只可以在组件内部调用函数实现自定义组件,这样多个组件均可以使用我们的自定义组件,那么需要将函数写到组件外部,加上function关键字,并且调用时不能加this关键字。实现如下。
3 通过变量值改变驱动UI刷新bug(@Link装饰器)
上述通过修改被state装饰装饰的变量实现UI刷新的方式存在bug:如果界面中存在多处修改该变量的值,多处调用变量时,就会出现逻辑错误。
例如:在某个自定义组件的点击事件中修改了该变量的值并使用该变量值驱动UI刷新,之后代码多处使用了这个自定义组件,就会出现一处点击,多处出现点击效果的情况。点击button1修改text1文字,点击button2修改text2文字,可能会呈现点击button1,text1和text2文字均被修改的情况。
解决办法为:遵守参数传递规则传递参数
- 自定义构建函数的参数传递有按值传递和按引用传递两种,均需要遵守:
- 参数的类型必须与参数声明的类型一致,不允许undefined,null或返回undefined、null;
- 在自定义构建函数中,由于可能会被复用,所以不允许改变参数值,如果需要改变参数值,且同步回调用点(即改变参数值后,希望变量值也能随之改变),需要使用@Link传参。
解决方式:参考下方的7.状态管理中的@State+@LInk
4.定义组件复用样式(@Styles装饰器)
布局中无可避免对于部分组件我们可能使用相同的样式,也就是复用样式的情况,为避免冗余代码,可以抽取出相同的样式代码进行复用。
定义样式函数>使用@Styles装饰器装饰函数>在函数中编写样式代码>采用链式编程方式调用样式函数(不可以用this),同样的如果要多个组件中均使用该样式函数,同样需要添加function关键字,其他均相同。
注意:
- 当同时具有外部和内部两个同名的样式函数时,优先级内部高于外部,也就是说会优先使用内部的样式函数定义的样式。
- 样式函数中只能写通用样式,不能写特殊样式,例如Text的fontColor()样式就不可以。
- 样式函数不能传参,编译阶段无问题,运行出错:@Styles can't have parameters。
- 样式函数不能给自定义组件使用,编译和运行均会出错:Property '组定义样式' does not exist on type 'void',Compile error occurred. Fix it based on the above message.
5.定义扩展组件样式(@Extend装饰器)
- @Extend相对于@Style的区别:
- @styles装饰器只能封装通用样式,对于部分组件的特殊样式无能为力,@Extend装饰器可以对这些特殊样式进行封装
- @Extend装饰器还可以传参(参数可以多种多样,甚至是函数例如点击事件),但是这种封装方法必须指定组件的名称。
- @Extend函数可以相互调用满足多种组件样式少冗余代码。
注意:
- @extend封装样式必须写明组件的类型
- 组件只能调用@Extend封装的是同类型组件的样式函数
6.多态样式(属性方法stateStyle)
不同场景下显示不同的样式:例如链接点击前、点击、点击后的样式,按钮点击前,短按,长按,点击完毕的样式。属性方法stateStyle
注意:
- 在多态样式中不能使用@Extend装饰的函数作为样式,测试发现样式会直接生效为最后一个键值对的值,不会根据场景改变。
- 多态样式中使用@Style装饰的函数可以作为样式,但是无法传递参数+特殊样式。
7.状态管理(状态装饰器state-prop-link)
在声明式UI中,是以章台驱动视图更新的。状态是指驱动视图更新的数据(被装饰器标记的变量),视图是指基于UI描述(例如build函数内部描述)渲染得到的用户界面。用户通过与视图中的页面元素交互触发互动事件可以改变状态变量的值。状态变量的变化可以触发自带的监控事件对UI界面重新渲染。
状态管理分为组件间的状态管理和硬件状态管理,由各种各样的装饰器装饰变量。
- @State+@Prop:单向的
- @State+@Link:双向的(3.通过变量值改变驱动UI刷新bug的解决方式)
7.1单向数据传递
单向数据传递:在父组件中被@State装饰的变量的值发生改变,传递给子组件,子组件的UI也会发生改变,但是子组件中@prop装饰的变量值发生改变,则不会影响父组件UI
7.2 双向数据传递
双向数据传递:在父组件中被@State装饰的变量的值发生改变,传递给子组件,子组件的UI也会发生改变,子组件中@link装饰的变量值发生改变,父组件UI也会改变
★★★注意:①State装饰的变量必须初始化,不能为空。②且State支持Object、class、string、number、boolean、enum类型以及这些类型的数组,不支持any、union等类型。③嵌套类型(例如Object对象,如果具备一个Object属性,它的Obhject属性发生变化就无法监控到)以及数组中的对象属性)无法触发视图更新。
8.多层双向传递(@Provider+@Cosume装饰器)
父子组件双向传值可以采用@State+@Link方式,但是对于多层传值,例如爷孙,或者中间间隔了很多代的数据双向传值时,可以采用@Provider+@Cosume装饰器传值。
- @Provider:提供者,表示提供了一个变量
- @Cosume:订阅者,表示订阅某个数据
注意:
- 在这种多层双向传递时,需要提供者和订阅者的变量名和数据类型相同。
- 且订阅者的变量不可以初始化。
多层双向传递变量名修改:可以采用定义别名的方式实现修改变量名的目的,但是别名需要相同,则可以达到同样的多层双向传递,使用代码示例如下:
@Provide('test') num4:number=2
@Consume('test') num7:number
9.嵌套对象或数组元素为对象的双向数据同步(@Observed+@ObjectLink装饰器)(补充)
之前的数据传递装饰器例如@State、@Link、@Prop、@Provider、@Cosume均不适用于嵌套对象或数组元素为对象,无法监听到嵌套对象或数组元素为对象的内部对象属性的改变,但是@Observed和@ObjectLink可以实现嵌套对象或数组元素为对象的双向数据同步。
注意:①凡是需要监控变化的嵌套对象数组元素为对象均需要使用@Observed装饰,除非对象内部嵌套的是对象本身。
②如果嵌套对象是封装的类,使用类的时候,会用嵌套的对象的属性作为参数,这个属性也是一个对象,这个对象作为参数无法被@ObjectLink装饰,则需要定义组件,将内部嵌套的对象传递,并且在定义的组件中使用变量接收,并用@ObjectLink装饰接收的变量。
使用示例:
(1)使用@State+@Link:除了对象中非嵌套的数据会被同步,其余均不会生效。
测试代码如下:
//被嵌套对象
export class FoodInfo{
id:number=0;
name:string='';
img:Resource|null=null;
category:number=0;
kk:number=0;
rl:number=0;
yy:number=0;
zf:number=0;
}
//嵌套对象
import { FoodInfo } from './FoodInfo';
//封装的类
export class Food{
price:number;
unit:string;
//嵌套了一个别的类
foodInfo:FoodInfo;
//嵌套了一个类本身
food:Food;
constructor(price:number,unit:string,foodInfo:FoodInfo,food?:Food){
this.price=price;
this.unit=unit;
this.foodInfo=foodInfo;
this.food=food;
}
}
//使用Food测试:
import PageTitle from '../view/PageTitle'
import { Food } from '../ViewData/Food'
@Entry
@Component
struct ObjectVariableWatch{
@State food:Food=new Food(2,"元/斤",
//1.修改嵌套的其他对象的属性
{id:0,name:'番茄',kk:15,category:1,img:$r('app.media.tomato'),rl:0,yy:0,zf:0},
//2.修改嵌套的对象本身的属性
new Food(2,"元/斤",
//修改多层嵌套对象的属性
{id:0,name:'番茄',kk:15,category:1,img:$r('app.media.tomato'),rl:0,yy:0,zf:0}))
build() {
Row() {
Column() {
//如果数据是嵌套的对象,使用@State修改嵌套的内部对象的属性值,将不会发生变化。
Test({food:$food})
}
}
}
}
@Component
struct Test{
@Link food:Food
build(){
Row() {
Column() {
//标题
PageTitle()
Stack(){
Image(this.food.foodInfo.img)
.objectFit(ImageFit.Contain)
Text(this.food.foodInfo.name)
.fontSize(26).fontColor(0X3E3E3E)
.width('90%').position({x:10,y:230})
}.height(300)
//测试
Column({space:10}){
//修改非嵌套属性:只有该处生效
Row(){
Circle({width:6,height:6}).margin({right:12}).fill('#ffaaaa')
Text('价格').fontSize(16).fontColor(0X3E3E3E)
Blank()
Text(this.food.price+' '+this.food.unit).fontSize(16).fontColor(0X3E3E3E)
}.onClick(()=>{
this.food.price++;
})
//修改嵌套的其他对象的属性
Row(){
Circle({width:6,height:6}).margin({right:12}).fill('#ffaaaa')
Text('热量').fontSize(16).fontColor(0X3E3E3E)
Blank()
Text(this.food.foodInfo.rl+' '+'千卡').fontSize(16).fontColor(0X3E3E3E)
}.onClick(()=>{
this.food.foodInfo.rl++;
})
//修改嵌套的对象本身的属性
Row(){
Circle({width:6,height:6}).margin({right:12}).fill('#ffaaaa')
Text('嵌套对象价格').fontSize(16).fontColor(0X3E3E3E)
Blank()
Text(this.food.food.price+' '+this.food.food.unit).fontSize(16).fontColor(0X3E3E3E)
}.onClick(()=>{
this.food.food.price++;
})
//修改嵌套的对象内嵌套对象的属性
Row(){
Circle({width:6,height:6}).margin({right:12}).fill('#ffaaaa')
Text('嵌套对象的嵌套对象的属性热量').fontSize(16).fontColor(0X3E3E3E)
Blank()
Text(this.food.food.foodInfo.rl+' '+'千卡').fontSize(16).fontColor(0X3E3E3E)
}.onClick(()=>{
this.food.food.foodInfo.rl++;
})
}.width('90%').margin({top:20,bottom:20})
.padding(20)
.borderRadius(10)
.backgroundColor(Color.White)
}
.height('100%')
.backgroundColor('#dedede')
}
}
}
实现效果:除了对象中非嵌套的数据会被同步,其余均不会生效。
(2)使用@Observed+@ObjectLink装饰器实现:均可生效。
测试代码如下:该测试案例中实现了:嵌套对象,多层嵌套对象,对象作为数组元素,嵌套对象作为数组元素,多层嵌套对象作为数组元素的数据同步,均生效。
//被嵌套对象
@Observed//这个装饰器必须添加
export class FoodInfoTest{
id:number=0;
name:string='';
img:Resource|null=null;
category:number=0;
kk:number=0;
rl:number=0;
yy:number=0;
zf:number=0;
constructor(id:number,name:string,img:Resource|null, category:number, kk:number, rl:number, yy:number, zf:number) {
this.id=id;
this.name=name;
this.img=img;
this,category=category;
this.kk=kk;
this.rl=rl;
this.yy=yy;
this.zf=zf;
}
}
//嵌套对象
import { FoodInfoTest } from './FoodInfoTest';
//封装的类
@Observed//这个装饰器也必须加
export class FoodTest{
price:number;
unit:string;
//嵌套了一个别的类
foodInfoTest:FoodInfoTest;
//嵌套了一个类本身
foodTest:FoodTest;
constructor(price:number,unit:string,foodInfoTest:FoodInfoTest,foodTest?:FoodTest){
this.price=price;
this.unit=unit;
this.foodInfoTest=foodInfoTest;
this.foodTest=foodTest;
}
}
//使用嵌套数据做测试
import PageTitle from '../view/PageTitle'
import { FoodInfoTest } from '../ViewData/FoodInfoTest'
import { FoodTest } from '../ViewData/FoodTest'
@Entry
@Component
struct ObjectVariableWatchTest{
//数组元素是对象
@State foodArray:Array<FoodInfoTest>=[new FoodInfoTest(0,"番茄",$r('app.media.tomato'),1,15,0,0,0)];
//数组元素是嵌套对象
@State foodObjectQTArray:Array<FoodTest>=[new FoodTest(2,"元/斤",
//1.修改嵌套的其他对象的属性
new FoodInfoTest(0,"番茄",$r('app.media.tomato'),1,15,0,0,0),
//2.修改嵌套的对象本身的属性
new FoodTest(2,"元/斤",
//修改多层嵌套对象的属性
new FoodInfoTest(0,"番茄",$r('app.media.tomato'),1,15,0,0,0)))];
@State foodTest:FoodTest=new FoodTest(2,"元/斤",
//1.修改嵌套的其他对象的属性
new FoodInfoTest(0,"番茄",$r('app.media.tomato'),1,15,0,0,0),
//2.修改嵌套的对象本身的属性
new FoodTest(2,"元/斤",
//修改多层嵌套对象的属性
new FoodInfoTest(0,"番茄",$r('app.media.tomato'),1,15,0,0,0)))
build() {
Row() {
Column() {
//直接传递该对象即使使用@ObjectLink装饰也不会生效
// Test({food:this.food})
PageTitle()
Stack(){
Image(this.foodTest.foodInfoTest.img)
.objectFit(ImageFit.Contain)
Text(this.foodTest.foodInfoTest.name)
.fontSize(26).fontColor(0X3E3E3E)
.width('90%').position({x:10,y:230})
}.height(300)
Column({space:10}){
//修改非嵌套属性:直接可以生效,无需做别的
Row(){
Circle({width:6,height:6}).margin({right:12}).fill('#ffaaaa')
Text('价格').fontSize(16).fontColor(0X3E3E3E)
Blank()
Text(this.foodTest.price+' '+this.foodTest.unit).fontSize(16).fontColor(0X3E3E3E)
}.onClick(()=>{
this.foodTest.price++;
})
//修改嵌套的其他对象的属性:将嵌套的对象作为参数传递到自定义组件中:生效
LineContent1({foodinfoqtyc:this.foodTest.foodInfoTest,name:'热量',unit:'千卡'})
//修改嵌套的对象本身的属性:生效
LineContent2({foodinfoqtbsyc:this.foodTest.foodTest})
//修改嵌套的对象内嵌套对象的属性:生效
LineContent1({foodinfoqtyc:this.foodTest.foodTest.foodInfoTest,name:'嵌套对象的嵌套对象的属性热量',unit:'千卡'})
//数组元素为对象,对象的属性
LineContent1({foodinfoqtyc:this.foodArray[0],name:'数组对象热量',unit:'千卡'})
//数组元素为对象,对象的嵌套对象的属性
LineContent1({foodinfoqtyc:this.foodObjectQTArray[0].foodInfoTest,name:'数组对象的嵌套对象热量',unit:'千卡'})
//数组元素为对象,对象的嵌套对象的嵌套对象的属性
LineContent1({foodinfoqtyc:this.foodObjectQTArray[0].foodTest.foodInfoTest,name:'数组对象的嵌套对象嵌套热量',unit:'千卡'})
}.width('90%').margin({top:20,bottom:20})
.padding(20)
.borderRadius(10)
.backgroundColor(Color.White)
}
}
}
}
@Component
struct LineContent1{
@ObjectLink foodinfoqtyc:FoodInfoTest
private name:string
private unit:string
build(){
//修改嵌套的其他对象的属性
Row(){
Circle({width:6,height:6}).margin({right:12}).fill('#ffaaaa')
Text(this.name).fontSize(16).fontColor(0X3E3E3E)
Blank()
Text(this.foodinfoqtyc.rl+' '+this.unit).fontSize(16).fontColor(0X3E3E3E)
}.onClick(()=>{
console.log(this.foodinfoqtyc.rl+"")
this.foodinfoqtyc.rl++;
console.log(this.foodinfoqtyc.rl+"")
})
}
}
@Component
struct LineContent2{
@ObjectLink foodinfoqtbsyc:FoodTest
build(){
//修改嵌套的其他对象的属性
Row(){
Circle({width:6,height:6}).margin({right:12}).fill('#ffaaaa')
Text('嵌套对象价格').fontSize(16).fontColor(0X3E3E3E)
Blank()
Text(this.foodinfoqtbsyc.price+' '+this.foodinfoqtbsyc.unit).fontSize(16).fontColor(0X3E3E3E)
}.onClick(()=>{
this.foodinfoqtbsyc.price++;
})
}
}
实现效果:
★★★★★★bug:如果嵌套对象内部的对象的属性数据同步不生效,可以检查是否出现了使用'{}'实例化对象而不是使用new关键字。例如上方嵌套对象按照写法1,其嵌套的FoodInfoTest无论是几层,均不会生效。按照写法二则均可生效,因为我们使用@Obversed装饰的是FoodInfoTest这个类。而不是'{}'括起来的数据。
写法一:错误写法:改写法无论赋值还是操作均不会出错,但是数据同步不会生效。
@State foodTest1:FoodTest=new FoodTest(2,"元/斤",
{id:0,name:"番茄",img:$r('app.media.tomato'),category:1,kk:15,rl:0,yy:0,zf:0},
new FoodTest(2,"元/斤",
{id:0,name:"番茄",img:$r('app.media.tomato'),category:1,kk:15,rl:0,yy:0,zf:0}));
写法二:正确写法
@State foodTest:FoodTest=new FoodTest(2,"元/斤",
new FoodInfoTest(0,"番茄",$r('app.media.tomato'),1,15,0,0,0),
new FoodTest(2,"元/斤",
new FoodInfoTest(0,"番茄",$r('app.media.tomato'),1,15,0,0,0)))
10.状态变量更改通知(@Watch装饰器)
被@Watch装饰的状态变量(被装饰器修饰的变量:@state,@link,@prop,@provide、@consume等)一旦发生改变,就会通知我们实现一些业务。
注意:自定义组件、自定义样式等在不同文件中定义均不可以重名,因为组件、样式均是按照项目进行树状存储的,即使在不同文件中定义重名也会出错。