目录

@SyncMonitor Decorator: Synchronous Listening for Value Changes of the State Variables

To enhance the state management framework's capability of listening for state variable changes, you can use the @SyncMonitor decorator to listen for state variables.

@SyncMonitor provides the capability of synchronously listening for state variables of V2. Before reading this document, you are advised to read @ComponentV2, @ObservedV2 and @Trace, @Local, and @Monitor.

The @SyncMonitor decorator is supported since API version 23.

This decorator can be used in atomic services since API version 23.

Overview

The @SyncMonitor decorator is used to synchronously listen to the modification of state variables so that the state variables have the deep listening capability.

Usage Rules

[h2]Comparison between @Monitor, @SyncMonitor, and @Watch

Similar to the @Watch decorator, you must define the callback function. The difference is that @Watch uses the function name as the parameter, while @SyncMonitor directly decorates the callback function. @SyncMonitor with a wildcard listening path has the same listening scope as @Watch. Functions decorated by @Watch and @SyncMonitor are executed synchronously. The following table compares the usage and functions of @Monitor, @SyncMonitor, and @Watch.

Category @Watch @Monitor @SyncMonitor
Parameters Callback method name. Listened state variable name and attribute name Listened state variable name and attribute name
Number of Intercepted Targets Single state variable Multiple state variables Multiple state variables
Type Fuzzy Listening Fuzzy interception and precise interception are supported. Fuzzy interception and precise interception are supported.
Whether to Obtain the Value Before the Change No Yes Yes
Observation Condition The observed object is a state variable. The observed object is a state variable or a class member attribute decorated with @Trace. The observed object is a state variable or a class member attribute decorated with @Trace.
Constraints Only in custom components decorated by @Component In the custom component decorated by @ComponentV2 and the class decorated by @ObservedV2 In the custom component decorated by @ComponentV2 and the class decorated by @ObservedV2
Whether Wildcards Are Supported No Not supported by default. Supported through configuration items since API version 26.0.0. Yes (since API version 23)
Callback Invoking Time Immediately (synchronized) After the status change function ends (asynchronous), multiple changes are triggered only once. Immediately (synchronized)

addMonitor and clearMonitor APIs allows you to dynamically add and clear listeners during application execution. When isSynchronous is set to true, addMonitor is similar to @SyncMonitor. When isSynchronous is set to false, addMonitor is similar to @Monitor.

@Monitor and @SyncMonitor are member function decorators of the @ComponentV2 and @ObservedV2 classes, respectively. They are part of V2 state management. @Watch is a variable decorator used in @Component. It is a part of V1 state management.

Functions decorated by @Monitor are executed asynchronously after the event handler is executed. After the @SyncMonitor and @Watch functions detect the change of the state variable, the callback function is executed immediately.

The execution of the @Monitor function can be triggered by the value change of one or more specific tracing object attributes. The @Watch function is executed when any observed object attribute or array item changes. It cannot listen to one or more specific attributes.

The behavior of @SyncMonitor with wildcard characters in the path is the same as that of @Watch. This makes it easier for applications to migrate from V1 state management to V2 state management. The following is an example:

@Watch sample code for V1:

@Component
struct CompV1 {
  @State @Watch('watchFuncName') varName: ClassA = initialValue; // ClassA is of the class type.
  watchFuncName(propertyStr: string) {
    // ....
  }
}

Sample code for migrating to @SyncMonitor in V2:

@ComponentV2
struct CompV2 {
  @Local varName: ClassA = initialValue;
  @SyncMonitor('varName.*') monitorFuncName(m: IMonitor) {
    // ....
  }
}

ClassA indicates the complex object type. The following example uses @SyncMonitor and @Monitor to track the change of the sum attribute.

The code calculates the sum of array elements. When the sum is calculated in a loop, the sum value changes to 1, 3, and 6 in sequence.

@Monitor is called only once. The value of before is 0, and the value of now is 6.

@SyncMonitor calls it for three times, corresponding to the changes from 0 to 1, from 1 to 3, and from 3 to 6.

import { hilog } from '@kit.PerformanceAnalysisKit';

@Entry
@ComponentV2
struct DocSampleArraySum {
  @Local sum: number = 0;
  arr: Array<number> = [1, 2, 3];
  @SyncMonitor('sum')
  syncSumMonitor(info: IMonitor) {
    let path = info.dirty[0];
    hilog.info(0xFF00, 'SyncMonitor', '%{public}s', `${path} changed from ${info.value(path)?.before} to ${info.value(path)?.now}`);
  }
  @Monitor('sum')
  asyncSumMonitor(info: IMonitor) {
    let path = info.dirty[0];
    hilog.info(0xFF00, 'Monitor', '%{public}s', `${path} changed from ${info.value(path)?.before} to ${info.value(path)?.now}`);
  }

