目录

AppStorage: Storing Application-wide UI State

Before reading this document, it is recommended that you review the State Management Overview to understand the role of AppStorage within the state management framework.

AppStorage is a global UI state storage center bound to the application process. Created by the UI framework at application launch, it stores UI state data in runtime memory to enable application-level global state sharing.

Serving as the application's "central hub," AppStorage acts as the intermediary bridge between persistent data (PersistentStorage), environment variables (Environment), and UI interactions. Its primary value lies in providing you with cross-ability UI state sharing capabilities.

AppStorage provides APIs for manual create, retrieve, update, delete (CRUD) operations outside custom components. For details, see AppStorage API Reference. For best practices, see State Management.

For details about UI decoupling, state management, and status sharing and synchronization between multiple components, see StateStore-based Global State Management.

For data processing that does not involve UI component synchronization, see Persisting User Preferences (ArkTS)

Overview

AppStorage is a singleton created at application startup, serving as the central repository for application-wide UI state data. This state data is accessible at the application level. AppStorage maintains all properties throughout the application lifecycle.

Properties are accessed through unique string keys. They can be synchronized with UI components and accessed within the application's service logic. AppStorage enables UI state sharing among multiple UIAbility instances within the application's main thread.

Properties in AppStorage support two-way synchronization and offer extended features, such as data persistence (see PersistentStorage). These UI states are implemented through service logic and decoupled from the UI. To use these UI states in the UI, the @StorageProp and @StorageLink decorators are required.

@StorageProp

@StorageProp establishes unidirectional data synchronization with the corresponding attribute in AppStorage.

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

[h2]Usage Rules

@StorageProp Decorator Description
Parameters

Constant string, which is mandatory. The string must be enclosed in quotation marks.

NOTE

Using null and undefined as keys will implicitly convert them to their corresponding string representations. This usage is not recommended.

Allowed Decorated Variable Types

Object, class, string, number, boolean, enum types, and arrays of these types.

API version 12 and later supports Map, Set, Date, undefined, and null types, as well as union types of these types. For examples, see Using Union Types in AppStorage.

For nested type scenarios, see Observed Changes and Behavior.

NOTE

The variable type must be specified. It is recommended to use the same type as the corresponding attribute in AppStorage. Otherwise, implicit type conversion may occur, causing abnormal app behavior.

Disallowed variable types Function.
Synchronization type

One-way: from the property in AppStorage to the component variable.

The component variable can be changed locally, but an update from AppStorage will overwrite local changes.

Initial value for the decorated variable Local initialization is mandatory. If the property does not exist in AppStorage, the initial value is used to initialize the property and store it into AppStorage.

[h2]Variable Transfer/Access Rules

Transfer/Access Description
Initialization and update from the parent component Prohibited. Only initialization using the property corresponding to the key in AppStorage is supported. If the corresponding key does not exist, initialization uses the local default value.
Child component initialization Supported. Can be used to initialize variables decorated with @State, @Link, @Prop, or @Provide.
Access from outside the component Not supported.

Figure 1 @StorageProp initialization rule

[h2]Observed Changes and Behavior

Observed Changes

Framework Behavior

  1. When a variable decorated with @StorageProp(key) is modified, the change will not be written back to the corresponding property in AppStorage. The change will trigger a re-render of the custom component and only applies to the private member variable of the current component. Other data bound to the same key will not be synchronized.

  2. When the property corresponding to the given key in AppStorage changes, all variables decorated with @StorageProp(key) will be synchronized and updated, and any local changes will be overwritten.

[h2]Usage Rules

@StorageLink Decorator Description
Parameters

key: constant string, which is mandatory. The string must be enclosed in quotation marks.

NOTE

Using null and undefined as keys will implicitly convert them to their corresponding string representations. This usage is not recommended.

Allowed variable types

Object, class, string, number, Boolean, enum, and array of these types.

API version 12 and later: Map, Set, Date, undefined, null, and union types of these types. For details, see Using Union Types in AppStorage.

