鸿蒙HarmonyOS开发:应用的状态管理,LocalStorage,AppStorage,PersistentStorage

299 阅读10分钟

状态共享相关的装饰器(@State、@Prop、@Link、@Provide、@Consume等),但是这些装饰器仅能在两个组件之间共享状态,如果开发者需要在一个页面内的所有组件中共享状态,或者是在多个页面之间共享状态,这些装饰器便不再适用了,此时我们需要的就是应用级状态管理功能。

ArkTS根据不同特性,提供了多种应用状态管理的能力:

  • LocalStorage:页面级UI状态存储,通常用于UIAbility内、页面间的状态共享。

  • AppStorage:特殊的单例LocalStorage对象,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。

  • PersistentStorage:持久化存储UI状态,通常和AppStorage配合使用,选择AppStorage存储的数据写入磁盘,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。

  • Environment:应用程序运行的设备的环境参数,环境参数会同步到AppStorage中,可以和AppStorage搭配使用。

一、页面级UI状态存储:LocalStorage

1、概述

LocalStorage是页面级的UI状态存储,通过@Entry装饰器接收的参数可以在页面内共享同一个LocalStorage实例。LocalStorage支持UIAbility实例内多个页面间状态共享。

ArkTs提供了两个装饰器用于访问LocalStorage,分别是@LocalStorageProp和@LocalStorageLink,前者可以和LocalStorage实现单向同步,后者可以和LocalStorage实现双向同步。

LocalStorage是ArkTS为构建页面级别状态变量提供存储的内存内的“数据库”。

应用程序可以创建多个LocalStorage实例,LocalStorage实例可以在页面内共享,也可以通过GetShared接口,实现跨页面、UIAbility实例内共享。

组件树的根节点,即被@Entry装饰的@Component,可以被分配一个LocalStorage实例,此组件的所有子组件实例将自动获得对该LocalStorage实例的访问权限。

被@Component装饰的组件最多可以访问一个LocalStorage实例和AppStorage,未被@Entry装饰的组件不可被独立分配LocalStorage实例,只能接受父组件通过@Entry传递来的LocalStorage实例。一个LocalStorage实例在组件树上可以被分配给多个组件。

LocalStorage中的所有属性都是可变的。

应用程序决定LocalStorage对象的生命周期。当应用释放最后一个指向LocalStorage的引用时,比如销毁最后一个自定义组件,LocalStorage将被JS Engine垃圾回收。

LocalStorage根据与@Component装饰的组件的同步类型不同,提供了两个装饰器:

  • @LocalStorageProp:@LocalStorageProp装饰的变量与LocalStorage中给定属性建立单向同步关系。

  • @LocalStorageLink:@LocalStorageLink装饰的变量与LocalStorage中给定属性建立双向同步关系。

2、方法

  • constructor 创建一个新的LocalStorage实例。使用Object.keys(initializingProperties)返回的属性和其数值,初始化LocalStorage实例。
let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para);
  • get 获取propName在LocalStorage中对应的属性值。
let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para);
let value: number = storage.get('PropA') as number; 
  • set 在LocalStorage中设置propName对应属性的值。如果newValue的值和propName对应属性的值相同,即不需要做赋值操作,状态变量不会通知UI刷新propName对应属性的值。
let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para);
let res: boolean = storage.set('PropA', 47); // true
let res1: boolean = storage.set('PropB', 47); // false
  • setOrCreate 如果propName已经在LocalStorage中存在,并且newValue和propName对应属性的值不同,则设置propName对应属性的值为newValue,否则状态变量不会通知UI刷新propName对应属性的值。
let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para);
let res: boolean = storage.setOrCreate('PropA', 121); // true
let res1: boolean = storage.setOrCreate('PropB', 111); // true
let res2: boolean = storage.setOrCreate('PropB', null); // true (API12及之后返回true,API11及之前返回false)

3、使用

//父组件

let storage = new LocalStorage({ count: 0 });

@Entry(storage)
@Component
struct Parent {
  //与storage中的count属性双向同步
  @LocalStorageLink('count') count: number = 0;
  
  build(){
    Child()
  }
}

//子组件

@Component
struct Child {

