AppStorage: Storing Application-wide UI State

Before reading this document, it is recommend 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.

NOTE

For details about UI decoupling, status 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 one-way data synchronization with the corresponding property in AppStorage.

NOTE

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

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 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 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, it will be created and initialized with this value.

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

storageprop-initialization

Observed Changes and Behavior

Observed Changes

  • When the decorated variable is of the Boolean, string, or number type, its value change can be observed.

  • When the decorated variable is of the class or object type, the complete object reassignment and property-level changes can be observed. For details, see Using AppStorage from Inside the UI.

  • When the decorated object is an array, you can observe the changes of adding, deleting, and updating array units.

  • When the decorated object is of the Date type, the following changes can be observed: (1) complete Date object reassignment; (2) property changes caused by calling setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, or setUTCMilliseconds. For details, see Decorating Variables of the Date Type.

  • When the decorated object is of the Map type, the following changes can be observed: (1) complete Map object reassignment; (2) changes caused by calling set, clear, or delete. For details, see Decorating Variables of the Map Type.

  • When the decorated object is of the Set type, the following changes can be observed: (1) complete Set object reassignment; (2) changes caused by calling add, clear, or delete. For details, see Decorating Variables of the Set Type.

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.

@StorageLink establishes two-way data synchronization with the corresponding property in AppStorage.

NOTE

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

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, it will be created and initialized with this value.

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

storagelink-initialization

Observed Changes and Behavior

Observed Changes

  • When the decorated variable is of the Boolean, string, or number type, its value change can be observed.

  • When the decorated variable is of the class or object type, the complete object reassignment and property-level changes can be observed. For details, see Using AppStorage from Inside the UI.

  • When the decorated object is an array, you can observe the changes of adding, deleting, and updating array units.

  • When the decorated object is of the Date type, the following changes can be observed: (1) complete Date object reassignment; (2) property changes caused by calling setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, or setUTCMilliseconds. For details, see Decorating Variables of the Date Type.

  • When the decorated object is of the Map type, the following changes can be observed: (1) complete Map object reassignment; (2) changes caused by calling set, clear, or delete. For details, see Decorating Variables of the Map Type.

  • When the decorated object is of the Set type, the following changes can be observed: (1) complete Set object reassignment; (2) changes caused by calling add, clear, or delete. For details, see Decorating Variables of the Set Type.

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. When a variable decorated by @StorageLink(key) is updated, the change is synchronized back to the corresponding key in AppStorage and triggers re-rendering of the owning 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. An error is reported during compilation.
    @StorageProp() storageProp: number = 1;
    @StorageLink() storageLink: number = 2;
    
    // Correct usage.
    @StorageProp('propA') storageProp: number = 1;
    @StorageLink('propA') storageLink: number = 2;
    
  2. @StorageProp and @StorageLink cannot decorate variables of the function type. Before API version 23, the framework throws a runtime error. Since API version 23, validation of @StorageProp and @StorageLink for function-type variables is added, and a compilation error will be reported.

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

    a. If a property exists in AppStorage before PersistentStorage.persistProp is called, the value in AppStorage will overwrite the value in PersistentStorage. As such, when possible, initialize PersistentStorage properties first. For an example with incorrect usage, see Accessing a Property in AppStorage Before PersistentStorage.

    b. If a property has already been created in AppStorage, subsequent calls to Environment.envProp to create a property with the same name will fail. Since AppStorage already contains a property with that name, the Environment variable will not be written to AppStorage. Therefore, avoid using the 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

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

Using AppStorage from Inside the UI

@StorageLink works in conjunction with AppStorage to establish two-way data synchronization using properties stored 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}`)
        .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, its value can be updated using the set/setOrCreate APIs of AppStorage.
      Text(`storageProp ${this.storageProp}`)
        .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')}`)
        .onClick(() => {
          hilog.info(DOMAIN, TAG, `Appstorage.get: ${AppStorage.get<number>('propA')}`);
          AppStorage.set<number>('propA', 100);
        })

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

      Text(`storagePropObject ${this.storagePropObject.code}`)
        .onClick(() => {
          this.storagePropObject.code += 1;
        })
    }
  }
}