  build() {
    Column() {
      Button('Calculate a sum')
        .onClick(() => {  // When the sum is modified, syncSumMonitor is called back for three times, and asyncSumMonitor is called back only once.
          this.sum = 0;
          this.arr.forEach((element) => this.sum += element);
        })
    }
  }
}

Log output:

SyncMonitor - sum changed from 0 to 1
SyncMonitor - sum changed from 1 to 3
SyncMonitor - sum changed from 3 to 6
Monitor - sum changed from 0 to 6

Decorator Description

@SyncMonitor Attribute Decorator Description
Parameters Object attribute name of the string type. Multiple object attributes can be listened at the same time. Use commas (,) to separate attributes, for example, @SyncMonitor ('prop1','prop2'). In addition, deep attribute changes can be monitored: elements in multi-dimensional arrays, attributes in nested objects, and attributes in object arrays. The wildcard (*) can be added at the end of a path to listen to the changes of any observed attribute. For details, see Listening for Changes.
Decorated Content Member method. When an observed property changes, this callback is triggered. The callback takes a variable of the IMonitor type as a parameter, from which developers can obtain information about the changes before and after the modification.

Wildcards in the Listening Path

When the listening path of the @SyncMonitor decorator uses the wildcard (*):

Syntax rules for the wildcard (*) path:

The following is an example of a valid path:

Available APIs

For the API description of the IMonitor and IMonitorValue<T> types, see @Monitor: Listening for Value Changes of the State Variables.

Listening for Changes

[h2]Using @SyncMonitor in a Custom Component Decorated by @ComponentV2

When the state variable monitored by @SyncMonitor changes, the callback method of @SyncMonitor is triggered.

[h2]Use @SyncMonitor in the class decorated by @ObservedV2.

When the attribute monitored by @SyncMonitor changes, the callback method of @SyncMonitor is triggered.

[h2]General Listening Capability

@SyncMonitor also provides some common listening capabilities.

Wildcards in the Observation Path

The wildcard (*) in the @SyncMonitor path can be used to trigger a callback when any object attribute or array item is changed.

[h2]When the observed attribute changes or a value is assigned to an object, the listening function is automatically executed.

import { hilog } from '@kit.PerformanceAnalysisKit';

@ObservedV2
class ClassA {
  @Trace public propA: number = 8;
  @Trace public propB: number = 99;

  constructor(a: number, b: number) {
    this.propA = a;
    this.propB = b;
  }
}

@Entry
@ComponentV2
struct DocSampleClass {
  @Local cls: ClassA = new ClassA(100, 100);

  @SyncMonitor('cls.*')
  onClsChanged(m: IMonitor) {
    hilog.info(0xFF00, 'testTag', '%{public}s', `### onClsChanged, dirty: ${m.dirty.toString()}`);
  }

  build() {
    Column() {
      Divider()
      Button(`#1 Change propA ${this.cls.propA}: +=1`)
        .onClick(() => {
          this.cls.propA += 1; // Trigger onClsChanged.
        })
      Button(`#2 Change propB ${this.cls.propB}: +=1`)
        .onClick(() => {
          this.cls.propB += 1; // Trigger onClsChanged.
        })
      Button(`#3 Assign class object`)
        .onClick(() => {
          this.cls = new ClassA(-200, -200); // Trigger onClsChanged.
        })
    }
    .border({ style: BorderStyle.Solid, width: 2, color: Color.Green })
  }
}

When button #1 or #2 is clicked (to update the properties of the listened object cls), the value of m.dirty is the same, including m.dirty==['cls.*']. The framework cannot accurately tell which attribute triggers the execution of the listening function.

For button #3 (assigning a new object to the cls attribute), the framework transfers cls to the dirty attribute array, that is, m.dirty==['cls.*'].

Click Button #1. The following log is displayed:
### onClsChanged, dirty: cls.*

Click Button #2. The following log is displayed:
### onClsChanged, dirty: cls.*

Click Button #3. The following log is displayed:
### onClsChanged, dirty: cls.*

[h2]When an array item is changed or a value is assigned to an array, the listening function is automatically executed.

Path for observing the synchronization listener: @SyncMonitor('arrayOfPerson.*')

import { hilog } from '@kit.PerformanceAnalysisKit';

@ObservedV2
class Person {
  @Trace public firstName: string;
  @Trace public lastName: string;

  constructor(first: string = 'no first', last: string = 'no last') {
    this.firstName = first;
    this.lastName = last;
  }
}

@ObservedV2
class ArrayOfPerson extends Array<Person> {
}

