鸿蒙HarmonyOS实战-ArkTS语言之状态管理详解
状态管理是指在应用程序中维护和更新应用程序状态的过程。在一个程序中,存在多个不同的组件和模块,它们需要共享和相互作用的状态。若没有明确的状态管理方式,将导致代码混乱、难以维护和扩展。
实现状态管理的目标是提供一种机制,使得所有组件和模块都能够访问和更新同一个状态。这个状态通常存储在一个名为状态存储或状态容器的中央存储区域中。状态管理与应用程序的响应式设计紧密相关,以便在状态改变时能够自动更新应用程序的界面。
一、ArkTS 语言状态管理
1. 概述
在声明式 UI 编程框架中,应用程序的 UI 是由程序状态驱动的。用户构建一个 UI 模型,其中应用的运行时状态作为参数传递进去。当参数改变时,UI 会根据新的参数重新渲染。这个运行时状态的变化是由状态管理机制来处理的,它会监控状态的变化,并自动更新 UI 的渲染。
在 ArkUI 中,自定义组件的变量必须被装饰器装饰为状态变量,这样它们的改变才能引起 UI 的重新渲染。如果不使用状态变量,UI 只能在初始化时渲染,后续将不会再刷新。状态变量和 UI 之间的关系如下图所示:
- View(UI):UI 渲染,指将 build 方法内的 UI 描述和@Builder 装饰的方法内的 UI 描述映射到界面。
- State:状态,指驱动 UI 更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起 UI 的重新渲染。
1.1 基本概念
@Component struct MyComponent { //状态变量:被状态装饰器装饰的变量,状态变量值的改变会引起 UI 的渲染更新 @State count: number = 0; //常规变量:没有被状态装饰器装饰的变量,通常应用于辅助计算。 private increaseBy: number = 1; build() { } } @Component struct Parent { build() { Column() { // 从父组件初始化,覆盖本地定义的默认值 MyComponent({ count: 1, increaseBy: 2 }) } } }
1.2 装饰器总览
ArkUI 提供了多种装饰器主要分为:管理组件拥有的状态、管理应用拥有的状态、其他状态管理功能,主要图形如下:
1.2.1 管理组件拥有的状态
@State 组件内状态
@State 变量装饰器只支持 Object、class、string、number、boolean、enum 类型,以及这些类型的数组。不支持复杂类型(比如 Date 类型)
父子组件初始化和传递装饰图如下:
变化规则
1、可变类型(boolean、string、number)
// for simple type @State count: number = 0; // value changing can be observed this.count = 1;
2、可变类型(class、Object)
class ClassA { public value: string; constructor(value: string) { this.value = value; } } class Model { public value: string; public name: ClassA; constructor(value: string, a: ClassA) { this.value = value; this.name = a; } } // class 类型 @State title: Model = new Model('Hello', new ClassA('World')); // class 类型赋值 this.title = new Model('Hi', new ClassA('ArkUI')); // class 属性的赋值 this.title.value = 'Hi' // 嵌套的属性赋值观察不到 this.title.name.value = 'ArkUI'
3、可变类型(array)
class Model { public value: number; constructor(value: number) { this.value = value; } } @State title: Model[] = [new Model(11), new Model(1)] this.title = [new Model(2)] this.title[0] = new Model(2) this.title.pop() this.title.push(new Model(12))
使用场景
1、简单类型
@Entry @Component struct MyComponent { @State count: number = 0; build() { Button(`click times: ${this.count}`) .onClick(() => { this.count += 1; }) } }
2、其他类型
class Model { public value: string; constructor(value: string) { this.value = value; } } @Entry @Component struct EntryComponent { build() { Column() { // 此处指定的参数都将在初始渲染时覆盖本地定义的默认值,并不是所有的参数都需要从父组件初始化 MyComponent({ count: 1, increaseBy: 2 }) MyComponent({ title: new Model('Hello, World 2'), count: 7 }) } } } @Component struct MyComponent { @State title: Model = new Model('Hello World'); @State count: number = 0; private increaseBy: number = 1; build() { Column() { Text(`${this.title.value}`) Button(`Click to change title`).onClick(() => { // @State 变量的更新将触发上面的 Text 组件内容更新 this.title.value = this.title.value === 'Hello ArkUI' ? 'Hello World' : 'Hello ArkUI'; }) Button(`Click to increase count=${this.count}`).onClick(() => { // @State 变量的更新将触发该 Button 组件的内容更新 this.count += this.increaseBy; }) } } }
@Prop 父子单向同步
@Prop 变量装饰器只支持 string、number、boolean、enum 类型,以及这些类型的数组。不支持复杂类型(比如 any 类型)
父子组件初始化和传递装饰图如下:
变化规则
1、简单类型
// 简单类型 @Prop count: number; // 赋值的变化可以被观察到 this.count = 1;
对于@State 和@Prop 的同步场景:
- 使用父组件中@State 变量的值初始化子组件中的@Prop 变量。当@State 变量变化时,该变量值也会同步更新至@Prop 变量。
- @Prop 装饰的变量的修改不会影响其数据源@State 装饰变量的值。
- 除了@State,数据源也可以用@Link 或@Prop 装饰,对@Prop 的同步机制是相同的。
- 数据源和@Prop 变量的类型需要相同。
使用场景
1、父组件@State 到子组件@Prop 简单数据类型同步
@Component struct CountDownComponent { @Prop count: number; costOfOneAttempt: number = 1; build() { Column() { if (this.count > 0) { Text(`You have ${this.count} Nuggets left`) } else { Text('Game over!') } // @Prop 装饰的变量不会同步给父组件 Button(`Try again`).onClick(() => { this.count -= this.costOfOneAttempt; }) } } } @Entry @Component struct ParentComponent { @State countDownStartValue: number = 10; build() { Column() { Text(`Grant ${this.countDownStartValue} nuggets to play.`) // 父组件的数据源的修改会同步给子组件 Button(`+1 - Nuggets in New Game`).onClick(() => { this.countDownStartValue += 1; }) // 父组件的修改会同步给子组件 Button(`-1 - Nuggets in New Game`).onClick(() => { this.countDownStartValue -= 1; }) CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 }) } } }
2、父组件@State 数组项到子组件@Prop 简单数据类型同步
@Component struct Child { @Prop value: number; build() { Text(`${this.value}`) .fontSize(50) .onClick(()=>{this.value++}) } } @Entry @Component struct Index { @State arr: number[] = [1,2,3]; build() { Row() { Column() { Child({value: this.arr[0]}) Child({value: this.arr[1]}) Child({value: this.arr[2]}) Divider().height(5) ForEach(this.arr, item => { Child({value: item}) }, item => item.toString() ) Text('replace entire arr') .fontSize(50) .onClick(()=>{ // 两个数组都包含项“3”。 this.arr = this.arr[0] == 1 ? [3,4,5] : [1,2,3]; }) } } } }
3、从父组件中的@State 类对象属性到@Prop 简单类型的同步
class Book { public title: string; public pages: number; public readIt: boolean = false; constructor(title: string, pages: number) { this.title = title; this.pages = pages; } } @Component struct ReaderComp { @Prop title: string; @Prop readIt: boolean; build() { Row() { Text(this.title) Text(`... ${this.readIt ? 'I have read' : 'I have not read it'}`) .onClick(() => this.readIt = true) } } } @Entry @Component struct Library { @State book: Book = new Book('100 secrets of C++', 765); build() { Column() { ReaderComp({ title: this.book.title, readIt: this.book.readIt }) ReaderComp({ title: this.book.title, readIt: this.book.readIt }) } } }
4、@Prop 本地初始化不和父组件同步
@Component struct MyComponent { @Prop customCounter: number; @Prop customCounter2: number = 5; build() { Column() { Row() { Text(`From Main: ${this.customCounter}`).width(90).height(40).fontColor('#FF0010') } Row() { Button('Click to change locally !').width(180).height(60).margin({ top: 10 }) .onClick(() => { this.customCounter2++ }) }.height(100).width(180) Row() { Text(`Custom Local: ${this.customCounter2}`).width(90).height(40).fontColor('#FF0010') } } } } @Entry @Component struct MainProgram { @State mainCounter: number = 10; build() { Column() { Row() { Column() { Button('Click to change number').width(480).height(60).margin({ top: 10, bottom: 10 }) .onClick(() => { this.mainCounter++ }) } } Row() { Column() // customCounter 必须从父组件初始化,因为 MyComponent 的 customCounter 成员变量缺少本地初始化;此处,customCounter2 可以不做初始化。 MyComponent({ customCounter: this.mainCounter }) // customCounter2 也可以从父组件初始化,父组件初始化的值会覆盖子组件 customCounter2 的本地初始化的值 MyComponent({ customCounter: this.mainCounter, customCounter2: this.mainCounter }) } } } }
@Link 父子双向同步
父组件中@State, @StorageLink 和@Link 和子组件@Link 可以建立双向数据同步。
@Link 变量装饰器只支持 string、number、boolean、enum 类型,以及这些类型的数组。不支持复杂类型(比如 any 类型)
父子组件初始化和传递装饰图如下:
变化规则
- 当装饰的数据类型为 boolean、string、number 类型时,可以同步观察到数值的变化。
- 当装饰的数据类型为 class 或者 Object 时,可以观察到赋值和属性赋值的变化,即 Object.keys(observedObject)返回的所有属性。
- 当装饰的对象是 array 时,可以观察到数组添加、删除、更新数组单元的变化。
使用场景
1、简单类型和类对象类型的@Link
class GreenButtonState { width: number = 0; constructor(width: number) { this.width = width; } } @Component struct GreenButton { @Link greenButtonState: GreenButtonState; build() { Button('Green Button') .width(this.greenButtonState.width) .height(150.0) .backgroundColor('#00ff00') .onClick(() => { if (this.greenButtonState.width < 700) { // 更新 class 的属性,变化可以被观察到同步回父组件 this.greenButtonState.width += 125; } else { // 更新 class,变化可以被观察到同步回父组件 this.greenButtonState = new GreenButtonState(100); } }) } } @Component struct YellowButton { @Link yellowButtonState: number; build() { Button('Yellow Button') .width(this.yellowButtonState) .height(150.0) .backgroundColor('#ffff00') .onClick(() => { // 子组件的简单类型可以同步回父组件 this.yellowButtonState += 50.0; }) } } @Entry @Component struct ShufflingContainer { @State greenButtonState: GreenButtonState = new GreenButtonState(300); @State yellowButtonProp: number = 100; build() { Column() { // 简单类型从父组件@State 向子组件@Link 数据同步 Button('Parent View: Set yellowButton') .onClick(() => { this.yellowButtonProp = (this.yellowButtonProp < 700) ? this.yellowButtonProp + 100 : 100; }) // class 类型从父组件@State 向子组件@Link 数据同步 Button('Parent View: Set GreenButton') .onClick(() => { this.greenButtonState.width = (this.greenButtonState.width < 700) ? this.greenButtonState.width + 100 : 100; }) // class 类型初始化@Link GreenButton({ greenButtonState: $greenButtonState }) // 简单类型初始化@Link YellowButton({ yellowButtonState: $yellowButtonProp }) } } }
2、数组类型的@Link
@Component struct Child { @Link items: number[]; build() { Column() { Button(`Button1: push`).onClick(() => { this.items.push(this.items.length + 1); }) Button(`Button2: replace whole item`).onClick(() => { this.items = [100, 200, 300]; }) } } } @Entry @Component struct Parent { @State arr: number[] = [1, 2, 3]; build() { Column() { Child({ items: $arr }) ForEach(this.arr, item => { Text(`${item}`) }, item => item.toString() ) } } }
@Provide/@Consume 与后代组件双向同步
@Prop 变量装饰器只支持 string、number、boolean、enum 类型,以及这些类型的数组。不支持复杂类型(比如 any 类型)
父子组件初始化和传递装饰图如下:
变化规则
- 当装饰的数据类型为 boolean、string、number 类型时,可以观察到数值的变化。
- 当装饰的数据类型为 class 或者 Object 的时候,可以观察到赋值和属性赋值的变化(属性为 Object.keys(observedObject)返回的所有属性)。
- 当装饰的对象是 array 的时候,可以观察到数组的添加、删除、更新数组单元。
使用场景
@Component struct CompD { // @Consume 装饰的变量通过相同的属性名绑定其祖先组件 CompA 内的@Provide 装饰的变量 @Consume reviewVotes: number; build() { Column() { Text(`reviewVotes(${this.reviewVotes})`) Button(`reviewVotes(${this.reviewVotes}), give +1`) .onClick(() => this.reviewVotes += 1) } .width('50%') } } @Component struct CompC { build() { Row({ space: 5 }) { CompD() CompD() } } } @Component struct CompB { build() { CompC() } } @Entry @Component struct CompA { // @Provide 装饰的变量 reviewVotes 由入口组件 CompA 提供其后代组件 @Provide reviewVotes: number = 0; build() { Column() { Button(`reviewVotes(${this.reviewVotes}), give +1`) .onClick(() => this.reviewVotes += 1) CompB() } } }
@Observed/@ObjectLink 嵌套类对象属性变化
类型必须是@Observed 装饰的 class,可用于初始化常规变量、@State、@Link、@Prop、@Provide
嵌套类对象装饰图如下:
变化规则
class ClassA { public c: number; constructor(c: number) { this.c = c; } } @Observed class ClassB { public a: ClassA; public b: number; constructor(a: ClassA, b: number) { this.a = a; this.b = b; } } @ObjectLink b: ClassB // 赋值变化可以被观察到 this.b.a = new ClassA(5) this.b.b = 5 // ClassA 没有被@Observed 装饰,其属性的变化观察不到 this.b.a.c = 5
使用场景
1、嵌套对象
// objectLinkNestedObjects.ets let NextID: number = 1; @Observed class ClassA { public id: number; public c: number; constructor(c: number) { this.id = NextID++; this.c = c; } } @Observed class ClassB { public a: ClassA; constructor(a: ClassA) { this.a = a; } } @Component struct ViewA { label: string = 'ViewA1'; @ObjectLink a: ClassA; build() { Row() { Button(`ViewA [${this.label}] this.a.c=${this.a.c} +1`) .onClick(() => { this.a.c += 1; }) } } } @Entry @Component struct ViewB { @State b: ClassB = new ClassB(new ClassA(0)); build() { Column() { ViewA({ label: 'ViewA #1', a: this.b.a }) ViewA({ label: 'ViewA #2', a: this.b.a }) Button(`ViewB: this.b.a.c+= 1`) .onClick(() => { this.b.a.c += 1; }) Button(`ViewB: this.b.a = new ClassA(0)`) .onClick(() => { this.b.a = new ClassA(0); }) Button(`ViewB: this.b = new ClassB(ClassA(0))`) .onClick(() => { this.b = new ClassB(new ClassA(0)); }) } } }
2、对象数组
@Component struct ViewA { // 子组件 ViewA 的@ObjectLink 的类型是 ClassA @ObjectLink a: ClassA; label: string = 'ViewA1'; build() { Row() { Button(`ViewA [${this.label}] this.a.c = ${this.a.c} +1`) .onClick(() => { this.a.c += 1; }) } } } @Entry @Component struct ViewB { // ViewB 中有@State 装饰的 ClassA[] @State arrA: ClassA[] = [new ClassA(0), new ClassA(0)]; build() { Column() { ForEach(this.arrA, (item) => { ViewA({ label: `#${item.id}`, a: item }) }, (item) => item.id.toString() ) // 使用@State 装饰的数组的数组项初始化@ObjectLink,其中数组项是被@Observed 装饰的 ClassA 的实例 ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] }) ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] }) Button(`ViewB: reset array`) .onClick(() => { this.arrA = [new ClassA(0), new ClassA(0)]; }) Button(`ViewB: push`) .onClick(() => { this.arrA.push(new ClassA(0)) }) Button(`ViewB: shift`) .onClick(() => { this.arrA.shift() }) Button(`ViewB: chg item property in middle`) .onClick(() => { this.arrA[Math.floor(this.arrA.length / 2)].c = 10; }) Button(`ViewB: chg item property in middle`) .onClick(() => { this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11); }) } } }
3、二维数组
@Observed class StringArray extends Array<String> { } @Observed class StringArray extends Array<String> { } @Component struct ItemPage { @ObjectLink itemArr: StringArray; build() { Row() { Text('ItemPage') .width(100).height(100) ForEach(this.itemArr, item => { Text(item) .width(100).height(100) }, item => item ) } } } @Entry @Component struct IndexPage { @State arr: Array<StringArray> = [new StringArray(), new StringArray(), new StringArray()]; build() { Column() { ItemPage({ itemArr: this.arr[0] }) ItemPage({ itemArr: this.arr[1] }) ItemPage({ itemArr: this.arr[2] }) Divider() ForEach(this.arr, itemArr => { ItemPage({ itemArr: itemArr }) }, itemArr => itemArr[0] ) Divider() Button('update') .onClick(() => { console.error('Update all items in arr'); if (this.arr[0][0] !== undefined) { // 正常情况下需要有一个真实的 ID 来与 ForEach 一起使用,但此处没有 // 因此需要确保推送的字符串是唯一的。 this.arr[0].push(`${this.arr[0].slice(-1).pop()}${this.arr[0].slice(-1).pop()}`); this.arr[1].push(`${this.arr[1].slice(-1).pop()}${this.arr[1].slice(-1).pop()}`); this.arr[2].push(`${this.arr[2].slice(-1).pop()}${this.arr[2].slice(-1).pop()}`); } else { this.arr[0].push('Hello'); this.arr[1].push('World'); this.arr[2].push('!'); } }) } } }
1.2.2 管理应用拥有的状态
LocalStorage:页面级 UI 状态存储
变化规则:
- 当@LocalStorageLink(key)装饰的数值改变被观察到时,修改将被同步回 LocalStorage 对应属性键值 key 的属性中。
- LocalStorage 中属性键值 key 对应的数据一旦改变,属性键值 key 绑定的所有的数据(包括双向@LocalStorageLink 和单向@LocalStorageProp)都将同步修改;
- 当@LocalStorageLink(key)装饰的数据本身是状态变量,它的改变不仅仅会同步回 LocalStorage 中,还会引起所属的自定义组件的重新渲染。
使用场景:
1、应用逻辑使用 LocalStorage
let storage = new LocalStorage({ 'PropA': 47 }); // 创建新实例并使用给定对象初始化 let propA = storage.get('PropA') // propA == 47 let link1 = storage.link('PropA'); // link1.get() == 47 let link2 = storage.link('PropA'); // link2.get() == 47 let prop = storage.prop('PropA'); // prop.get() = 47 link1.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() == 49
2、从 UI 内部使用 LocalStorage
// 创建新实例并使用给定对象初始化 let storage = new LocalStorage({ 'PropA': 47 }); @Component struct Child { // @LocalStorageLink 变量装饰器与 LocalStorage 中的'PropA'属性建立双向绑定 @LocalStorageLink('PropA') storLink2: number = 1; build() { Button(`Child from LocalStorage ${this.storLink2}`) // 更改将同步至 LocalStorage 中的'PropA'以及 Parent.storLink1 .onClick(() => this.storLink2 += 1) } } // 使 LocalStorage 可从@Component 组件访问 @Entry(storage) @Component struct CompA { // @LocalStorageLink 变量装饰器与 LocalStorage 中的'PropA'属性建立双向绑定 @LocalStorageLink('PropA') storLink1: number = 1; build() { Column({ space: 15 }) { Button(`Parent from LocalStorage ${this.storLink1}`) // initial value from LocalStorage will be 47, because 'PropA' initialized already .onClick(() => this.storLink1 += 1) // @Component 子组件自动获得对 CompA LocalStorage 实例的访问权限。 Child() } } }
3、@LocalStorageProp 和 LocalStorage 单向同步的简单场景
// 创建新实例并使用给定对象初始化 let storage = new LocalStorage({ 'PropA': 47 }); // 使 LocalStorage 可从@Component 组件访问 @Entry(storage) @Component struct CompA { // @LocalStorageProp 变量装饰器与 LocalStorage 中的'PropA'属性建立单向绑定 @LocalStorageProp('PropA') storProp1: number = 1; build() { Column({ space: 15 }) { // 点击后从 47 开始加 1,只改变当前组件显示的 storProp1,不会同步到 LocalStorage 中 Button(`Parent from LocalStorage ${this.storProp1}`) .onClick(() => this.storProp1 += 1) Child() } } } @Component struct Child { // @LocalStorageProp 变量装饰器与 LocalStorage 中的'PropA'属性建立单向绑定 @LocalStorageProp('PropA') storProp2: number = 2; build() { Column({ space: 15 }) { // 当 CompA 改变时,当前 storProp2 不会改变,显示 47 Text(`Parent from LocalStorage ${this.storProp2}`) } } }
4、@LocalStorageLink 和 LocalStorage 双向同步的简单场景
// 构造 LocalStorage 实例 let storage = new LocalStorage({ 'PropA': 47 }); // 调用 link9+接口构造'PropA'的双向同步数据,linkToPropA 是全局变量 let linkToPropA = storage.link('PropA'); @Entry(storage) @Component struct CompA { // @LocalStorageLink('PropA')在 CompA 自定义组件中创建'PropA'的双向同步数据,初始值为 47,因为在构造 LocalStorage 已经给“PropA”设置 47 @LocalStorageLink('PropA') storLink: number = 1; build() { Column() { Text(`incr @LocalStorageLink variable`) // 点击“incr @LocalStorageLink variable”,this.storLink 加 1,改变同步回 storage,全局变量 linkToPropA 也会同步改变 .onClick(() => this.storLink += 1) // 并不建议在组件内使用全局变量 linkToPropA.get(),因为可能会有生命周期不同引起的错误。 Text(`@LocalStorageLink: ${this.storLink} - linkToPropA: ${linkToPropA.get()}`) } } }
5、兄弟节点之间同步状态变量
let storage = new LocalStorage({ countStorage: 1 }); @Component struct Child { // 子组件实例的名字 label: string = 'no name'; // 和 LocalStorage 中“countStorage”的双向绑定数据 @LocalStorageLink('countStorage') playCountLink: number = 0; build() { Row() { Text(this.label) .width(50).height(60).fontSize(12) Text(`playCountLink ${this.playCountLink}: inc by 1`) .onClick(() => { this.playCountLink += 1; }) .width(200).height(60).fontSize(12) }.width(300).height(60) } } @Entry(storage) @Component struct Parent { @LocalStorageLink('countStorage') playCount: number = 0; build() { Column() { Row() { Text('Parent') .width(50).height(60).fontSize(12) Text(`playCount ${this.playCount} dec by 1`) .onClick(() => { this.playCount -= 1; }) .width(250).height(60).fontSize(12) }.width(300).height(60) Row() { Text('LocalStorage') .width(50).height(60).fontSize(12) Text(`countStorage ${this.playCount} incr by 1`) .onClick(() => { storage.set<number>('countStorage', 1 + storage.get<number>('countStorage')); }) .width(250).height(60).fontSize(12) }.width(300).height(60) Child({ label: 'ChildA' }) Child({ label: 'ChildB' }) Text(`playCount in LocalStorage for debug ${storage.get<number>('countStorage')}`) .width(300).height(60).fontSize(12) } } }
6、将 LocalStorage 实例从 UIAbility 共享到一个或多个视图
// EntryAbility.ts import UIAbility from '@ohos.app.ability.UIAbility'; import window from '@ohos.window'; let para:Record<string,number> = { 'PropA': 47 }; let localStorage: LocalStorage = new LocalStorage(para); export default class EntryAbility extends UIAbility { storage: LocalStorage = localStorage onWindowStageCreate(windowStage: window.WindowStage) { windowStage.loadContent('pages/Index', this.storage); } }
// 通过 GetShared 接口获取 stage 共享的 LocalStorage 实例 let storage = LocalStorage.GetShared() @Entry(storage) @Component struct CompA { // can access LocalStorage instance using // @LocalStorageLink/Prop decorated variables @LocalStorageLink('PropA') varA: number = 1; build() { Column() { Text(`${this.varA}`).fontSize(50) } } }
AppStorage:AppStorage
变化规则
和前面一样传递的参数变成@StorageProp 和@StorageLink
- 当装饰的数据类型为 boolean、string、number 类型时,可以观察到数值的变化。
- 当装饰的数据类型为 class 或者 Object 时,可以观察到赋值和属性赋值的变化,即 Object.keys(observedObject)返回的所有属性。
- 当装饰的对象是 array 时,可以观察到数组添加、删除、更新数组单元的变化。
使用场景
1、从应用逻辑使用 AppStorage 和 LocalStorage
AppStorage.SetOrCreate('PropA', 47); let storage: LocalStorage = new LocalStorage({ 'PropA': 17 }); let propA: number = AppStorage.Get('PropA') // propA in AppStorage == 47, propA in LocalStorage == 17 var link1: SubscribedAbstractProperty<number> = AppStorage.Link('PropA'); // link1.get() == 47 var link2: SubscribedAbstractProperty<number> = AppStorage.Link('PropA'); // link2.get() == 47 var prop: SubscribedAbstractProperty<number> = AppStorage.Prop('PropA'); // prop.get() == 47 link1.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() == 49 storage.get('PropA') // == 17 storage.set('PropA', 101); storage.get('PropA') // == 101 AppStorage.Get('PropA') // == 49 link1.get() // == 49 link2.get() // == 49 prop.get() // == 49
2、从 UI 内部使用 AppStorage 和 LocalStorage
AppStorage.SetOrCreate('PropA', 47); let storage = new LocalStorage({ '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) } } }
3、不建议借助@StorageLink 的双向同步机制实现事件通知
// 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 }) } } }
// 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 = uri this.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 = -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 } // 选中态:黑变红 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() { //定义事件 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 }) } } }
以上通知事件逻辑简化成三元表达式
// 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 的双向同步机制实现事件通知。
PersistentStorage:持久化存储 UI 状态
变化规则
类似 AppStorage,流程图如下:
使用场景
PersistentStorage.PersistProp('aProp', 47); @Entry @Component struct Index { @State message: string = 'Hello World' @StorageLink('aProp') aProp: number = 48 build() { Row() { Column() { Text(this.message) // 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果 Text(`${this.aProp}`) .onClick(() => { this.aProp += 1; }) } } } }
Environment:设备环境查询
Environment 是 ArkUI 框架在应用程序启动时创建的单例对象。它为 AppStorage 提供了一系列描述应用程序运行状态的属性。Environment 的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型。
变化规则
不可读写。
使用场景
1、从 UI 中访问 Environment 参数
// 将设备 languageCode 存入 AppStorage 中 Environment.EnvProp('languageCode', 'en'); let enable = AppStorage.Get('languageCode'); @Entry @Component struct Index { @StorageProp('languageCode') languageCode: string = 'en'; build() { Row() { Column() { // 输出当前设备的 languageCode Text(this.languageCode) } } } }
2、应用逻辑使用 Environment
// 使用 Environment.EnvProp 将设备运行 languageCode 存入 AppStorage 中; Environment.EnvProp('languageCode', 'en'); // 从 AppStorage 获取单向绑定的 languageCode 的变量 const lang: SubscribedAbstractProperty<string> = AppStorage.Prop('languageCode'); if (lang.get() === 'zh') { console.info('你好'); } else { console.info('Hello!'); }
1.2.3 其他状态管理功能
- @Watch:用于监听状态变量的变化。
运算符:给内置组件提供 TS 变量的引用,使得 TS 变量和内置组件的内部状态保持同步。
使用场景
1、@Watch 和自定义组件更新
clike @Component struct TotalView { @Prop @Watch('onCountUpdated') count: number; @State total: number = 0; // @Watch cb onCountUpdated(propName: string): void { this.total += this.count; } build() { Text(`Total: ${this.total}`) } } @Entry @Component struct CountModifier { @State count: number = 0; build() { Column() { Button('add to basket') .onClick(() => { this.count++ }) TotalView({ count: this.count }) } } } ```
2、@Watch 与@Link 组合使用
clike class PurchaseItem { static NextId: number = 0; public id: number; public price: number; constructor(price: number) { this.id = PurchaseItem.NextId++; this.price = price; } } @Component struct BasketViewer { @Link @Watch('onBasketUpdated') shopBasket: PurchaseItem[]; @State totalPurchase: number = 0; updateTotal(): number { let total = this.shopBasket.reduce((sum, i) => sum + i.price, 0); // 超过 100 欧元可享受折扣 if (total >= 100) { total = 0.9 * total; } return total; } // @Watch 回调 onBasketUpdated(propName: string): void { this.totalPurchase = this.updateTotal(); } build() { Column() { ForEach(this.shopBasket, (item) => { Text(`Price: ${item.price.toFixed(2)} €`) }, item => item.id.toString() ) Text(`Total: ${this.totalPurchase.toFixed(2)} €`) } } } @Entry @Component struct BasketModifier { @State shopBasket: PurchaseItem[] = []; build() { Column() { Button('Add to basket') .onClick(() => { this.shopBasket.push(new PurchaseItem(Math.round(100 * Math.random()))) }) BasketViewer({ shopBasket: $shopBasket }) } } }
$$语法:内置组件双向同步
clike // xxx.ets @Entry @Component struct RefreshExample { @State isRefreshing: boolean = false @State counter: number = 0 build() { Column() { Text('Pull Down and isRefreshing: ' + this.isRefreshing) .fontSize(30) .margin(10) Refresh({ refreshing: $$this.isRefreshing, offset: 120, friction: 100 }) { Text('Pull Down and refresh: ' + this.counter) .fontSize(30) .margin(10) } .onStateChange((refreshStatus: RefreshStatus) => { console.info('Refresh onStatueChange state is ' + refreshStatus) }) } } }
码云笔记 » 鸿蒙HarmonyOS实战-ArkTS语言之状态管理详解