Using Union Types in AppStorage

The following example demonstrates how to use union types. The type of variable linkA is number | null, and the type of variable linkB is number | undefined. The Text components display null and undefined upon initialization, numbers when clicked, and null and undefined when clicked again.

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

  build() {
    Column() {
      Text('@StorageLink initialization, @StorageLink value')
      Text(`${this.linkA}`).fontSize(20).onClick(() => {
        this.linkA ? this.linkA = null : this.linkA = 1;
      })
      Text(`${this.linkB}`).fontSize(20).onClick(() => {
        this.linkB ? this.linkB = undefined : this.linkB = 1;
      })
    }
    .borderWidth(3).borderColor(Color.Red)
  }
}

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

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

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

Decorating Variables of the Date Type

NOTE

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')
        .margin(10)
        .onClick(() => {
          AppStorage.setOrCreate('date', new Date('2023-07-08'));
        })
      Button('increase the year by 1')
        .margin(10)
        .onClick(() => {
          this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1);
        })
      Button('increase the month by 1')
        .margin(10)
        .onClick(() => {
          this.selectedDate.setMonth(this.selectedDate.getMonth() + 1);
        })
      Button('increase the day by 1')
        .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%')
  }
}

Decorating Variables of the Map Type

NOTE

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)
          Text(`${item[1]}`).fontSize(30)
          Divider()
        })
        Button('init map').onClick(() => {
          this.message = new Map([[0, 'a'], [1, 'b'], [3, 'c']]);
        })
        Button('set new one').onClick(() => {
          this.message.set(4, 'd');
        })
        Button('clear').onClick(() => {
          this.message.clear();
        })
        Button('replace the existing one').onClick(() => {
          this.message.set(0, 'aa');
        })
        Button('delete the existing one').onClick(() => {
          AppStorage.get<Map<number, string>>('map')?.delete(0);
        })
      }
      .width('100%')
    }
    .height('100%')
  }
}

Decorating Variables of the Set Type

NOTE

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)
          Divider()
        })
        Button('init set')
          .onClick(() => {
            this.memberSet = new Set([0, 1, 2, 3, 4]);
          })
        Button('set new one')
          .onClick(() => {
            AppStorage.get<Set<number>>('set')?.add(5);
          })
        Button('clear')
          .onClick(() => {
            this.memberSet.clear();
          })
        Button('delete the first one')
          .onClick(() => {
            this.memberSet.delete(0);
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

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)
          Text(`${this.propB}`)
            .fontSize(50)
            .fontWeight(FontWeight.Bold)
          Button('Change linkA')
            .onClick(() => {
              // Refresh the UI. The modification will be synchronized back to AppStorage.
              this.linkA++;
            })
          Button('Change propB')
            .onClick(() => {
              // Refresh the UI. The modification will not be synchronized back to AppStorage.
              this.propB++;
            })
          Button('To Page')
            .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)
          Text(`${this.propB}`)
            .fontSize(50)
            .fontWeight(FontWeight.Bold)
          Button('Change linkA')
            .onClick(() => {
              // Refresh the UI. The modification will be synchronized back to AppStorage.
              this.linkA++;
            })
          Button('Change propB')
            .onClick(() => {
              // Refresh the UI. The modification will not be synchronized back to AppStorage.
              this.propB++;
            })
          Button('Back Index')
            .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

For performance considerations, using the two-way synchronization mechanism between @StorageLink and AppStorage for event notification is not recommended: (1) Variables in AppStorage may be bound to components across multiple pages, while event notifications typically do not need to be broadcast to all these components. (2) When @StorageLink decorated variables are used in UI, changes trigger component re-renders, causing performance overhead even when no visual updates are required.