@Entry
@ComponentV2
struct DocSampleArray {
  @Local arrayOfPerson: ArrayOfPerson =
    [new Person('Adrian'), new Person('Andrew'), new Person('Aaliyah'), new Person('Amir'), new Person('Angel')];

  @SyncMonitor('arrayOfPerson.*')
  arrayOfPersonMonitor(monitor: IMonitor) {
    hilog.info(0xFF00, 'testTag', '%{public}s', `### SyncMonitor dirty: ${monitor.dirty.toString()}`);
  }

  build() {
    Column() {
      Button('#1 arrayOfPerson.push')
        .onClick(() => {
          // Trigger the arrayOfPersonMonitor callback.
          this.arrayOfPerson.push(new Person('Austin'));
        })
      Button('#2 arrayOfPerson.splice(0,1,P)')
        .onClick(() => {
          // Trigger the arrayOfPersonMonitor callback.
          this.arrayOfPerson.splice(0, 1, new Person('Addison'));
        })
      Button('#3 arrayOfPerson.assign new [1]')
        .onClick(() => {
          if (this.arrayOfPerson.length > 1) {
            // Trigger the arrayOfPersonMonitor callback.
            this.arrayOfPerson[1] = new Person('Andy');
          }
        })
      Button('#4 arrayOfPerson shift')
        .onClick(() => {
          if (this.arrayOfPerson.length > 2) {
            // Trigger the arrayOfPersonMonitor callback.
            this.arrayOfPerson.shift();
          }
        })
      Button('#5 arrayOfPerson length change')
        .onClick(() => {
          // Trigger the arrayOfPersonMonitor callback.
          this.arrayOfPerson.length = this.arrayOfPerson.length + 1;
        })
      Button('#6 arrayOfPerson = new Array')
        .onClick(() => {
          // Trigger the arrayOfPersonMonitor callback.
          this.arrayOfPerson = new ArrayOfPerson(new Person('Adrian'), new Person('Andrew'))
        })
      Button('#7 arrayOfPerson [1] last name')
        .onClick(() => {
          if (this.arrayOfPerson.length > 1 && this.arrayOfPerson[1] instanceof Person) {
            // The arrayOfPersonMonitor callback is not triggered.
            this.arrayOfPerson[1].lastName += '~'
          }
        })
    }
  }
}

When you press buttons 1 to 6, the listener function is triggered.

### SyncMonitor dirty: arrayOfPerson.*

When you press button 7, the listener function is not called because no array item is changed.

[h2]The Listening Function Is Executed When the Attributes of the Nested Observed Object Are Changed

import { hilog } from '@kit.PerformanceAnalysisKit';

@ObservedV2
class Person {
  @Trace public firstName: string = 'first';
  @Trace public lastName: string = 'last';
}

@ObservedV2
class Class1 {
  @Trace public person: Person = new Person();
}

@ObservedV2
class Class0 {
  @Trace public class1: Class1 = new Class1();
}

@Entry
@ComponentV2
export struct DocSampleNestedClass {
  @Local class0: Class0 | number = new Class0();

  @SyncMonitor('class0.class1.person.*')
  onPersonChange(info: IMonitor) {
    hilog.info(0xFF00, 'testTag', '%{public}s', '### onPersonChange, dirty: ' + info.dirty.toString());
  }

  build() {
    Column() {
      Button('#1 Class0 = new Class')
        .onClick(() => {
          // Trigger the @SyncMonitor callback.
          this.class0 = new Class0();
        })
      Button('#2 Class0 = new Class, keep Class1')
        .onClick(() => {
          if (this.class0 instanceof Class0) {
            // When class0 is of the Class0 type, the @SyncMonitor callback is not triggered.
            let newClass0 = new Class0();
            newClass0.class1.person = (this.class0 as Class0).class1.person;
            this.class0 = newClass0;
          }
        })
      Button('#3 Class0.class1 = new Class1')
        .onClick(() => {
          if (this.class0 instanceof Class0) {
            // When class0 is of the Class0 type, the @SyncMonitor callback is triggered.
            (this.class0 as Class0).class1 = new Class1();
          }
        })
      Button('#4 Class0.class1.person = new Person')
        .onClick(() => {
          if (this.class0 instanceof Class0) {
            // When class0 is of the Class0 type, the @SyncMonitor callback is triggered.
            (this.class0 as Class0).class1.person = new Person();
          }
        })
      Button('#5 Class0....person.last update')
        .onClick(() => {
          if (this.class0 instanceof Class0) {
            // When class0 is of the Class0 type, the @SyncMonitor callback is triggered.
            (this.class0 as Class0).class1.person.lastName += '+';
          }
        })
      Button('#6 Class0 toggle number <=> new Class0')
        .onClick(() => {
          // Trigger the @SyncMonitor callback.
          this.class0 = (typeof this.class0 === 'object') ? 500 : new Class0();
        })
    }
  }
}