For details about the scenarios of nested objects, see Observed Changes and Behavior.

NOTE

The variable type must be specified. Whenever possible, use the same type as that of the corresponding property in AppStorage. Otherwise, implicit type conversion occurs, causing application behavior exceptions.

Disallowed variable types Function.
Synchronization type Two-way: from the property in AppStorage to the custom component variable and vice versa
Initial value for the decorated variable Local initialization is mandatory. If the property does not exist in AppStorage, the initial value is used to initialize the property and store it into AppStorage.

[h2]Variable Transfer/Access Rules

Transfer/Access Description
Initialization and update from the parent component Forbidden.
Child component initialization Supported. The decorated variable can be used to initialize a regular variable or an @State, @Link, @Prop, or @Provide decorated variable in the child component.
Whether external access is supported Not supported.

Figure 2 @StorageLink initialization rule

[h2]Observed Changes and Behavior

Observed Changes

Framework Behavior

  1. Changes to variables decorated with @StorageLink(key) are automatically written back to the corresponding property in AppStorage.

  2. When the value of a key in AppStorage changes, all data bound to that key (including both two-way binding with @StorageLink and one-way binding with @StorageProp) will be synchronized.

  3. The data decorated by @StorageLink(key) is a state variable. Its changes are not only synchronized to AppStorage but also trigger re-rendering of the custom component.

Constraints

  1. The parameter of @StorageProp and @StorageLink must be of the string type. Otherwise, an error is reported during compilation.

    AppStorage.setOrCreate('propA', 47);
    
    // Incorrect usage, which causes a compilation error.
    @StorageProp() storageProp: number = 1;
    @StorageLink() storageLink: number = 2;
    
    // Correct usage.
    @StorageProp('propA') storageProp: number = 1;
    @StorageLink('propA') storageLink: number = 2;
  2. @StorageProp and @StorageLink do not support decorating variables of the Function type. Before API version 23, the app will encounter an error at runtime.

    Since API version 23, relevant validation has been added during app compilation. Decorating a Function-type variable with @StorageProp or @StorageLink will prompt an ERROR. The @StorageProp or @StorageLink decorator should be removed from Function-type variables in the code.

  3. When using AppStorage together with PersistentStorage and Environment, pay attention to the following:

    (1) After a property is created in AppStorage, calling PersistentStorage.persistProp will use the existing value in AppStorage and overwrite the property with the same name in PersistentStorage. Therefore, the reverse calling order is recommended. For a counterexample, see Accessing a Property in AppStorage Before PersistentStorage.

    (2) If a property has already been created in AppStorage, calling Environment.envProp to create a property with the same name will fail. Because AppStorage already has a property with the same name, the environment variable will not be written to AppStorage. Therefore, it is recommended not to use preset environment variable names in AppStorage.

    AppStorage.setOrCreate('languageCode', 'en');
    // The result is false.
    let result = Environment.envProp('languageCode','en');
  4. Changes to variables decorated with state decorators will trigger UI re-rendering. If a variable is modified only for message passing (not for UI updates), using the emitter is recommended. For the example, see Avoiding @StorageLink for Event Notification.

  5. AppStorage is shared within the same process. Since the UIAbility and UIExtensionAbility run in separate processes, the UIExtensionAbility does not share the AppStorage of the main process.

When to Use

[h2]Using AppStorage and LocalStorage in Application Logic

AppStorage is implemented as a singleton, with all its APIs exposed as static methods. How these APIs work resembles the non-static APIs of LocalStorage.

AppStorage.setOrCreate('propA', 47);

let storage: LocalStorage = new LocalStorage();
storage.setOrCreate('propA',17);
let propA: number | undefined = AppStorage.get('propA'); // propA in AppStorage == 47, propA in LocalStorage == 17
let link1: SubscribedAbstractProperty<number> = AppStorage.link('propA'); // link1.get() == 47
let link2: SubscribedAbstractProperty<number> = AppStorage.link('propA'); // link2.get() == 47
let prop: SubscribedAbstractProperty<number> = AppStorage.prop('propA'); // prop.get() == 47