In the following example, any click event in the TapImage component will trigger a change of the tapIndex property. As @StorageLink establishes a two-way data synchronization with AppStorage, the local change is synchronized to AppStorage. As a result, all custom components bound to the tapIndex property in AppStorage are notified of the change. After @Watch observes the change to tapIndex, the state variable tapColor is updated, triggering UI re-rendering. (Because tapIndex is not directly bound to the UI, its change does not directly trigger UI re-rendering.)

When using this mechanism for event notification, make sure the property in AppStorage is not directly bound to the UI and control the complexity of the @Watch function– If the @Watch function execution time is too long, it will impact UI re-rendering efficiency.

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

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

class ViewData {
  public title: string;
  public uri: Resource;
  public color: Color = Color.Black;

  constructor(title: string, uri: Resource) {
    this.title = title;
    this.uri = uri;
  }
}

@Entry
@Component
struct Gallery {
  // Replace $r('app.media.startIcon') with the actual resource file.
  dataList: Array<ViewData> =
    [new ViewData('flower', $r('app.media.startIcon')), new ViewData('OMG', $r('app.media.startIcon')),
      new ViewData('OMG', $r('app.media.startIcon'))];
  scroller: Scroller = new Scroller();

  build() {
    Column() {
      Grid(this.scroller) {
        ForEach(this.dataList, (item: ViewData, index?: number) => {
          GridItem() {
            TapImage({
              uri: item.uri,
              index: index
            })
          }.aspectRatio(1)

        }, (item: ViewData, index?: number) => {
          return JSON.stringify(item) + index;
        })
      }.columnsTemplate('1fr 1fr')
    }

  }
}

@Component
export struct TapImage {
  @StorageLink('tapIndex') @Watch('onTapIndexChange') tapIndex: number = -1;
  @State tapColor: Color = Color.Black;
  private index: number = 0;
  private uri: Resource = {
    id: 0,
    type: 0,
    moduleName: '',
    bundleName: ''
  };

  // Check whether the component is selected.
  onTapIndexChange() {
    if (this.tapIndex >= 0 && this.index === this.tapIndex) {
      hilog.info(DOMAIN, TAG, `tapindex: ${this.tapIndex}, index: ${this.index}, red`);
      this.tapColor = Color.Red;
    } else {
      hilog.info(DOMAIN, TAG, `tapindex: ${this.tapIndex}, index: ${this.index}, black`);
      this.tapColor = Color.Black;
    }
  }

  build() {
    Column() {
      Image(this.uri)
        .objectFit(ImageFit.Cover)
        .onClick(() => {
          this.tapIndex = this.index;
        })
        .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
    }

  }
}

Compared with the use of @StorageLink, the use of emit allows you to subscribe to events and receive event callbacks, thereby reducing overhead and improving code readability.

NOTE

The emit API is not available in DevEco Studio Previewer.

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

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

let nextId: number = 0;

class ViewData {
  public title: string;
  public uri: Resource;
  public color: Color = Color.Black;
  public id: number;

  constructor(title: string, uri: Resource) {
    this.title = title;
    this.uri = uri;
    this.id = nextId++;
  }
}

@Entry
@Component
struct Gallery {
  // Replace $r('app.media.startIcon') with the actual resource file.
  dataList: Array<ViewData> =
    [new ViewData('flower', $r('app.media.startIcon')), new ViewData('OMG', $r('app.media.startIcon')),
      new ViewData('OMG', $r('app.media.startIcon'))];
  scroller: Scroller = new Scroller();
  private preIndex: number = -1;