Start the application. When the #1 Class0 = new Class button is pressed, the listening function is triggered because the Person object has been changed.

### onPersonChange, dirty: class0.class1.person.*

Start the application. When the #2 Class0 = new Class, keep Class1 button is pressed, the listening function is not executed because the listened Person object remains unchanged.

Start the application. When the #3 Class0.class1 = new Class1 button is pressed, the listening function is triggered because the Person object has been changed.

### onPersonChange, dirty: class0.class1.person.*

Start the application. When the #4 Class0.class1.person = new Person button is pressed, the listening function is triggered because the Person object has been changed.

### onPersonChange, dirty: class0.class1.person.*

Start the application. When the #5 Class0....person.last update button is pressed, the listening function is triggered because the attributes of the Person object have been changed.

### onPersonChange, dirty: class0.class1.person.*

Start the application. When the #6 Class0 toggle number <=> new Class0 button is pressed, the listening function is triggered because the path value is changed from the Person class reference to undefined.

### onPersonChange, dirty: class0.class1.person.*

When the same button #6 Class0 toggle number <=> new Class0 is pressed for the second time, the framework calls the listening function again and notifies it that the object has been changed from undefined to an instance of the Person class.

@Monitor and @SyncMonitor differ in how they handle paths that become unavailable.

@SyncMonitor triggers execution in both cases - when the path becomes unavailable and when the path becomes available again.

@Monitor triggers the execution of only one condition - only when the path becomes available again. When undefined is assigned to an object in the path, the callback function decorated by @Monitor will not be triggered.

If the sample application contains @Monitor('class0.class1.person'), @Monitor cannot listen to the change when the path becomes unavailable. When this.class0 = 500 is used to change the value, the callback function decorated by @Monitor is not triggered. When a value is assigned to Class0 again, that is, this.class0 = new Class0, the callback function decorated by @Monitor is triggered.

[h2]Wildcard Listening for Array Item Changes

When the properties of a nested observed object are changed, the listening function is executed.

The listening function is also executed when the array and array item changes are observed.

In the following example, there are two @SyncMonitor listening paths: topArray.1.* and topArray.*.

import { hilog } from '@kit.PerformanceAnalysisKit';

@ObservedV2
class Person {
  @Trace firstName: string = 'first';
  @Trace lastName: string = 'last';

  constructor(first: string = 'no first', last: string = 'no last') {
    this.firstName = first;
    this.lastName = last;
  }
}

@ObservedV2
class ArrayOfPerson extends Array<Person> {
}

@ObservedV2
class TopArray extends Array<ArrayOfPerson> {
}

@Entry
@ComponentV2
struct DocSampleArrayOfArrays {
  @Local topArray: TopArray = this.makeNewTopArray();

  @SyncMonitor('topArray.1.*')
  topArrayMonitor1Star(monitor: IMonitor) {
    hilog.info(0xFF00, 'testTag', '%{public}s', `TopArray[1]: ${monitor.dirty.toString()}`);
  }

  @SyncMonitor('topArray.*')
  topArrayMonitorStar(monitor: IMonitor) {
    hilog.info(0xFF00, 'testTag', '%{public}s', `TopArray: ${monitor.dirty.toString()}`);
  }

  makeNewTopArray(): TopArray {
    return new TopArray(
      new ArrayOfPerson(new Person('Adrian'), new Person('Andrew'), new Person('Aaliyah'), new Person('Amir'),
        new Person('Angel')),
      new ArrayOfPerson(new Person('Carter'), new Person('Charlie'), new Person('Cooper'), new Person('Cole'),
        new Person('Callie')),
      new ArrayOfPerson(new Person('Daniel'), new Person('Daisy'), new Person('Dawson'), new Person('Dana'),
        new Person('Dalton'))
    );
  }