  //与storage中的count属性单向同步
  @LocalStorageProp('count') count: number = 0;

  build(){
    ...
  }
}

二、应用全局的UI状态存储:AppStorage

1、概述

AppStorage用于存储应用级的状态数据,位于AppStorage中的状态数据可以在整个应用的所有组件中共享。

ArkTs提供了两个装饰器用于访问AppStorage实例,分别是@StorageProp和@StorageLink,前者可以和AppStorage实现单向同步,后者可以和AppStorage实现双向同步。

AppStorage是在应用启动的时候会被创建的单例。它的目的是为了提供应用状态数据的中心存储,这些状态数据在应用级别都是可访问的。AppStorage将在应用运行过程保留其属性。属性通过唯一的键字符串值访问。

AppStorage可以和UI组件同步,且可以在应用业务逻辑中被访问。

AppStorage支持应用的主线程内多个UIAbility实例间的状态共享。

AppStorage中的属性可以被双向同步,数据可以是存在于本地或远程设备上,并具有不同的功能,比如数据持久化(详见PersistentStorage)。这些数据是通过业务逻辑中实现,与UI解耦,如果希望这些数据在UI中使用,需要用到@StorageProp和@StorageLink。

2、方法

  • ref 如果给定的propName在AppStorage中存在,则获得AppStorage中propName对应数据的引用。否则,返回undefined。

  • setAndRef 与ref接口类似,如果给定的propName在AppStorage中存在,则获得AppStorage中propName对应数据的引用。如果不存在,则使用defaultValue在AppStorage中创建和初始化propName对应的属性,并返回其引用。

  • get 获取propName在AppStorage中对应的属性值。如果不存在则返回undefined。

  • set 在AppStorage中设置propName对应属性的值。如果newValue的值和propName对应属性的值相同,即不需要做赋值操作,状态变量不会通知UI刷新propName对应属性的值。

  • setOrCreate 如果propName已经在AppStorage中存在,并且newValue和propName对应属性的值不同,则设置propName对应属性的值为newValue,否则状态变量不会通知UI刷新propName对应属性的值。

  • delete 在AppStorage中删除propName对应的属性。

  • clear 删除AppStorage中所有属性。删除所有属性的前提是,AppStorage已经没有任何订阅者。如果有订阅者,clear将不会生效并返回false。如果没有订阅者,则删除成功,并返回true。

3、使用

//PageOne

AppStorage.SetOrCreate('count', 0)

@Entry
@Component
struct PageOne {
  //与AppStorage中的count属性双向同步
  @StorageLink('count') count: number = 0;

  build(){
    ...
  }
}

//PageTwo

@Entry
@Component
struct PageTwo {
  //与AppStorage中的count属性单向同步
  @StorageProp('count') count: number = 0;

  build(){
    ...
  }
}

三、持久化存储UI状态:PersistentStorage

1、概述

前两个小节介绍的LocalStorage和AppStorage都是运行时的内存,但是在应用退出再次启动后,依然能保存选定的结果,是应用开发中十分常见的现象,这就需要用到PersistentStorage。

PersistentStorage是应用程序中的可选单例对象。此对象的作用是持久化存储选定的AppStorage属性,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。

PersistentStorage提供状态变量持久化的能力,但是需要注意,其持久化和读回UI的能力都需要依赖AppStorage。

PersistentStorage将选定的AppStorage属性保留在设备磁盘上。应用程序通过API,以决定哪些AppStorage属性应借助PersistentStorage持久化。UI和业务逻辑不直接访问PersistentStorage中的属性,所有属性访问都是对AppStorage的访问,AppStorage中的更改会自动同步到PersistentStorage。

PersistentStorage和AppStorage中的属性建立双向同步。应用开发通常通过AppStorage访问PersistentStorage,另外还有一些接口可以用于管理持久化属性,但是业务逻辑始终是通过AppStorage获取和设置属性的。

持久化数据是一个相对缓慢的操作,应用程序应避免以下情况:

  • 持久化大型数据集。
  • 持久化经常变化的变量。