  build() {
    Column() {
      Grid(this.scroller) {
        ForEach(this.dataList, (item: ViewData) => {
          GridItem() {
            TapImage({
              uri: item.uri,
              index: item.id
            })
          }.aspectRatio(1)
          .onClick(() => {
            if (this.preIndex === item.id) {
              return;
            }
            let innerEvent: emitter.InnerEvent = { eventId: item.id };
            // Selected: from black to red
            let eventData: emitter.EventData = {
              data: {
                'colorTag': 1
              }
            };
            emitter.emit(innerEvent, eventData);

            if (this.preIndex != -1) {
              hilog.info(DOMAIN, TAG, `preIndex: ${this.preIndex}, index: ${item.id}, black`);
              let innerEvent: emitter.InnerEvent = { eventId: this.preIndex };
              // Deselected: from red to black
              let eventData: emitter.EventData = {
                data: {
                  'colorTag': 0
                }
              };
              emitter.emit(innerEvent, eventData);
            }
            this.preIndex = item.id;
          })
        }, (item: ViewData) => JSON.stringify(item))
      }.columnsTemplate('1fr 1fr')
    }

  }
}

@Component
export struct TapImage {
  @State tapColor: Color = Color.Black;
  private index: number = 0;
  private uri: Resource = {
    id: 0,
    type: 0,
    moduleName: '',
    bundleName: ''
  };

  onTapIndexChange(colorTag: emitter.EventData) {
    if (colorTag.data != null) {
      this.tapColor = colorTag.data.colorTag ? Color.Red : Color.Black;
    }
  }

  aboutToAppear() {
    // Define the event ID.
    let innerEvent: emitter.InnerEvent = { eventId: this.index };
    emitter.on(innerEvent, data => {
      this.onTapIndexChange(data);
    });
  }

  build() {
    Column() {
      Image(this.uri)
        .objectFit(ImageFit.Cover)
        .border({ width: 5, style: BorderStyle.Dotted, color: this.tapColor })
    }
  }
}

The preceding notification logic is simple. It can be simplified into a ternary expression as follows:


class ViewData {
  public title: string;
  public uri: Resource;
  public color: Color = Color.Black;

  constructor(title: string, uri: Resource) {
    this.title = title;
    this.uri = uri;
  }
}

@Entry
@Component
struct Gallery {
  // Replace $r('app.media.startIcon') with the actual resource file.
  dataList: Array<ViewData> =
    [new ViewData('flower', $r('app.media.startIcon')), new ViewData('OMG', $r('app.media.startIcon')),
      new ViewData('OMG', $r('app.media.startIcon'))];
  scroller: Scroller = new Scroller();

  build() {
    Column() {
      Grid(this.scroller) {
        ForEach(this.dataList, (item: ViewData, index?: number) => {
          GridItem() {
            TapImage({
              uri: item.uri,
              index: index
            })
          }.aspectRatio(1)

        }, (item: ViewData, index?: number) => {
          return JSON.stringify(item) + index;
        })
      }.columnsTemplate('1fr 1fr')
    }

  }
}

@Component
export struct TapImage {
  @StorageLink('tapIndex') tapIndex: number = -1;
  private index: number = 0;
  private uri: Resource = {
    id: 0,
    type: 0,
    moduleName: '',
    bundleName: ''
  };

  build() {
    Column() {
      Image(this.uri)
        .objectFit(ImageFit.Cover)
        .onClick(() => {
          this.tapIndex = this.index;
        })
        .border({
          width: 5,
          style: BorderStyle.Dotted,
          color: (this.tapIndex >= 0 && this.index === this.tapIndex) ? Color.Red : Color.Black
        })
    }
  }
}

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

When a key's value is updated using the setOrCreate or** set** API, if the new value is identical to the existing one, setOrCreate will not trigger updates for @StorageLink or @StorageProp. In addition, since @StorageProp keeps its own local data copy, modifying this local value directly will not synchronize the change back to AppStorage. This can lead to a common misunderstanding: You may assume you have updated the value via AppStorage, only to find that the @StorageProp value remains unchanged in practice. An example is as follows:

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}`)
      Button('change')
        .onClick(() => {
          AppStorage.setOrCreate('propA', false);
          hilog.info(DOMAIN, TAG, `propA: ${this.propA}`);
        })
    }
  }
}

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.