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

编程入门 行业动态 更新时间:2024-10-24 10:25:21

OpenHarmony 应用<a href=https://www.elefans.com/category/jswz/34/1765343.html style=全局的 UI 状态存储:AppStorage"/>

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

AppStorage 是应用全局的 UI 状态存储,是和应用的进程绑定的,由 UI 框架在应用程序启动时创建,为应用程序 UI 状态属性提供中央存储。

和 AppStorage 不同的是,LocalStorage 是页面级的,通常应用于页面内的数据共享。而 AppStorage 是应用级的全局状态共享,还相当于整个应用的“中枢”,持久化数据PersistentStorage和环境变量Environment都是通过 AppStorage 中转,才可以和 UI 交互。

本文仅介绍 AppStorage 使用场景和相关的装饰器:@StorageProp 和 @StorageLink。

概述

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

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

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

@StorageProp

在上文中已经提到,如果要建立 AppStorage 和自定义组件的联系,需要使用 @StorageProp 和 @StorageLink 装饰器。使用 @StorageProp(key)/@StorageLink(key)装饰组件内的变量,key 标识了 AppStorage 的属性。

当自定义组件初始化的时候,会使用 AppStorage 中对应 key 的属性值将 @StorageProp(key)/@StorageLink(key)装饰的变量初始化。由于应用逻辑的差异,无法确认是否在组件初始化之前向 AppStorage 实例中存入了对应的属性,所以 AppStorage 不一定存在 key 对应的属性,因此 @StorageProp(key)/@StorageLink(key)装饰的变量进行本地初始化是必要的。

@StorageProp(key)是和 AppStorage 中 key 对应的属性建立单向数据同步,允许本地改变,但是对于 @StorageProp,本地的修改永远不会同步回 AppStorage 中,相反,如果 AppStorage 给定 key 的属性发生改变,改变会被同步给 @StorageProp,并覆盖掉本地的修改。

装饰器使用规则说明

变量的传递/访问规则说明

图 1 @StorageProp 初始化规则图示  

观察变化和行为表现

观察变化

● 当装饰的数据类型为 boolean、string、number 类型时,可以观察到数值的变化。

● 当装饰的数据类型为 class 或者 Object 时,可以观察到赋值和属性赋值的变化,即 Object.keys(observedObject)返回的所有属性。

● 当装饰的对象是 array 时,可以观察到数组添加、删除、更新数组单元的变化。

框架行为

● 当 @StorageProp(key)装饰的数值改变被观察到时,修改不会被同步回 AppStorage 对应属性键值 key 的属性中。

● 当前 @StorageProp(key)单向绑定的数据会被修改,即仅限于当前组件的私有成员变量改变,其他的绑定该 key 的数据不会同步改变。

● 当 @StorageProp(key)装饰的数据本身是状态变量,它的改变虽然不会同步回 AppStorage 中,但是会引起所属的自定义组件的重新渲染。

● 当 AppStorage 中 key 对应的属性发生改变时,会同步给所有 @StorageProp(key)装饰的数据,@StorageProp(key)本地的修改将被覆盖。

@StorageLink

@StorageLink(key)是和 AppStorage 中 key 对应的属性建立双向数据同步:

1.  本地修改发生,该修改会被回 AppStorage 中;

2.  AppStorage 中的修改发生后,该修改会被同步到所有绑定 AppStorage 对应 key 的属性上,包括单向(@StorageProp 和通过 Prop 创建的单向绑定变量)、双向(@StorageLink 和通过 Link 创建的双向绑定变量)变量和其他实例(比如 PersistentStorage)。

装饰器使用规则说明

变量的传递/访问规则说明

图 2 @StorageLink 初始化规则图示  

观察变化和行为表现

观察变化

● 当装饰的数据类型为 boolean、string、number 类型时,可以观察到数值的变化。

● 当装饰的数据类型为 class 或者 Object 时,可以观察到赋值和属性赋值的变化,即 Object.keys(observedObject)返回的所有属性。

● 当装饰的对象是 array 时,可以观察到数组添加、删除、更新数组单元的变化。

框架行为

1.  当 @StorageLink(key)装饰的数值改变被观察到时,修改将被同步回 AppStorage 对应属性键值 key 的属性中。

2.  AppStorage 中属性键值 key 对应的数据一旦改变,属性键值 key 绑定的所有的数据(包括双向 @StorageLink 和单向 @StorageProp)都将同步修改。

3.  当 @StorageLink(key)装饰的数据本身是状态变量,它的改变不仅仅会同步回 AppStorage 中,还会引起所属的自定义组件的重新渲染。

使用场景

从应用逻辑使用 AppStorage 和 LocalStorage