link1.set(48); // Two-way synchronization: link1.get() == link2.get() == prop.get() == 48
prop.set(1); // One-way synchronization: prop.get() == 1; but link1.get() == link2.get() == 48
link1.set(49); // Two-way synchronization: link1.get() == link2.get() == prop.get() == 49

storage.get<number>('propA') // == 17
storage.set('propA', 101);
storage.get<number>('propA') // == 101

AppStorage.get<number>('propA') // == 49
link1.get() // == 49
link2.get() // == 49
prop.get() // == 49

[h2]Using AppStorage from Inside the UI

@StorageLink is used in conjunction with AppStorage to establish two-way data synchronization through properties in AppStorage.

@StorageProp works in conjunction with AppStorage to establish one-way data synchronization using properties stored in AppStorage.

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

const DOMAIN = 0x0001;
const TAG: string = '[SampleAppStorage]';

class Data {
  public code: number;

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

AppStorage.setOrCreate('propA', 47);
AppStorage.setOrCreate('propB', new Data(50));
let storage = new LocalStorage();
storage.setOrCreate('linkA', 48);
storage.setOrCreate('linkB', new Data(100));

@Entry(storage)
@Component
struct TestStorageProp {
  @StorageLink('propA') storageLink: number = 1;
  @StorageProp('propA') storageProp: number = 1;
  @StorageLink('propB') storageLinkObject: Data = new Data(1);
  @StorageProp('propB') storagePropObject: Data = new Data(1);

  build() {
    Column({ space: 20 }) {
      // StorageLink establishes a two-way synchronization with AppStorage; local changes will be synchronized back to the value of key 'propA' in AppStorage.
      Text(`storageLink ${this.storageLink}`)
        .fontSize(20)
        .margin(10)
        .onClick(() => {
          this.storageLink += 1;
        })

      // @StorageProp establishes a one-way synchronization with AppStorage; local changes will not be synchronized back to the value of key 'propA' in AppStorage.
      // However, the value can be updated via AppStorage's set/setOrCreate.
      Text(`storageProp ${this.storageProp}`)
        .fontSize(20)
        .margin(10)
        .onClick(() => {
          this.storageProp += 1;
        })

      // Although AppStorage APIs can obtain values, they do not have the capability to refresh the UI (the value change can be seen in logs).
      // A connection with the custom component can only be established to refresh the UI by relying on @StorageLink/@StorageProp.
      Text(`change by AppStorage: ${AppStorage.get<number>('propA')}`)
        .fontSize(20)
        .margin(10)
        .onClick(() => {
          hilog.info(DOMAIN, TAG, `Appstorage.get: ${AppStorage.get<number>('propA')}`);
          AppStorage.set<number>('propA', 100);
        })

      Text(`storageLinkObject ${this.storageLinkObject.code}`)
        .fontSize(20)
        .margin(10)
        .onClick(() => {
          this.storageLinkObject.code += 1;
        })

      Text(`storagePropObject ${this.storagePropObject.code}`)
        .fontSize(20)
        .margin(10)
        .onClick(() => {
          this.storagePropObject.code += 1;
        })
    }
    .width('100%')
  }
}

[h2]Using Union Types in AppStorage

In the following example, the type of variable linkA is number | null, and the type of variable linkB is number | undefined. The Text component initially displays null and undefined respectively. After a tap, they switch to numbers, and after another tap, they switch back to null and undefined.

@Component
struct StorageLinkComponent {
  @StorageLink('linkA') linkA: number | null = null;
  @StorageLink('linkB') linkB: number | undefined = undefined;