  build() {
    Column() {
      // Both the topArrayMonitor1Star and topArrayMonitorStar callbacks are triggered.
      Button('topArray = new TopArray')
        .onClick(() => {
          this.topArray = this.makeNewTopArray();
        })

      // When topArray[1][0] exists, the topArrayMonitor1Star callback is triggered, but the topArrayMonitorStar callback is not triggered.
      Button('topArray[1][0] = new Person')
        .onClick(() => {
          if (this.topArray.length > 1 && this.topArray[1].length > 0) {
            this.topArray[1][0] = new Person();
          }
        })

      // When topArray[0][1] exists, neither the topArrayMonitor1Star nor the topArrayMonitorStar callback is triggered.
      Button('topArray[0][1] = new Person')
        .onClick(() => {
          if (this.topArray.length > 0 && this.topArray[0].length > 1) {
            this.topArray[0][1] = new Person();
          }
        })

      // When topArray[1] exists, the topArrayMonitor1Star callback is triggered, but the topArrayMonitorStar callback is not triggered.
      Button('topArray[1].push')
        .onClick(() => {
          if (this.topArray.length > 1 && this.topArray[1] instanceof ArrayOfPerson) {
            this.topArray[1].push(new Person());
          }
        })

      // When the length of topArray is greater than 2, both the topArrayMonitor1Star and topArrayMonitorStar callbacks are triggered.
      Button('topArray.shift (length>2)')
        .onClick(() => {
          if (this.topArray.length > 2) {
            this.topArray.shift();
          }
        })

      // When topArray[0] exists, the topArrayMonitor1Star callback is not triggered, but the topArrayMonitorStar callback is triggered.
      Button('topArray[0] = new ArrayOfPerson')
        .onClick(() => {
          if (this.topArray.length > 0) {
            this.topArray[0] = new ArrayOfPerson(new Person(), new Person());
          }
        })

      // When topArray[1][0] exists, neither the topArrayMonitor1Star nor the topArrayMonitorStar callback is triggered.
      Button('topArray[1][0].last update')
        .onClick(() => {
          if (this.topArray.length > 1 && this.topArray[1].length > 0 && this.topArray[1][0] instanceof Person) {
            this.topArray[1][0].lastName += '~';
          }
        })

      // The topArrayMonitor1Star callback is not triggered, but the topArrayMonitorStar callback is triggered.
      Button('topArray = new TopArray, keep [1]')
        .onClick(() => {
          let newTop = this.makeNewTopArray();
          newTop[1] = this.topArray[1]; // topArray.1 is not changed, and the last determined value before the wildcard in the path 'topArray.1.*' is not changed.
          this.topArray = newTop;
        })

      // The topArrayMonitor1Star callback is not triggered, but the topArrayMonitorStar callback is triggered.
      Button('topArray.push')
        .onClick(() => {
          this.topArray.push(new ArrayOfPerson(new Person(), new Person()));
        })
    }
  }
}

Constraints

Pay attention to the following restrictions when using @SyncMonitor:

Use Cases

[h2]Listening for Deep Property Changes

@SyncMonitor can listen to the changes of deep attributes and classify the attributes based on the values before and after the changes.

In the following example, the change of property value is listened to and the display style of the Text component is changed based on the change amplitude.

@ObservedV2
class Info {
  @Trace public value: number = 50;
}

@ObservedV2
class UIStyle {
  public info: Info = new Info();
  @Trace public color: Color = Color.Black;
  @Trace public fontSize: number = 45;

  @SyncMonitor('info.value')
  onValueChange(monitor: IMonitor) {
    let lastValue: number = monitor.value()?.before as number;
    let curValue: number = monitor.value()?.now as number;
    if (lastValue != 0) {
      let diffPercent: number = (curValue - lastValue) / lastValue;
      if (diffPercent > 0.1) {
        this.color = Color.Red;
        this.fontSize = 50;
      } else if (diffPercent < -0.1) {
        this.color = Color.Green;
        this.fontSize = 40;
      } else {
        this.color = Color.Black;
        this.fontSize = 45;
      }
    }
  }
}

@Entry
@ComponentV2
struct Index {
  textStyle: UIStyle = new UIStyle();

  build() {
    Column() {
      Text(`Important Value: ${this.textStyle.info.value}`)
        .fontColor(this.textStyle.color)
        .fontSize(this.textStyle.fontSize)
      Button('change!')
        .onClick(() => {
          this.textStyle.info.value = Math.floor(Math.random() * 100) + 1;
        })
    }
  }
}

FAQs

[h2]Effective and Expiration Time of Variable Listening by the @SyncMonitor in the Custom Component

When @SyncMonitor is defined in a custom component decorated by @ComponentV2, @SyncMonitor takes effect after the state variable is initialized and becomes invalid when the component is destroyed.

import { hilog } from '@kit.PerformanceAnalysisKit';

@ObservedV2
class Info {
  @Trace public message: string = 'not initialized';

  constructor() {
    hilog.info(0xFF00, 'testTag', '%{public}s', 'in constructor message change to initialized');
    this.message = 'initialized';
  }
}

@ComponentV2
struct Child {
  @Param info: Info = new Info();

