1:装饰器是什么
装饰器本质上是一种特殊类型的声明,它可以被附加到类声明、方法、属性或参数上,用来修改类的行为。简单来说,装饰器是一个函数,它接收目标对象、目标对象的属性名以及属性描述符作为参数,然后返回一个新的属性描述符或者直接修改目标对象
在 HarmonyOS 的开发中,装饰器是一种非常强大的工具,用于声明组件的行为和数据管理
状态管理指的是,管理数据变化去刷新UI的整个过程
2.装饰器
- 组件装饰器
- 状态管理装饰器
2.1 @Entry 这个装饰器用于标记页面的入口点,必须声明在页面的主结构上。
@Entry
@Component
struct Index {
aboutToAppear() {
console.log('页面初始化:Index');
}
build() {
// 页面构建逻辑
Column() {
Text('这是主页')
.fontSize(24)
.margin(10)
}
}
}
2.2@Component - 该装饰器用于声明页面或子组件的构建结构。
@Component
struct MyComponent {
aboutToAppear() {
console.log('组件初始化:MyComponent');
}
build() {
Row() {
Text('Hello World')
}
}
}
2.3 @Builder 自定义构建函数
ArkUI提供了一种轻量的UI元素复用机制@Builder,其内部UI结构固定,仅与使用方进行数据传递,开发者可以将重复使用的UI元素抽象成一个方法,在build方法里调用。
@Builder装饰器有两种使用方式,
- 分别是定义在自定义组件内部的私有自定义构建函数
- 定义在全局的全局自定义构建函数。
// 私有
@Entry
@Component
struct BuilderDemo {
@Builder
showTextBuilder() {
Text('Hello World')
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
@Builder
showTextValueBuilder(param: string) {
Text(param)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
build() {
Column() {
// 无参数
this.showTextBuilder()
// 有参数
this.showTextValueBuilder('Hello @Builder')
}
}
}
//全局
@Builder
function showTextBuilder() {
Text('Hello World')
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
@Entry
@Component
struct BuilderDemo {
build() {
Column() {
showTextBuilder()
}
}
}
2.4 @Style @Extend AttributeModifier 样式装饰器
如果每个组件的样式都需要单独设置,在开发过程中会出现大量代码在进行重复样式设置,虽然可以复制粘贴,但为了代码简洁性和后续方便维护,我们推出了可以提炼公共样式进行复用的装饰器@Styles,@Extend和AttributeModifier。
注意:
1: @Style 不能传参数 可局部和全局 不可类外
2: @Extend 可以传递参数 不可局部 可全局 不可类外
2: Extend和AttributeModifier 可以传递参数 可局部 可全局 可类外
| 能力 | @Styles | @Extend | AttributeModifier |
|---|---|---|---|
| 跨文件导出 | 不支持 | 不支持 | 支持 |
| 通用属性设置 | 支持 | 支持 | 支持 |
| 通用事件设置 | 支持 | 支持 | 部分支持 |
| 组件特有属性设置 | 不支持 | 支持 | 部分支持 |
| 组件特有事件设置 | 不支持 | 支持 | 部分支持 |
| 参数传递 | 不支持 | 支持 | 支持 |
| 多态样式 | 支持 | 不支持 | 支持 |
| 业务逻辑 | 不支持 | 不支持 | 支持 |
2.4.1 @Style 定义组件重用样式
- 当前@Styles仅支持通用属性和通用事件。
- @Styles可以定义在组件内或全局,在全局定义时需在方法名前面添加function关键字,组件内定义时则不需要添加function关键字。
- 只能在当前文件内使用,不支持export。
- 不支持在@Styles方法内使用逻辑组件,在逻辑组件内的属性不生效。
- @Styles方法不能有参数,编译期会报错,提醒开发者@Styles方法不支持参数。
// 定义在全局的@Styles封装的样式
@Styles function globalFancy () {
.width(150)
.height(100)
.backgroundColor(Color.Pink)
}
@Entry
@Component
struct FancyUse {
@State heightValue: number = 100;
// 定义在组件内的@Styles封装的样式
@Styles fancy() {
.width(200)
.height(this.heightValue)
.backgroundColor(Color.Yellow)
.onClick(() => {
this.heightValue = 200
})
}
build() {
Column({ space: 10 }) {
// 使用全局的@Styles封装的样式
Text('FancyA')
.globalFancy()
.fontSize(30)
// 使用组件内的@Styles封装的样式
Text('FancyB')
.fancy()
.fontSize(30)
}
}
}
2.4.2 @Extend 用于扩展原生组件样式
支持封装指定组件的私有属性、私有事件和自身定义的全局方法。
注意:
和@Styles不同,@Extend仅支持在全局定义,不支持在组件内部定义。
// 正确写法
@Extend(Text) function fancy (fontSize: number) {
.fontSize(fontSize)
}
@Entry
@Component
struct FancyUse {
build() {
Row({ space: 10 }) {
Text('Fancy')
.fancy(16)
}
}
}
2.4.2 stateStyles可以依据组件的内部状态的不同
@Styles仅仅应用于静态页面的样式复用,stateStyles可以依据组件的内部状态的不同,快速设置不同样式。这就是我们本章要介绍的内容stateStyles(又称为:多态样式)。
stateStyles是属性方法,可以根据UI内部状态来设置样式,类似于css伪类,但语法不同。ArkUI提供以下五种状态:
- focused:获焦态。
- normal:正常态。
- pressed:按压态。
- disabled:不可用态。
- selected10+:选中态。
@Entry
@Component
struct MyComponent {
@Styles normalStyle() {
.backgroundColor(Color.Gray)
}
@Styles pressedStyle() {
.backgroundColor(Color.Red)
}
build() {
Column() {
Text('Text1')
.fontSize(50)
.fontColor(Color.White)
.stateStyles({
normal: this.normalStyle,
pressed: this.pressedStyle,
})
}
}
}
2.4.3 AttributeModifier AttributeUpdater 类外可调用 支持逻辑
AttributeModifier
声明式语法引入的@Styles和@Extend两个装饰器,虽然可以解决复用相同自定义样式的问题,但是使用场景存在一定局限性,如无法跨文件导出等。为此,ArkUI引入了AttributeModifier机制,可以通过Modifier对象动态修改属性。与@Styles和@Extend相比,AttributeModifier提供了更强的能力和灵活性,且在持续完善全量的属性和事件设置能力,因此推荐优先使用AttributeModifier。
AttributeUpdater
AttributeUpdater是一个特殊的AttributeModifier,除了继承AttributeModifier的能力,还提供了获取属性对象的能力。通过属性对象可以不经过状态变量,直接更新对应属性。开发者可以通过AttributeUpdater实现自定义的更新策略,进一步提高属性更新的性能。
区别:
- AttributeModifier:常用于组件属性的初始化,可通过链式调用一次性设置多属性,如设置文本组件初始的字体大小、颜色和宽度。
- AttributeUpdater:主要用于响应式更新,当组件状态改变时自动更新属性,如按钮点击状态改变时更新其样式。
// button_modifier.ets
export class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
// 可以实现一个Modifier,定义私有的成员变量,外部可动态修改
isDark: boolean = false
// 通过构造函数,创建时传参
constructor(dark?: boolean) {
this.isDark = dark ? dark : false
}
applyNormalAttribute(instance: ButtonAttribute): void {
// instance为Button的属性对象,可以通过instance对象对属性进行修改
if (this.isDark) { // 支持业务逻辑的编写
// 属性变化触发apply函数时,变化前已设置并且变化后未设置的属性会恢复为默认值
instance.backgroundColor('#707070')
} else {
// 支持属性的链式调用
instance.backgroundColor('#17A98D')
.borderColor('#707070')
.borderWidth(2)
}
}
}
// demo.ets
import { MyButtonModifier } from './button_modifier'
@Entry
@Component
struct attributeDemo {
// 支持用状态装饰器修饰,行为和普通的对象一致
@State modifier: MyButtonModifier = new MyButtonModifier(true);
build() {
Row() {
Column() {
Button("Button")
.attributeModifier(this.modifier)
.onClick(() => {
// 对象的一层属性被修改时,会触发UI刷新,重新执行applyNormalAttribute
this.modifier.isDark = !this.modifier.isDark
})
}
.width('100%')
}
.height('100%')
}
}
import { AttributeUpdater } from '@ohos.arkui.modifier'
class MyButtonModifier extends AttributeUpdater<ButtonAttribute> {
// 首次绑定时触发initializeModifier方法,进行属性初始化
initializeModifier(instance: ButtonAttribute): void {
instance.backgroundColor('#2787D9')
.width('50%')
.height(30)
}
}
@Entry
@Component
struct updaterDemo {
modifier: MyButtonModifier = new MyButtonModifier()
build() {
Row() {
Column() {
Button("Button")
.attributeModifier(this.modifier)
.onClick(() => {
// 通过attribute,直接修改组件属性,并立即触发组件属性更新
this.modifier.attribute?.backgroundColor('#17A98D').width('30%')
})
}
.width('100%')
}
.height('100%')
}
}
2.5 @AnimatableExtend装饰器:定义可动画属性
@AnimatableExtend装饰器用于自定义可动画的属性方法,在这个属性方法中修改组件不可动画的属性。在动画执行过程时,通过逐帧回调函数修改不可动画属性值,让不可动画属性也能实现动画效果。也可通过逐帧回调函数修改可动画属性的值,实现逐帧布局的效果。
- 可动画属性:如果一个属性方法在animation属性前调用,改变这个属性的值可以使animation属性的动画效果生效,这个属性称为可动画属性。比如height、width、backgroundColor、translate属性,和Text组件的fontSize属性等。
- 不可动画属性:如果一个属性方法在animation属性前调用,改变这个属性的值不能使animation属性的动画效果生效,这个属性称为不可动画属性。比如Polyline组件的points属性等。
@AnimatableExtend(Text)
function animatableWidth(width: number) {
.width(width)
}
@Entry
@Component
struct AnimatablePropertyExample {
@State textWidth: number = 80;
build() {
Column() {
Text("AnimatableProperty")
.animatableWidth(this.textWidth)
.animation({ duration: 2000, curve: Curve.Ease })
Button("Play")
.onClick(() => {
this.textWidth = this.textWidth == 80 ? 160 : 80;
})
}.width("100%")
.padding(10)
}
}
2.6 @State 用于声明组件内的可变状态。当状态值改变时,UI 将自动重新渲染。(@Local替代)
z注意:
不允许在build里改变状态变量,状态管理框架会在运行时报出Error级别日志
@Entry
@Component
struct Index {
@State message: string = 'Hello World'
@State numCount = 1
aboutToAppear() {
console.log(`初始消息: ${this.message}`);
}
build() {
Column() {
Button('修改消息')
.onClick(() => {
this.message = '消息已修改';
})
// 这样是不允许的 这样会造成 应避免直接在Text组件内改变count的值
//这样会造成Text被多执行一次
Text(`${this.numCount++}`)
.width(50)
.height(50)
}
}
}
3: V2所属装饰器
V1版本装饰器 V1版本装饰,在复杂逻辑开发中出现了一些或多或少的调用异常 因此迭代出了V2版本 建议直接从V2版本开发
3.1 @ComponentV2装饰器:自定义组件
为了在自定义组件中使用V2版本状态变量装饰器的能力,开发者可以使用@ComponentV2装饰器装饰自定义组件。
和@Component装饰器一样,@ComponentV2装饰器用于装饰自定义组件:
除非特别说明,@ComponentV2装饰的自定义组件将与@Component装饰的自定义组件保持相同的行为。
@ComponentV2 // 装饰器
struct Index { // struct声明的数据结构
build() { // build定义的UI
}
}
3.2 @Local装饰器:组件内部状态
为了实现对@ComponentV2装饰的自定义组件中变量变化的观测,开发者可以使用@Local装饰器装饰变量。 替代了@State
@Local表示组件内部的状态,使得自定义组件内部的变量具有观测变化的能力:
- 被@Local装饰的变量无法从外部初始化,因此必须在组件内部进行初始化。
- 当被@Local装饰的变量变化时,会刷新使用该变量的组件。
- @Local支持观测number、boolean、string、Object、class等基本类型以及Array、Set、Map、Date等内嵌类型。
- @Local的观测能力仅限于被装饰的变量本身。当装饰简单类型时,能够观测到对变量的赋值;当装饰对象类型时,仅能观测到对对象整体的赋值;当装饰数组类型时,能观测到数组整体以及数组元素项的变化;当装饰Array、Set、Map、Date等内嵌类型时,可以观测到通过API调用带来的变化。详见观察变化。
- @Local支持null、undefined以及联合类型。
状态管理V1使用@State装饰器定义类中的状态变量。但由于@State装饰器能够从外部初始化,因此@State无法准确表达组件内部状态不能被外面修改的语义。
@Entry
@ComponentV2
struct Index {
@Local count: number = 0;
@Local message: string = "Hello";
@Local flag: boolean = false;
build() {
Column() {
Text(`${this.count}`)
Text(`${this.message}`)
Text(`${this.flag}`)
Button("change Local")
.onClick(()=>{
// 当@Local装饰简单类型时,能够观测到对变量的赋值
this.count++;
this.message += " World";
this.flag = !this.flag;
})
}
}
}
3.3 @ObservedV2装饰器和@Trace装饰器:类属性变化观测
为了增强状态管理框架对类对象中属性的观测能力,开发者可以使用@ObservedV2装饰器和@Trace装饰器装饰类以及类中的属性。
@ObservedV2和@Trace提供了对嵌套类对象属性变化直接观测的能力,是状态管理V2中相对核心的能力之一。在阅读本文档前,建议提前阅读:状态管理概述来了解状态管理V2整体的能力架构。
概述
@ObservedV2装饰器与@Trace装饰器用于装饰类以及类中的属性,使得被装饰的类和属性具有深度观测的能力:
- @ObservedV2装饰器与@Trace装饰器需要配合使用,单独使用@ObservedV2装饰器或@Trace装饰器没有任何作用。
- 被@Trace装饰器装饰的属性property变化时,仅会通知property关联的组件进行刷新。
- 在嵌套类中,嵌套类中的属性property被@Trace装饰且嵌套类被@ObservedV2装饰时,才具有触发UI刷新的能力。
- 在继承类中,父类或子类中的属性property被@Trace装饰且该property所在类被@ObservedV2装饰时,才具有触发UI刷新的能力。
- 未被@Trace装饰的属性用在UI中无法感知到变化,也无法触发UI刷新。
- @ObservedV2的类实例目前不支持使用JSON.stringify进行序列化。
@ObservedV2
class Son {
@Trace age: number = 100;
}
class Father {
son: Son = new Son();
}
@Entry
@ComponentV2
struct Index {
father: Father = new Father();
build() {
Column() {
// 当点击改变age时,Text组件会刷新
Text(`${this.father.son.age}`)
.onClick(() => {
this.father.son.age++;
})
}
}
}
注意:
@Trace装饰的属性具有被观测变化的能力
@ObservedV2
class Manager {
@Trace static count: number = 1;
}
@Entry
@ComponentV2
struct Index {
build() {
Column() {
// 当点击改变count时,Text组件会刷新
Text(`${Manager.count}`)
.onClick(() => {
Manager.count++;
})
}
}
}
3.4 @Param:参数外部输入
为了增强子组件接受外部参数输入的能力,开发者可以使用@Param装饰器。
@Param不仅可以接受组件外部输入,还可以接受@Local的同步变化。在阅读本文档前,
作用:
- 参数传递
- 变化同步
@Entry
@ComponentV2
struct Index {
@Local count: number = 0;
@Local message: string = "Hello";
@Local flag: boolean = false;
build() {
Column() {
Text(`Local ${this.count}`)
Text(`Local ${this.message}`)
Text(`Local ${this.flag}`)
Button("change Local")
.onClick(()=>{
// 对数据源的更改会同步给子组件
this.count++;
this.message += " World";
this.flag = !this.flag;
})
Child({
count: this.count,
message: this.message,
flag: this.flag
})
}
}
}
@ComponentV2
struct Child {
@Require @Param count: number;
@Require @Param message: string;
@Require @Param flag: boolean;
build() {
Column() {
Text(`Param ${this.count}`)
Text(`Param ${this.message}`)
Text(`Param ${this.flag}`)
}
}
}
3.5 @Once:初始化同步一次
为了实现仅从外部初始化一次、不接受后续同步变化的能力,开发者可以使用@Once装饰器搭配@Param装饰器使用。
仅初始化数据传递 变化不传递 搭配@Param 使用
@ComponentV2
struct ChildComponent {
@Param @Once onceParam: string = "";
build() {
Column() {
Text(`onceParam: ${this.onceParam}`)
}
}
}
@Entry
@ComponentV2
struct MyComponent {
@Local message: string = "Hello World";
build() {
Column() {
Text(`Parent message: ${this.message}`)
Button("change message")
.onClick(() => {
this.message = "Hello Tomorrow";
})
ChildComponent({ onceParam: this.message })
}
}
}
3.6 @Event装饰器 子组件属性变化通知父组件
@Entry
@ComponentV2
struct Index {
@Local title: string = "Title One";
@Local fontColor: Color = Color.Red;
build() {
Column() {
Child({
title: this.title,
fontColor: this.fontColor,
changeFactory: (type: number) => {
if (type == 1) {
this.title = "Title One";
this.fontColor = Color.Red;
} else if (type == 2) {
this.title = "Title Two";
this.fontColor = Color.Green;
}
}
})
}
}
}
@ComponentV2
struct Child {
@Param title: string = '';
@Param fontColor: Color = Color.Black;
@Event changeFactory: (x: number) => void = (x: number) => {};
build() {
Column() {
Text(`${this.title}`)
.fontColor(this.fontColor)
Button("change to Title Two")
.onClick(() => {
this.changeFactory(2);
})
Button("change to Title One")
.onClick(() => {
this.changeFactory(1);
})
}
}
}
3.7 @Provider装饰器和@Consumer装饰器:跨组件层级双向同步
@Provider和@Consumer用于跨组件层级数据双向同步,可以使得开发者不用拘泥于组件层级。
@Provider和@Consumer属于状态管理V2装饰器,所以只能在@ComponentV2中才能使用,在@Component中使用会编译报错。
@Provider和@Consumer提供了跨组件层级数据双向同步的能力。在阅读本文档前,建议提前阅读:@ComponentV2。
@Entry
@ComponentV2
struct Parent {
@Provider() str: string = 'hello';
build() {
Column() {
Button(this.str)
.onClick(() => {
this.str += '0';
})
Child()
}
}
}
@ComponentV2
struct Child {
@Consumer() str: string = 'world';
build() {
Column() {
Button(this.str)
.onClick(() => {
this.str += '0';
})
}
}
}
总结
- @Entry 用于启动
- @Component 用于构造组件 (@ComponentV2替代)
- @Builder 构造自定义函数
- @Style @Extend AttributeModifier用于样式
- @Local 用于数据更新出发UI更新
- @ObservedV2 搭配 @Trace 实现属性变动触发更新
- @Param 构造参数传递(数据同步)
- @Once:搭配 @Param 仅初始化的时候同步
- @Event装饰器 搭配@Param 实现 子组件数据更新 通知父组件
- @Provider装饰器和@Consumer装饰器:跨组件层级双向同步