DEV Community

Studotie Handwer
Studotie Handwer

Posted on

The Evolution of ArkTS and ArkUI State Management: From V1 to V2

After a period of development, ArkTS and ArkUI have moved beyond the stage of merely borrowing strengths from other frameworks. They are now evolving their own distinct styles and taking unique paths. One of the most critical systems in ArkUI's runtime—state management, which handles data and page interaction—has naturally undergone an upgrade from V1 to V2. To keep up with the latest trends, I’ve started a new project fully built using V2 state management. Below is a brief overview of the features I’ve used so far and the pitfalls I’ve encountered.


Differences Between State Management V1 and V2

State Management V1: Proxy Observation Pattern

Core Mechanism: When creating a state variable, a proxy observer is created simultaneously. This observer can only detect changes at the proxy level.

Usage Limitations:

  • State variables are tightly coupled with the UI and cannot exist independently.
  • When multiple views proxy the same data, changes made in one view do not notify the others to update.
  • Observation is shallow—only first-level property changes are detected. Deep observation and listening are not supported.

State Management V2: Data Itself is Observable

Core Mechanism: The data itself becomes observable. Any data changes directly trigger the corresponding view updates.

Advantages:

  • State variables are independent of the UI, and data changes are instantly reflected across all relevant views, ensuring data and UI are always synchronized.
  • Supports deep observation and listening, with an optimized observation mechanism that maintains performance.
  • Enables precise updates at the object property level and minimal updates for array elements, improving efficiency.
  • Decorators are user-friendly and highly extensible. Input and output within components are clear, facilitating componentized development and maintenance.

State Variables

Let’s start with the basics—creating a state variable. In V1, this was done using @State, but in V2, a new decorator @Local is used:

@Local var: SomeType = SomeValue
Enter fullscreen mode Exit fullscreen mode

Compared to @State, @Local does not allow external initialization, making its role as “internal state” clearer.

Additionally, with the new @ObservedV2 and @Trace capabilities, observing internal state variables is simpler and no longer requires @ObjectLink.

The @ObservedV2 and @Trace decorators are used to decorate classes and their properties, enabling deep observation:

  • @ObservedV2 and @Trace must be used together; using either one alone has no effect.
  • When a property decorated with @Trace changes, only components associated with that property are refreshed.
  • For nested classes, a property must be decorated with @Trace, and the nested class itself must be decorated with @ObservedV2 for UI updates to be triggered.

Example:

@ObservedV2
class Person {
  @Trace age: number = 100;
}

@Entry
@ComponentV2
struct Index {
  father: Person = new Person();

  build() {
    Column() {
      Text(`${this.father.son.age}`)
        .onClick(() => {
          this.father.son.age++;
        })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The combination of @ObservedV2 + @Trace supports:

  • Nested classes
  • Inherited classes
  • Arrays of basic/object types
  • Map and Set types

However, it does not support types that require JSON.stringify serialization.


External Initialization

It’s common for child components to require external parameters. In such cases, the @Param decorator is used. It replaces the older @Prop (initialization with copy) and @Link (initialization with synchronization), unifying them into a one-way synchronization from parent to child. It also allows for local initialization. To enforce required parameters, add the @Require decorator.

Example of a custom component with parameter initialization:

@ComponentV2
export struct ActionButton {
  @Param @Require icon: Resource
  @Param @Require clickAction: () => void

  build() {
    Button() {
      SymbolGlyph(this.icon)
        .fontSize(24)
        .fontColor([$r('sys.color.font_primary')])
    }.onClick(() => { this.clickAction(); })
    .backgroundColor($r('sys.color.comp_background_secondary'))
    .padding(8)
    .clickEffect({
      level: ClickEffectLevel.LIGHT
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Usage is the same as in V1.

To support one-time initialization without responding to future changes from the parent, V2 introduces the @Once decorator. Use it with @Param to intercept parent data changes.


Two-Way Synchronization

To achieve synchronization from child to parent or bi-directional syncing, V2 provides two mechanisms:

@ Provider and @Consumer: Cross-Component Hierarchical Synchronization

The new provider mechanism is similar to the original @Provide, but with enhanced capabilities:

  • @Consumer allows local initialization with default values if no matching @Provider is found.
  • @Consumer supports function types.
  • @Provider and @Consumer support overloading—multiple @Providers with the same name are allowed, and @Consumer will look up the nearest @Provider.

@Event Decorator: Standardized Component Output

The @Event decorator enables child components to update parent variables. When the parent variable is also the data source for the child’s @Param, changes will be synced back. Since I personally don’t use this pattern much, I won’t elaborate here.


AppStorageV2

The usage of AppStorageV2 is further simplified compared to V1. It no longer uses @StorageProp and @StorageLink. Instead, it adopts a global singleton pattern:

@Local windowAvoidance: WindowAvoidanceData =
    AppStorageV2.connect(WindowAvoidanceData, SCApp.windowAvoidance, () => new WindowAvoidanceData(0, 0))!
Enter fullscreen mode Exit fullscreen mode

In this example (used to record window avoidance values):

  • The first parameter is the type.
  • The second is the alias.
  • The third is the default initialization function.

Since the return value is nullable but we provided a default initializer, we can safely append the !.

Top comments (0)