  @SyncMonitor('info.message')
  onMessageChange(monitor: IMonitor) {
    hilog.info(0xFF00, 'testTag', '%{public}s',
      `Child message change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
  }

  aboutToAppear(): void {
    this.info.message = 'Child aboutToAppear';
  }

  aboutToDisappear(): void {
    hilog.info(0xFF00, 'testTag', '%{public}s', 'Child aboutToDisappear');
    this.info.message = 'Child aboutToDisappear';
  }

  build() {
    Column() {
      Text('Child')
      Button('change message in Child')
        .onClick(() => {
          this.info.message = 'Child click to change Message';
        })
    }
    .borderColor(Color.Red)
    .borderWidth(2)

  }
}

@Entry
@ComponentV2
struct Index {
  @Local info: Info = new Info();
  @Local flag: boolean = false;

  @SyncMonitor('info.message')
  onMessageChange(monitor: IMonitor) {
    hilog.info(0xFF00, 'testTag', '%{public}s',
      `Index message change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
  }

  build() {
    Column() {
      Button('show/hide Child')
        .onClick(() => {
          this.flag = !this.flag;
        })
      Button('change message in Index')
        .onClick(() => {
          this.info.message = 'Index click to change Message';
        })
      if (this.flag) {
        Child({ info: this.info })
      }
    }
  }
}

In the preceding example, you can create and destroy a Child component to observe the effective and expiration time of the @SyncMonitor defined in the custom component. You are advised to follow the steps below:

The preceding steps indicate that the @SyncMonitor defined in the Child component takes effect when the Child component is created and initialized, and becomes invalid when the Child component is destroyed.

[h2]Effective and Expiration Time of Variable Listening by the @SyncMonitor in the Class

When @SyncMonitor is defined in a class decorated with @ObservedV2, it takes effect after the class instance is created and becomes invalid when the class instance is destroyed.

import { hilog } from '@kit.PerformanceAnalysisKit';

@ObservedV2
class Info {
  @Trace public message: string = 'not initialized';

  constructor() {
    this.message = 'initialized';
  }

  @SyncMonitor('message')
  onMessageChange(monitor: IMonitor) {
    hilog.info(0xFF00, 'testTag', '%{public}s',
      `message change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
  }
}

@Entry
@ComponentV2
struct Index {
  info: Info = new Info();

  aboutToAppear(): void {
    this.info.message = 'Index aboutToAppear';
  }

  build() {
    Column() {
      Button('change message')
        .onClick(() => {
          this.info.message = 'Index click to change message';
        })
    }
  }
}

In the preceding example, @SyncMonitor takes effect after the info class is created, which is later than the constructor of the class and earlier than the aboutToAppear of the custom component. After the page is loaded, click the change message button to modify the message variable. The log outputs the messages as below:

message change from initialized to Index aboutToAppear
message change from Index aboutToAppear to Index click to change message

@SyncMonitor defined in a class becomes invalid when the class is destroyed. However, since the actual destruction and release of the class depends on the garbage collection mechanism, a situation may occur where the class is not destroyed in time even though the custom component that hosts it has already been destroyed. As a result, the @SyncMonitor defined in the class still listens for changes.

import { hilog } from '@kit.PerformanceAnalysisKit';

@ObservedV2
class InfoWrapper {
  public info?: Info;

  constructor(info: Info) {
    this.info = info;
  }

  @SyncMonitor('info.age')
  onInfoAgeChange(monitor: IMonitor) {
    hilog.info(0xFF00, 'testTag', '%{public}s',
      `age change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
  }
}

@ObservedV2
class Info {
  @Trace public age: number;

  constructor(age: number) {
    this.age = age;
  }
}

@ComponentV2
struct Child {
  @Param @Require infoWrapper: InfoWrapper;

  aboutToDisappear(): void {
    hilog.info(0xFF00, 'testTag', '%{public}s', 'Child aboutToDisappear');
  }

  build() {
    Column() {
      Text(`${this.infoWrapper.info?.age}`)
    }
  }
}

@Entry
@ComponentV2
struct Index {
  dataArray: Info[] = [];
  @Local showFlag: boolean = true;

  aboutToAppear(): void {
    for (let i = 0; i < 5; i++) {
      this.dataArray.push(new Info(i));
    }
  }