  build() {
    Column() {
      Text('@StorageLink initialization, @StorageLink value')
        .fontSize(20)
        .margin(10)
      // When linkA is null, tapping switches it to 1; when linkA is 1, tapping switches it to null.
      Text(`${this.linkA}`)
        .fontSize(20)
        .margin(10)
        .onClick(() => {
          this.linkA ? this.linkA = null : this.linkA = 1;
        })
      Text(`${this.linkB}`)
        .fontSize(20)
        .margin(10)
        .onClick(() => {
          this.linkB ? this.linkB = undefined : this.linkB = 1;
        })
    }
    .borderWidth(3).borderColor(Color.Red)
    .width('100%')
  }
}

@Component
struct StoragePropComponent {
  @StorageProp('propA') propA: number | null = null;
  @StorageProp('propB') propB: number | undefined = undefined;

  build() {
    Column() {
      Text('@StorageProp initialization, @StorageProp value')
        .fontSize(20)
        .margin(10)
      Text(`${this.propA}`)
        .fontSize(20)
        .margin(10)
        .onClick(() => {
          this.propA ? this.propA = null : this.propA = 1;
        })
      Text(`${this.propB}`)
        .fontSize(20)
        .margin(10)
        .onClick(() => {
          this.propB ? this.propB = undefined : this.propB = 1;
        })
    }
    .borderWidth(3).borderColor(Color.Blue)
    .width('100%')
  }
}

@Entry
@Component
struct TestPageStorageLink {
  build() {
    Row() {
      Column() {
        StorageLinkComponent()
        StoragePropComponent()
      }
      .width('100%')
    }
    .height('100%')
  }
}

[h2]Decorating Variables of the Array Type

In the following example, the type of message decorated by @StorageLink is number[]. After the button is clicked, the value of message changes, and the view refreshes accordingly.

@Entry
@Component
struct ArraySample {
  @StorageLink('array') message: number[] = [0, 1, 2, 3];

  build() {
    Column() {
      ForEach(this.message, (item: number) => {
        Text(`${item}`)
          .fontSize(20)
          .margin(10)
      })
      // Push an array element to trigger a UI refresh.
      Button('Push element')
        .width(300)
        .margin(10)
        .onClick(() => {
          this.message.push(4);
        })
      // Pop an array element to trigger a UI refresh.
      Button('Pop element')
        .width(300)
        .margin(10)
        .onClick(() => {
          this.message.pop();
        })
      // Reassign the entire array to trigger a UI refresh.
      Button('Reset array')
        .width(300)
        .margin(10)
        .onClick(() => {
          this.message = [9, 8, 7, 6];
        })
      // Modify element[0] to trigger a UI refresh.
      Button('Modify element[0]')
        .width(300)
        .margin(10)
        .onClick(() => {
          this.message[0] = 10;
        })
    }
    .width('100%')
  }
}

[h2]Decorating Variables of the Date Type

AppStorage supports the Date type since API version 12.

In the following example, the type of selectedDate decorated by @StorageLink is Date. After the button is clicked, the value of selectedDate changes, and the UI is re-rendered.

@Entry
@Component
struct DateSample {
  @StorageLink('date') selectedDate: Date = new Date('2021-08-08');

  build() {
    Column() {
      Button('set selectedDate to 2023-07-08')
        .width(300)
        .margin(10)
        .onClick(() => {
          AppStorage.setOrCreate('date', new Date('2023-07-08'));
        })
      // Tap the button to update the year data in selectedDate and trigger a view refresh.
      Button('increase the year by 1')
        .width(300)
        .margin(10)
        .onClick(() => {
          this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1);
        })
      Button('increase the month by 1')
        .width(300)
        .margin(10)
        .onClick(() => {
          this.selectedDate.setMonth(this.selectedDate.getMonth() + 1);
        })
      Button('increase the day by 1')
        .width(300)
        .margin(10)
        .onClick(() => {
          this.selectedDate.setDate(this.selectedDate.getDate() + 1);
        })
      DatePicker({
        start: new Date('1970-1-1'),
        end: new Date('2100-1-1'),
        selected: $$this.selectedDate
      })
    }.width('100%')
  }
}