PersistentStorage的持久化变量最好是小于2kb的数据,不要大量的数据持久化,因为PersistentStorage写入磁盘的操作是同步的,大量的数据本地化读写会同步在UI线程中执行,影响UI渲染性能。如果开发者需要存储大量的数据,建议使用数据库api。

PersistentStorage和UI实例相关联,持久化操作需要在UI实例初始化成功后才可以被调用,早于该时机调用会导致持久化失败。

2、方法

  • persistProp 将AppStorage中key对应的属性持久化到文件中。该接口的调用通常在访问AppStorage之前。
PersistentStorage.persistProps('highScore',0);
  • deleteProp persistProp的逆向操作。将key对应的属性从PersistentStorage中删除,后续AppStorage的操作,对PersistentStorage不会再有影响。该操作会将对应的key从持久化文件中删除,如果希望再次持久化,可以再次调用persistProp接口。
PersistentStorage.deleteProp('highScore');
  • persistProps 行为和persistProp类似,不同在于可以一次性持久化多个数据,适合在应用启动的时候初始化。
PersistentStorage.persistProps([{ key: 'highScore', defaultValue: '0' }, { key: 'wightScore', defaultValue: '1' }]);
  • keys 返回所有持久化属性的属性名的数组。
let keys: Array<string> = PersistentStorage.keys();

3、使用

从AppStorage中访问PersistentStorage初始化的属性

// 1、初始化PersistentStorage:
PersistentStorage.persistProp('aProp', 47);


// 2、在AppStorage获取对应属性:
AppStorage.get<number>('aProp'); 


// 3、组件内部定义:
@StorageLink('aProp') aProp: number = 48;


完整代码如下:

PersistentStorage.PersistProp('aProp', 0);

@Entry
@Component
struct Index {
  @StorageLink('aProp') aProp: number = 0

  build() {
    Row() {
      Column() {
        // 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果
        Text(`${this.aProp}`)
          .onClick(() => {
            this.aProp += 1;
          })
      }
    }
  }
}

新应用安装后首次启动运行:

  • 调用persistProp初始化PersistentStorage,首先查询在PersistentStorage本地文件中是否存在“aProp”,查询结果为不存在,因为应用是第一次安装。
  • 接着查询属性“aProp”在AppStorage中是否存在,依旧不存在。
  • 在AppStorage中创建名为“aProp”的number类型属性,属性初始值是定义的默认值47。
  • PersistentStorage将属性“aProp”和值47写入磁盘,AppStorage中“aProp”对应的值和其后续的更改将被持久化。
  • 在Index组件中创建状态变量@StorageLink('aProp') aProp,和AppStorage中“aProp”双向绑定,在创建的过程中会在AppStorage中查找,成功找到“aProp”,所以使用其在AppStorage找到的值47。

在这里插入图片描述

触发点击事件后:

  • 状态变量@StorageLink('aProp') aProp改变,触发Text组件重新刷新。
  • @StorageLink装饰的变量是和AppStorage中建立双向同步的,所以@StorageLink('aProp') aProp的变化会被同步回AppStorage中。
  • AppStorage中“aProp”属性的改变会同步到所有绑定该“aProp”的单向或者双向变量,在本示例中没有其他的绑定“aProp”的变量。
  • 因为“aProp”对应的属性已经被持久化,所以在AppStorage中“aProp”的改变会触发PersistentStorage,将新的改变写入本地磁盘。

后续启动应用:

  • 执行PersistentStorage.persistProp('aProp', 47),首先在PersistentStorage本地文件查询“aProp”属性,成功查询到。
  • 将在PersistentStorage查询到的值写入AppStorage中。
  • 在Index组件里,@StorageLink绑定的“aProp”为PersistentStorage写入AppStorage中的值,即为上一次退出应用存入的值。

4、注意

在PersistentStorage之前访问AppStorage中的属性

在调用PersistentStorage.persistProp或者persistProps之前使用接口访问AppStorage中的属性是错误的,因为这样的调用顺序会丢失上一次应用程序运行中的属性值。

注意:下面这样写是错的!!!

注意:下面这样写是错的!!!

注意:下面这样写是错的!!!

let aProp = AppStorage.setOrCreate('aProp', 47);
PersistentStorage.persistProp('aProp', 48);