  build() {
    Column() {
      Button('change showFlag')
        .onClick(() => {
          this.showFlag = !this.showFlag;
        })
      Button('change number')
        .onClick(() => {
          hilog.info(0xFF00, 'testTag', '%{public}s', 'click to change age');
          this.dataArray.forEach((info: Info) => {
            info.age += 100;
          });
        })
      if (this.showFlag) {
        Column() {
          Text('Children')
          ForEach(this.dataArray, (info: Info) => {
            Child({ infoWrapper: new InfoWrapper(info) })
          })
        }
        .borderColor(Color.Red)
        .borderWidth(2)
      }
    }
  }
}

In the preceding example, when you click change showFlag to switch the condition of the if component, the Child component is destroyed. But when you click change number to change the value of age, the @SyncMonitor callback defined in InfoWrapper is still triggered. This is because the custom component Child has executed aboutToDisappear, but its member variable infoWrapper is not destroyed immediately. When the variable changes, the onInfoAgeChange method defined in infoWrapper can still be called, therefore, the @SyncMonitor callback is still triggered.

The result is unstable when you use the garbage collection mechanism to cancel the listening of @SyncMonitor. You can use the following methods to manage the expiration time of the @SyncMonitor:

Define @SyncMonitor in a custom component. When a custom component is destroyed, the state management framework cancels the @SyncMonitor listening. Therefore, after the custom component calls aboutToDisappear, the @SyncMonitor callback will no longer be triggered, even though the custom component's data may not have been released yet. Unlike @Monitor, when the custom component is about to be destroyed, even if the object listened by @SyncMonitor is actively set to empty, @SyncMonitor can still listen for changes of the original target.

import { hilog } from '@kit.PerformanceAnalysisKit';

@ObservedV2
class InfoWrapper {
  public info?: Info;

  constructor(info: Info) {
    this.info = info;
  }
}

@ObservedV2
class Info {
  @Trace public age: number;

  constructor(age: number) {
    this.age = age;
  }
}

@ComponentV2
struct Child {
  @Param @Require infoWrapper: InfoWrapper;

  @SyncMonitor('infoWrapper.info.age')
  onInfoAgeChange(monitor: IMonitor) {
    hilog.info(0xFF00, 'testTag', '%{public}s',
      `age change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
  }

  aboutToDisappear(): void {
    hilog.info(0xFF00, 'testTag', '%{public}s', 'Child aboutToDisappear');
  }

  build() {
    Column() {
      Text(`${this.infoWrapper.info?.age}`)
    }
  }
}

@Entry
@ComponentV2
struct Index {
  dataArray: Info[] = [];
  @Local showFlag: boolean = true;

  aboutToAppear(): void {
    for (let i = 0; i < 5; i++) {
      this.dataArray.push(new Info(i));
    }
  }

  build() {
    Column() {
      Button('change showFlag')
        .onClick(() => {
          this.showFlag = !this.showFlag;
        })
      Button('change number')
        .onClick(() => {
          hilog.info(0xFF00, 'testTag', '%{public}s', 'click to change age');
          this.dataArray.forEach((info: Info) => {
            info.age += 100;
          });
        })
      if (this.showFlag) {
        Column() {
          Text('Children')
          ForEach(this.dataArray, (info: Info) => {
            Child({ infoWrapper: new InfoWrapper(info) })
          })
        }
        .borderColor(Color.Red)
        .borderWidth(2)
      }
    }
  }
}

[h2]Passing Correct Input Parameters to @SyncMonitor

@SyncMonitor cannot verify input parameters during compilation. Currently, the following statements do not meet the listening condition, but @SyncMonitor is still triggered. Developers must correctly pass @SyncMonitor input parameters and avoid passing non-state variables to prevent function exceptions or unexpected behavior.

[Incorrect Usage 1]

import { hilog } from '@kit.PerformanceAnalysisKit';

@ObservedV2
class Info {
  public name: string = 'John';
  @Trace public age: number = 24;

  // Only state variable 'age' can be observed. Observing the non-state variable 'name' will cause a compilation warning.
  // Note: `Cannot observe non-existent variables or non-state variables, except in wildcard-based monitoring scenarios.`
  @SyncMonitor('age', 'name')
  onPropertyChange(monitor: IMonitor) {
    monitor.dirty.forEach((path: string) => {
      hilog.info(0xFF00, 'testTag', '%{public}s',
        `property path:${path} change from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
    })
  }
}

@Entry
@ComponentV2
struct Index {
  info: Info = new Info();

  build() {
    Column() {
      Button('change age&name')
        .onClick(() => {
          this.info.age = 25; // Change both state variable age and non-state variable name.
          this.info.name = 'Johny';
        })
    }
  }
}

In the preceding code, an alarm is reported during compilation because the @SyncMonitor input parameter transfers a non-state variable name. You are advised to remove the listening from the name attribute or use @Trace to decorate the name attribute as a state variable.

When you click to change the state variable age and non-state variable name at the same time, the following log is generated:

property path:age change from 24 to 25

[Correct Usage 1]

import { hilog } from '@kit.PerformanceAnalysisKit';

@ObservedV2
class Info {
  public name: string = 'John';
  @Trace public age: number = 24;