[h2]Decorating Variables of the Map Type

AppStorage supports the Map type since API version 12.

In this example, the message variable decorated with @StorageLink is of the Map<number, string> type. After the button is clicked, the value of message changes, and the UI is re-rendered.

@Entry
@Component
struct MapSample {
  @StorageLink('map') message: Map<number, string> = new Map([[0, 'a'], [1, 'b'], [3, 'c']]);

  build() {
    Row() {
      Column() {
        ForEach(Array.from(this.message.entries()), (item: [number, string]) => {
          Text(`${item[0]}`)
            .fontSize(30)
            .margin(10)
          Text(`${item[1]}`)
            .fontSize(30)
            .margin(10)
          Divider()
        })
        // Tap the button to initialize the message.
        Button('init map')
          .width(300)
          .margin(10)
          .onClick(() => {
            this.message = new Map([[0, 'a'], [1, 'b'], [3, 'c']]);
          })
        Button('set new one')
          .width(300)
          .margin(10)
          .onClick(() => {
            this.message.set(4, 'd');
          })
        Button('clear')
          .width(300)
          .margin(10)
          .onClick(() => {
            this.message.clear();
          })
        Button('replace the existing one')
          .width(300)
          .margin(10)
          .onClick(() => {
            this.message.set(0, 'aa');
          })
        Button('delete the existing one')
          .width(300)
          .margin(10)
          .onClick(() => {
            AppStorage.get<Map<number, string>>('map')?.delete(0);
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

[h2]Decorating Variables of the Set Type

AppStorage supports the Set type since API version 12.

In this example, the memberSet variable decorated with @StorageLink is of the Set<number> type. After the button is clicked, the value of memberSet changes, and the UI is re-rendered.

@Entry
@Component
struct SetSample {
  @StorageLink('set') memberSet: Set<number> = new Set([0, 1, 2, 3, 4]);

  build() {
    Row() {
      Column() {
        ForEach(Array.from(this.memberSet.entries()), (item: [number, number]) => {
          Text(`${item[0]}`)
            .fontSize(30)
            .margin(10)
          Divider()
        })
        // Tap the button to initialize memberSet.
        Button('init set')
          .width(300)
          .margin(10)
          .onClick(() => {
            this.memberSet = new Set([0, 1, 2, 3, 4]);
          })
        Button('set new one')
          .width(300)
          .margin(10)
          .onClick(() => {
            AppStorage.get<Set<number>>('set')?.add(5);
          })
        Button('clear')
          .width(300)
          .margin(10)
          .onClick(() => {
            this.memberSet.clear();
          })
        Button('delete the first one')
          .width(300)
          .margin(10)
          .onClick(() => {
            this.memberSet.delete(0);
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

[h2]Sharing the AppStorage Across Multiple Pages

In the following example, the Index and Page pages share the linkA data through the same global AppStorage object. If the value of the linkA is modified in one page, the updated value can be obtained in the other page.

AppStorage.setOrCreate('linkA', 47)
AppStorage.setOrCreate('propB', 48)

@Entry
@Component
struct Index {
  @StorageLink('linkA') linkA: number = 1; // Bidirectional data synchronization with AppStorage
  @StorageProp('propB') propB: number = 1; // Unidirectional data synchronization with AppStorage
  pageStack: NavPathStack = new NavPathStack();

  build() {
    Navigation(this.pageStack) {
      Row() {
        Column({ space: 5 }) {
          Text(`${this.linkA}`)
            .fontSize(50)
            .fontWeight(FontWeight.Bold)
            .margin(10)
          Text(`${this.propB}`)
            .fontSize(50)
            .fontWeight(FontWeight.Bold)
            .margin(10)
          Button('Change linkA')
            .width(300)
            .margin(10)
            .onClick(() => {
              // Refresh the UI. The modification will be synchronized back to AppStorage.
              this.linkA++;
            })
          Button('Change propB')
            .width(300)
            .margin(10)
            .onClick(() => {
              // Refresh the UI. The modification will not be synchronized back to AppStorage.
              this.propB++;
            })
          Button('To Page')
            .width(300)
            .margin(10)
            .onClick(() => {
              this.pageStack.pushPathByName('Page', null);
            })
        }
        .width('100%')
      }
      .height('100%')
    }
  }
}
@Builder
export function PageBuilder() {
  Page()
}

// Share an AppStorage object globally.
@Component
struct Page {
  @StorageLink('linkA') linkA: number = 2; // Bidirectional data synchronization with AppStorage
  @StorageProp('propB') propB: number = 2; // Unidirectional data synchronization with AppStorage
  pageStack: NavPathStack = new NavPathStack();

  build() {
    NavDestination() {
      Row() {
        Column({ space: 5 }) {
          Text(`${this.linkA}`)
            .fontSize(50)
            .fontWeight(FontWeight.Bold)
            .margin(10)
          Text(`${this.propB}`)
            .fontSize(50)
            .fontWeight(FontWeight.Bold)
            .margin(10)
          Button('Change linkA')
            .width(300)
            .margin(10)
            .onClick(() => {
              // Refresh the UI. The modification will be synchronized back to AppStorage.
              this.linkA++;
            })
          Button('Change propB')
            .width(300)
            .margin(10)
            .onClick(() => {
              // Refresh the UI. The modification will not be synchronized back to AppStorage.
              this.propB++;
            })
          Button('Back Index')
            .width(300)
            .margin(10)
            .onClick(() => {
              this.pageStack.pop();
            })
        }
        .width('100%')
      }
    }
    .onReady((context: NavDestinationContext) => {
      this.pageStack = context.pathStack;
    })
  }
}

When using Navigation, you need to manually add the system route table file src/main/resources/base/profile/router_map.json and add "routerMap": "$profile:router_map" to module.json5.

{
  "routerMap": [
    {
      "name": "Page",
      "pageSourceFile": "src/main/ets/pages/Page.ets",
      "buildFunction": "PageBuilder",
      "data": {
        "description": "AppStorage example"
      }
    }
  ]
}

AppStorage Usage Recommendations

[h2]Notes on Update Rules When @StorageProp Is Used with AppStorage APIs

When updating a key's value using the setOrCreate/set API, if the value is the same, setOrCreate will not notify @StorageLink/@StorageProp to update. However, because @StorageProp itself has a data copy, changing the value will not synchronize it to AppStorage. This may lead you to mistakenly believe that the value has been changed through AppStorage, when in fact @StorageProp has not been notified to update the value. An example is shown below.

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

const DOMAIN = 0x0001;
const TAG: string = '[SampleAppStorage]';
AppStorage.setOrCreate('propA', false);

@Entry
@Component
struct PageStorageProp {
  @StorageProp('propA') @Watch('onChange') propA: boolean = false;

  onChange() {
    hilog.info(DOMAIN, TAG, `propA change`);
  }

  aboutToAppear(): void {
    this.propA = true;
  }

  build() {
    Column() {
      Text(`${this.propA}`)
        .fontSize(20)
        .margin(10)
      Button('change')
        .width(300)
        .margin(10)
        .onClick(() => {
          AppStorage.setOrCreate('propA', false);
          // Output the current value of this.propA.
          hilog.info(DOMAIN, TAG, `propA: ${this.propA}`);
        })
    }
    .width('100%')
  }
}

In the preceding example, the value of propA has been changed to true locally before the click event, while the value stored in AppStorage remains false. When the click event attempts to update the value of propA to false through the setOrCreate API, no synchronization is triggered because the value in AppStorage is already false (matching the new value). As a result, the value of the @StorageProp decorated variable remains true.

To achieve synchronization, there are two approaches:

  1. Change @StorageProp to @StorageLink.

  2. Use AppStorage.setOrCreate('propA', true) to change the local value.