AppStorage 是单例,它的所有 API 都是静态的,使用方法类似于中 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() == 47link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48
link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49storage.get<number>('PropA') // == 17
storage.set('PropA', 101);
storage.get<number>('PropA') // == 101AppStorage.get<number>('PropA') // == 49
link1.get() // == 49
link2.get() // == 49
prop.get() // == 49

从 UI 内部使用 AppStorage 和 LocalStorage

@StorageLink 变量装饰器与 AppStorage 配合使用,正如 @LocalStorageLink 与 LocalStorage 配合使用一样。此装饰器使用 AppStorage 中的属性创建双向数据同步。

AppStorage.setOrCreate('PropA', 47);
let storage = new LocalStorage();
storage.setOrCreate('PropA',48);@Entry(storage)
@Component
struct CompA {@StorageLink('PropA') storLink: number = 1;@LocalStorageLink('PropA') localStorLink: number = 1;build() {Column({ space: 20 }) {Text(`From AppStorage ${this.storLink}`).onClick(() => this.storLink += 1)Text(`From LocalStorage ${this.localStorLink}`).onClick(() => this.localStorLink += 1)}}
}

不建议借助 @StorageLink 的双向同步机制实现事件通知

不建议开发者使用 @StorageLink 和 AppStorage 的双向同步的机制来实现事件通知,AppStorage 是和 UI 相关的数据存储,改变会带来 UI 的刷新,相对于一般的事件通知,UI 刷新的成本较大。

TapImage 中的点击事件,会触发 AppStorage 中 tapIndex 对应属性的改变。因为 @StorageLink 是双向同步,修改会同步会 AppStorage 中,所以,所有绑定 AppStorage 的 tapIndex 自定义组件都会被通知 UI 刷新。UI 刷新带来的成本是巨大的,因此不建议开发者使用此方式来实现基本的事件通知功能。

// xxx.ets
class ViewData {title: string;uri: Resource;color: Color = Color.Black;constructor(title: string, uri: Resource) {this.title = title;this.uri = uri}
}@Entry
@Component
struct Gallery2 {dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]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: ""};// 判断是否被选中onTapIndexChange() {if (this.tapIndex >= 0 && this.index === this.tapIndex) {console.info(`tapindex: ${this.tapIndex}, index: ${this.index}, red`)this.tapColor = Color.Red;} else {console.info(`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 })}}
}

开发者可以使用 emit 订阅某个事件并接收事件回调,可以减少开销,增强代码的可读性。

// xxx.ets
import emitter from '@ohos.events.emitter';let NextID: number = 0;class ViewData {title: string;uri: Resource;color: Color = Color.Black;id: number;constructor(title: string, uri: Resource) {this.title = title;this.uri = urithis.id = NextID++;}
}@Entry
@Component
struct Gallery2 {dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]scroller: Scroller = new Scroller()private preIndex: number = -1build() {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 }// 选中态:黑变红let eventData: emitter.EventData = {data: {"colorTag": 1}}emitter.emit(innerEvent, eventData)if (this.preIndex != -1) {console.info(`preIndex: ${this.preIndex}, index: ${item.id}, black`)let innerEvent: emitter.InnerEvent = { eventId: this.preIndex }// 取消选中态:红变黑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() {//定义事件IDlet 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 })}}
}

以上通知事件逻辑简单,也可以简化成三元表达式。

// xxx.ets
class ViewData {title: string;uri: Resource;color: Color = Color.Black;constructor(title: string, uri: Resource) {this.title = title;this.uri = uri}
}@Entry
@Component
struct Gallery2 {dataList: Array<ViewData> = [new ViewData('flower', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon')), new ViewData('OMG', $r('app.media.icon'))]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;@State tapColor: Color = Color.Black;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})}}
}

限制条件

AppStorage 与PersistentStorage以及Environment配合使用时,需要注意以下几点:

● 在 AppStorage 中创建属性后,调用 PersistentStorage.persistProp()接口时,会使用在 AppStorage 中已经存在的值,并覆盖 PersistentStorage 中的同名属性,所以建议要使用相反的调用顺序,反例可见在PersistentStorage之前访问AppStorage中的属性;

● 如果在 AppStorage 中已经创建属性后,再调用 Environment.envProp()创建同名的属性,会调用失败。因为 AppStorage 已经有同名属性,Environment 环境变量不会再写入 AppStorage 中,所以建议 AppStorage 中属性不要使用 Environment 预置环境变量名。

● 状态装饰器装饰的变量,改变会引起 UI 的渲染更新,如果改变的变量不是用于 UI 更新,只是用于消息传递,推荐使用 emitter 方式。例子可见不建议借助@StorageLink的双向同步机制实现事件通知。

更多推荐

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

本文发布于:2023-12-08 00:22:48,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1672082.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:全局   状态   OpenHarmony   AppStorage   UI

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!