harmony 鸿蒙\@ObservedV2 and \@Trace Decorators: Observing Class Property Changes
\@ObservedV2 and \@Trace Decorators: Observing Class Property Changes
To allow the state management framework to observe properties in class objects, you can use the \@ObservedV2 decorator and \@Trace decorator to decorate classes and properties in classes.
\@ObservedV2 and \@Trace provide the capability of directly observing the property changes of nested objects. They are one of the core capabilities of state management V2. Before reading this topic, you are advised to read State Management Overview to have a basic understanding of the overall capability architecture of state management V2.
NOTE
The \@ObservedV2 and \@Trace decorators are supported since API version 12.
Overview
The \@ObservedV2 and \@Trace decorators are used to decorate classes and properties in classes so that changes to the classes and properties can be observed.
- \@ObservedV2 and \@Trace must come in pairs. Using either of them alone does not work.
- If a property decorated by \@Trace changes, only the component bound to the property is instructed to re-render.
- In a nested class, changes to its property trigger UI re-render only when the property is decorated by \@Trace and the class is decorated by \@ObservedV2.
- In an inherited class, changes to a property of the parent or child class trigger UI re-renders only when the property is decorated by \@Trace and the owning class is decorated by \@ObservedV2.
- Attributes that are not decorated by \@Trace cannot detect changes nor trigger UI re-renders.
- Instances of \@ObservedV2 decorated classes cannot be serialized using JSON.stringify.
Limitations of State Management V1 on Observability of Properties in Nested Class Objects
With state management V1, properties of nested class objects are not directly observable.
@Observed
class Father {
son: Son;
constructor(name: string, age: number) {
this.son = new Son(name, age);
}
}
@Observed
class Son {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
@Entry
@Component
struct Index {
@State father: Father = new Father("John", 8);
build() {
Row() {
Column() {
Text(`name: ${this.father.son.name} age: ${this.father.son.age}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.father.son.age++;
})
}
.width('100%')
}
.height('100%')
}
}
In the preceding example, clicking the Text component increases the value of age, but does not trigger UI re-renders. The reason is that, the age property is in a nested class and not observable to the current state management framework. To resolve this issue, state management V1 uses \@ObjectLink with custom components.
@Observed
class Father {
son: Son;
constructor(name: string, age: number) {
this.son = new Son(name, age);
}
}
@Observed
class Son {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
@Component
struct Child {
@ObjectLink son: Son;
build() {
Row() {
Column() {
Text(`name: ${this.son.name} age: ${this.son.age}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.son.age++;
})
}
.width('100%')
}
.height('100%')
}
}
@Entry
@Component
struct Index {
@State father: Father = new Father("John", 8);
build() {
Column() {
Child({son: this.father.son})
}
}
}
Yet, this approach has its drawbacks: If the nesting level is deep, the code becomes unnecessarily complicated and the usability poor. This is where the class decorator \@ObservedV2 and member property decorator \@Trace come into the picture.
Decorator Description
\@ObservedV2 Decorator | Description |
---|---|
Decorator parameters | None. |
Class decorator | Decorates a class. You must use new to create a class object before defining the class. |
\@Trace member property decorator | Description |
---|---|
Decorator parameters | None. |
Allowed variable types | Member properties in classes in any of the following types: number, string, boolean, class, Array, Date, Map, Set |
Observed Changes
In classes decorated by \@ObservedV2, properties decorated by \@Trace are observable. This means that, any of the following changes can be observed and will trigger UI re-renders of bound components:
- Changes to properties decorated by \@Trace in nested classes decorated by \@ObservedV2
@ObservedV2
class Son {
@Trace age: number = 100;
}
class Father {
son: Son = new Son();
}
@Entry
@ComponentV2
struct Index {
father: Father = new Father();
build() {
Column() {
// If age is changed, the Text component is re-rendered.
Text(`${this.father.son.age}`)
.onClick(() => {
this.father.son.age++;
})
}
}
}
- Changes to properties decorated by \@Trace in inherited classes decorated by \@ObservedV2
@ObservedV2
class Father {
@Trace name: string = "Tom";
}
class Son extends Father {
}
@Entry
@ComponentV2
struct Index {
son: Son = new Son();
build() {
Column() {
// If name is changed, the Text component is re-rendered.
Text(`${this.son.name}`)
.onClick(() => {
this.son.name = "Jack";
})
}
}
}
- Changes to static properties decorated by \@Trace in classes decorated by \@ObservedV2
@ObservedV2
class Manager {
@Trace static count: number = 1;
}
@Entry
@ComponentV2
struct Index {
build() {
Column() {
// If count is changed, the Text component is re-rendered.
Text(`${Manager.count}`)
.onClick(() => {
Manager.count++;
})
}
}
}
- Changes caused by the APIs listed below to properties of built-in types decorated by \@Trace
Type | APIs that can observe changes |
---|---|
Array | push, pop, shift, unshift, splice, copyWithin, fill, reverse, sort |
Date | setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds |
Map | set, clear, delete |
Set | add, clear, delete |
Constraints
Note the following constraints when using the \@ObservedV2 and \@Trace decorators:
- The member property that is not decorated by \@Trace cannot trigger UI re-renders.
@ObservedV2
class Person {
id: number = 0;
@Trace age: number = 8;
}
@Entry
@ComponentV2
struct Index {
person: Person = new Person();
build() {
Column() {
// age is decorated by @Trace and can trigger re-renders when used in the UI.
Text(`${this.person.age}`)
.onClick(() => {
this.person.age++; // The click event can trigger a UI re-render.
})
// id is not decorated by @Trace and cannot trigger re-renders when used in the UI.
Text(`${this.person.id}`) // UI is not re-rendered when id changes.
.onClick(() => {
this.person.id++; // The click event cannot trigger a UI re-render.
})
}
}
}
- \@ObservedV2 can decorate only classes.
@ObservedV2 // Incorrect usage. An error is reported during compilation.
struct Index {
build() {
}
}
- \@Trace cannot be used in classes that are not decorated by \@ObservedV2.
class User {
id: number = 0;
@Trace name: string = "Tom"; // Incorrect usage. An error is reported at compile time.
}
- \@Trace is a decorator for properties in classes and cannot be used in a struct.
@ComponentV2
struct Comp {
@Trace message: string = "Hello World"; // Incorrect usage. An error is reported at compile time.
build() {
}
}
- \@ObservedV2 and \@Trace cannot be used together with \@Observed and \@Track.
@Observed
class User {
@Trace name: string = "Tom"; // Incorrect usage. An error is reported at compile time.
}
@ObservedV2
class Person {
@Track name: string = "Jack"; // Incorrect usage. An error is reported at compile time.
}
- Classes decorated by @ObservedV2 and @Trace cannot be used together with \@State or other decorators of V1. Otherwise, an error is reported at compile time.
// @State is used as an example.
@ObservedV2
class Job {
@Trace jobName: string = "Teacher";
}
@ObservedV2
class Info {
@Trace name: string = "Tom";
@Trace age: number = 25;
job: Job = new Job();
}
@Entry
@Component
struct Index {
@State info: Info = new Info(); // As @State is not allowed here, an error is reported at compile time.
build() {
Column() {
Text(`name: ${this.info.name}`)
Text(`age: ${this.info.age}`)
Text(`jobName: ${this.info.job.jobName}`)
Button("change age")
.onClick(() => {
this.info.age++;
})
Button("Change job")
.onClick(() => {
this.info.job.jobName = "Doctor";
})
}
}
}
- Classes extended from \@ObservedV2 cannot be used together with \@State or other decorators of V1. Otherwise, an error is reported during running.
// @State is used as an example.
@ObservedV2
class Job {
@Trace jobName: string = "Teacher";
}
@ObservedV2
class Info {
@Trace name: string = "Tom";
@Trace age: number = 25;
job: Job = new Job();
}
class Message extends Info {
constructor() {
super();
}
}
@Entry
@Component
struct Index {
@State message: Message = new Message(); // As @State is not allowed here, an error is reported during running.
build() {
Column() {
Text(`name: ${this.message.name}`)
Text(`age: ${this.message.age}`)
Text(`jobName: ${this.message.job.jobName}`)
Button("change age")
.onClick(() => {
this.message.age++;
})
Button("Change job")
.onClick(() => {
this.message.job.jobName = "Doctor";
})
}
}
}
- Instances of \@ObservedV2 decorated classes cannot be serialized using JSON.stringify.
Use Scenarios
Nested Class
In the following example, Pencil is the innermost class in the Son class. As Pencil is decorated by \@ObservedV2 and its length property is decorated by \@Trace, changes to length can be observed.
The example demonstrates how \@Trace is stacked up against \@Track and \@State under the existing state management framework: The @Track decorator offers property-level update capability for classes, but not deep observability; \@State can only observe the changes of the object itself and changes at the first layer; in multi-layer nesting scenarios, you must encapsulate custom components and use \@Observed and \@ObjectLink to observe the changes.
- Click Button(“change length”), in which length is a property decorated by \@Trace. The change of length can trigger the re-render of the associated UI component, that is, UINode (1), and output the log “id: 1 renderTimes: x” whose x increases according to the number of clicks.
- Because son on the custom component page is a regular variable, no change is observed for clicks on Button(“assign Son”).
- Clicks on Button(“assign Son”) and Button(“change length”) do not trigger UI re-renders. The reason is that, the change to son is not updated to the bound component.
@ObservedV2
class Pencil {
@Trace length: number = 21; // If length changes, the bound component is re-rendered.
}
class Bag {
width: number = 50;
height: number = 60;
pencil: Pencil = new Pencil();
}
class Son {
age: number = 5;
school: string = "some";
bag: Bag = new Bag();
}
@Entry
@ComponentV2
struct Page {
son: Son = new Son();
renderTimes: number = 0;
isRender(id: number): number {
console.info(`id: ${id} renderTimes: ${this.renderTimes}`);
this.renderTimes++;
return 40;
}
build() {
Column() {
Text('pencil length'+ this.son.bag.pencil.length)
.fontSize(this.isRender(1)) // UINode (1)
Button("change length")
.onClick(() => {
// The value of length is changed upon a click, which triggers a re-render of UINode (1).
this.son.bag.pencil.length += 100;
})
Button("assign Son")
.onClick(() => {
// Changes to the regular variable son do not trigger UI re-renders of UINode (1).
this.son = new Son();
})
}
}
}
Inheritance
Properties in base or derived classes are observable only when decorated by \@Trace. In the following example, classes GrandFather, Father, Uncle, Son, and Cousin are declared. The following figure shows the inheritance relationship.
Create instances of the Son and Cousin classes. Clicks on Button(‘change Son age’) and Button(‘change Cousin age’) can trigger UI re-renders.
@ObservedV2
class GrandFather {
@Trace age: number = 0;
constructor(age: number) {
this.age = age;
}
}
class Father extends GrandFather{
constructor(father: number) {
super(father);
}
}
class Uncle extends GrandFather {
constructor(uncle: number) {
super(uncle);
}
}
class Son extends Father {
constructor(son: number) {
super(son);
}
}
class Cousin extends Uncle {
constructor(cousin: number) {
super(cousin);
}
}
@Entry
@ComponentV2
struct Index {
son: Son = new Son(0);
cousin: Cousin = new Cousin(0);
renderTimes: number = 0;
isRender(id: number): number {
console.info(`id: ${id} renderTimes: ${this.renderTimes}`);
this.renderTimes++;
return 40;
}
build() {
Row() {
Column() {
Text(`Son ${this.son.age}`)
.fontSize(this.isRender(1))
.fontWeight(FontWeight.Bold)
Text(`Cousin ${this.cousin.age}`)
.fontSize(this.isRender(2))
.fontWeight(FontWeight.Bold)
Button('change Son age')
.onClick(() => {
this.son.age++;
})
Button('change Cousin age')
.onClick(() => {
this.cousin.age++;
})
}
.width('100%')
}
.height('100%')
}
}
Decorating an Array of a Built-in Type with \@Trace
With an array of a built-in type decorated by \@Trace, changes caused by supported APIs can be observed. For details about the supported APIs, see Observed Changes. In the following example, the numberArr property in the \@ObservedV2 decorated Arr class is an \@Trace decorated array. If an array API is used to operate numberArr, the change caused can be observed. Perform length checks on arrays to prevent out-of-bounds access.
let nextId: number = 0;
@ObservedV2
class Arr {
id: number = 0;
@Trace numberArr: number[] = [];
constructor() {
this.id = nextId++;
this.numberArr = [0, 1, 2];
}
}
@Entry
@ComponentV2
struct Index {
arr: Arr = new Arr();
build() {
Column() {
Text(`length: ${this.arr.numberArr.length}`)
.fontSize(40)
Divider()
if (this.arr.numberArr.length >= 3) {
Text(`${this.arr.numberArr[0]}`)
.fontSize(40)
.onClick(() => {
this.arr.numberArr[0]++;
})
Text(`${this.arr.numberArr[1]}`)
.fontSize(40)
.onClick(() => {
this.arr.numberArr[1]++;
})
Text(`${this.arr.numberArr[2]}`)
.fontSize(40)
.onClick(() => {
this.arr.numberArr[2]++;
})
}
Divider()
ForEach(this.arr.numberArr, (item: number, index: number) => {
Text(`${index} ${item}`)
.fontSize(40)
})
Button('push')
.onClick(() => {
this.arr.numberArr.push(50);
})
Button('pop')
.onClick(() => {
this.arr.numberArr.pop();
})
Button('shift')
.onClick(() => {
this.arr.numberArr.shift();
})
Button('splice')
.onClick(() => {
this.arr.numberArr.splice(1, 0, 60);
})
Button('unshift')
.onClick(() => {
this.arr.numberArr.unshift(100);
})
Button('copywithin')
.onClick(() => {
this.arr.numberArr.copyWithin(0, 1, 2);
})
Button('fill')
.onClick(() => {
this.arr.numberArr.fill(0, 2, 4);
})
Button('reverse')
.onClick(() => {
this.arr.numberArr.reverse();
})
Button('sort')
.onClick(() => {
this.arr.numberArr.sort();
})
}
}
}
Decorating an Object Array with \@Trace
- In the following example, the personList object array and the age property in the Person class are decorated by \@Trace. As such, changes to personList and age can be observed.
- Clicking the Text component changes the value of age and thereby triggers a UI re-render of the Text component
let nextId: number = 0;
@ObservedV2
class Person {
@Trace age: number = 0;
constructor(age: number) {
this.age = age;
}
}
@ObservedV2
class Info {
id: number = 0;
@Trace personList: Person[] = [];
constructor() {
this.id = nextId++;
this.personList = [new Person(0), new Person(1), new Person(2)];
}
}
@Entry
@ComponentV2
struct Index {
info: Info = new Info();
build() {
Column() {
Text(`length: ${this.info.personList.length}`)
.fontSize(40)
Divider()
if (this.info.personList.length >= 3) {
Text(`${this.info.personList[0].age}`)
.fontSize(40)
.onClick(() => {
this.info.personList[0].age++;
})
Text(`${this.info.personList[1].age}`)
.fontSize(40)
.onClick(() => {
this.info.personList[1].age++;
})
Text(`${this.info.personList[2].age}`)
.fontSize(40)
.onClick(() => {
this.info.personList[2].age++;
})
}
Divider()
ForEach(this.info.personList, (item: Person, index: number) => {
Text(`${index} ${item.age}`)
.fontSize(40)
})
}
}
}
Decorating a Property of the Map Type with \@Trace
- With a property of the Map type decorated by \@Trace, changes caused by supported APIs, such as set, clear, and delete, can be observed.
- In the following example, the Info class is decorated by \@ObservedV2 and its memberMap property is decorated by \@Trace; as such, changes to the memberMap property caused by clicking Button(‘init map’) can be observed.
@ObservedV2
class Info {
@Trace memberMap: Map<number, string> = new Map([[0, "a"], [1, "b"], [3, "c"]]);
}
@Entry
@ComponentV2
struct MapSample {
info: Info = new Info();
build() {
Row() {
Column() {
ForEach(Array.from(this.info.memberMap.entries()), (item: [number, string]) => {
Text(`${item[0]}`)
.fontSize(30)
Text(`${item[1]}`)
.fontSize(30)
Divider()
})
Button('init map')
.onClick(() => {
this.info.memberMap = new Map([[0, "a"], [1, "b"], [3, "c"]]);
})
Button('set new one')
.onClick(() => {
this.info.memberMap.set(4, "d");
})
Button('clear')
.onClick(() => {
this.info.memberMap.clear();
})
Button('set the key: 0')
.onClick(() => {
this.info.memberMap.set(0, "aa");
})
Button('delete the first one')
.onClick(() => {
this.info.memberMap.delete(0);
})
}
.width('100%')
}
.height('100%')
}
}
Decorating a Property of the Set Type with \@Trace
- With a property of the Set type decorated by \@Trace, changes caused by supported APIs, such as add, clear, and delete, can be observed.
- In the following example, the Info class is decorated by \@ObservedV2 and its memberSet property is decorated by \@Trace; as such, changes to the memberSet property caused by clicking Button(‘init set’) can be observed.
@ObservedV2
class Info {
@Trace memberSet: Set<number> = new Set([0, 1, 2, 3, 4]);
}
@Entry
@ComponentV2
struct SetSample {
info: Info = new Info();
build() {
Row() {
Column() {
ForEach(Array.from(this.info.memberSet.entries()), (item: [number, string]) => {
Text(`${item[0]}`)
.fontSize(30)
Divider()
})
Button('init set')
.onClick(() => {
this.info.memberSet = new Set([0, 1, 2, 3, 4]);
})
Button('set new one')
.onClick(() => {
this.info.memberSet.add(5);
})
Button('clear')
.onClick(() => {
this.info.memberSet.clear();
})
Button('delete the first one')
.onClick(() => {
this.info.memberSet.delete(0);
})
}
.width('100%')
}
.height('100%')
}
}
Decorating a Property of the Date Type with \@Trace
- With a property of the Date type decorated by \@Trace, changes caused by the following APIs can be observed: setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds.
- In the following example, the Info class is decorated by \@ObservedV2 and its selectedDate property is decorated by \@Trace; as such, changes to the selectedDate property caused by clicking Button(‘set selectedDate to 2023-07-08’) can be observed.
@ObservedV2
class Info {
@Trace selectedDate: Date = new Date('2021-08-08')
}
@Entry
@ComponentV2
struct DateSample {
info: Info = new Info()
build() {
Column() {
Button('set selectedDate to 2023-07-08')
.margin(10)
.onClick(() => {
this.info.selectedDate = new Date('2023-07-08');
})
Button('increase the year by 1')
.margin(10)
.onClick(() => {
this.info.selectedDate.setFullYear(this.info.selectedDate.getFullYear() + 1);
})
Button('increase the month by 1')
.margin(10)
.onClick(() => {
this.info.selectedDate.setMonth(this.info.selectedDate.getMonth() + 1);
})
Button('increase the day by 1')
.margin(10)
.onClick(() => {
this.info.selectedDate.setDate(this.info.selectedDate.getDate() + 1);
})
DatePicker({
start: new Date('1970-1-1'),
end: new Date('2100-1-1'),
selected: this.info.selectedDate
})
}.width('100%')
}
}
你可能感兴趣的鸿蒙文章
harmony 鸿蒙\@AnimatableExtend Decorator: Definition of Animatable Attributes
harmony 鸿蒙Application State Management Overview
harmony 鸿蒙AppStorage: Storing Application-wide UI State
harmony 鸿蒙Basic Syntax Overview
harmony 鸿蒙\@Builder Decorator: Custom Builder Function
harmony 鸿蒙\@BuilderParam Decorator: Referencing the \@Builder Function
harmony 鸿蒙Creating a Custom Component
harmony 鸿蒙Mixing Use of Custom Components
harmony 鸿蒙Constraints on Access Modifiers of Custom Component Member Variables
- 所属分类: 后端技术
- 本文标签:
热门推荐
-
2、 - 优质文章
-
3、 gate.io
-
8、 golang
-
9、 openharmony
-
10、 Vue中input框自动聚焦