  // Observe only the state variable age
  @SyncMonitor('age')
  onPropertyChange(monitor: IMonitor) {
    monitor.dirty.forEach((path: string) => {
      hilog.info(0xFF00, 'testTag', '%{public}s',
        `property path:${path} change from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
    })
  }
}

@Entry
@ComponentV2
struct Index {
  info: Info = new Info();

  build() {
    Column() {
      Button('change age&name')
        .onClick(() => {
          this.info.age = 25; // The state variable "age" is changed.
          this.info.name = 'Johny';
        })
    }
  }
}

[Negative example 2]

import { hilog } from '@kit.PerformanceAnalysisKit';

@ObservedV2
class Info {
  public name: string = 'John';
  public age: number = 24;

  get myAge() {
    return this.age; // age is a non-state variable.
  }

  // Listen to the non-@Computed decorated getter accessor. An alarm is reported during compilation.
  @SyncMonitor('myAge')
  onPropertyChange(monitor: IMonitor) {
    monitor.dirty.forEach((path: string) => {
      hilog.info(0xFF00, 'testTag', '%{public}s',
        `property path:${path} change from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
    })
  }
}

@Entry
@ComponentV2
struct Index {
  info: Info = new Info();

  build() {
    Column() {
      Button('change age')
        .onClick(() => {
          this.info.age = 25; // The state variable "age" is changed.
        })
    }
  }
}

In the preceding code, the input parameter of @SyncMonitor is the name of a getter accessor. This accessor is not decorated by @Computed and is not a variable that can be listened for. You are advised to add an @Computed decorator to myAge or directly listen for the state variable itself when the getter accessor returns the state variable.

[Correct Usage 2]

Change myAge to a state variable:

import { hilog } from '@kit.PerformanceAnalysisKit';

@ObservedV2
class Info {
  public name: string = 'John';
  @Trace public age: number = 24;

  // Add @Computed to make myAge a state variable
  @Computed
  get myAge() {
    return this.age;
  }

  // Observe the @Computed-decorated getter accessor
  @SyncMonitor('myAge')
  onPropertyChange() {
    hilog.info(0xFF00, 'testTag', '%{public}s', 'age changed');
  }
}

@Entry
@ComponentV2
struct Index {
  info: Info = new Info();

  build() {
    Column() {
      Button('change age')
        .onClick(() => {
          this.info.age = 25; // The state variable "age" is changed.
        })
    }
  }
}

Alternatively, listen to the state variable itself.

import { hilog } from '@kit.PerformanceAnalysisKit';

@ObservedV2
class Info {
  public name: string = 'John';
  @Trace public age: number = 24;

  // Observe the state variable age directly
  @SyncMonitor('age')
  onPropertyChange() {
    hilog.info(0xFF00, 'testTag', '%{public}s', 'age changed');
  }
}

@Entry
@ComponentV2
struct Index {
  info: Info = new Info();

  build() {
    Column() {
      Button('change age')
        .onClick(() => {
          this.info.age = 25; // The state variable "age" is changed.
        })
    }
  }
}

[h2]Invalid Path Using Wildcards

Valid listening path rules are as follows:

Invalid wildcards are used in the following paths: An error is reported during compilation.

[h2]Observed Variables During Accessibility Changes

@Monitor only saves values when variables are accessible. When a state variable becomes inaccessible, value changes aren't recorded. Since API version 20, if you need to observe accessibility changes (from accessible to inaccessible or vice versa), use addMonitor.

@SyncMonitor can listen to the change of a variable from accessible to inaccessible or from inaccessible to accessible. In the following example, the onChange callback is triggered when the three buttons are clicked.

import { hilog } from '@kit.PerformanceAnalysisKit';

@ObservedV2
class User {
  @Trace public age: number = 10;
}

@Entry
@ComponentV2
struct Page {
  @Local user: User | undefined | null = new User();

  @SyncMonitor('user.age')
  onChange(mon: IMonitor) {
    mon.dirty.forEach((path: string) => {
      hilog.info(0xFF00, 'testTag', '%{public}s',
        `onChange: User property ${path} change from ${mon.value(path)?.before} to ${mon.value(path)?.now}`);
    });
  }

  build() {
    Column() {
      Text(`User age ${this.user?.age}`).fontSize(20)
      Button('set user to undefined').onClick(() => {
        // age: from accessible to inaccessible
        this.user = undefined;
      })
      Button('set user to User').onClick(() => {
        // age: from inaccessible to accessible
        this.user = new User();
      })
      Button('set user to null').onClick(() => {
        // age: from accessible to inaccessible
        this.user = null;
      })
    }
  }
}