写点什么

鸿蒙 HarmonyOS 实战 -ArkTS 语言(状态管理)

作者:蜀道山
  • 2024-04-13
    湖南
  • 本文字数:18050 字

    阅读完需:约 59 分钟

鸿蒙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 基本概念


@Componentstruct MyComponent {  //状态变量:被状态装饰器装饰的变量,状态变量值的改变会引起UI的渲染更新  @State count: number = 0;  //常规变量:没有被状态装饰器装饰的变量,通常应用于辅助计算。  private increaseBy: number = 1;
build() { }}
@Componentstruct Parent { build() { Column() { // 从父组件初始化,覆盖本地定义的默认值 MyComponent({ count: 1, increaseBy: 2 }) } }}
复制代码


🦋1.2 装饰器总览 ArkUI 提供了多种装饰器主要分为:管理组件拥有的状态、管理应用拥有的状态、其他状态管理功能,主要图形如下:



☀️1.2.1 管理组件拥有的状态



🌈1.2.1.1 @State 组件内状态



@State 变量装饰器只支持 Object、class、string、number、boolean、enum 类型,以及这些类型的数组。不支持复杂类型(比如 Date 类型)


父子组件初始化和传递装饰图如下:



🍬1.2.1.1.1 变化规则 1、可变类型(boolean、string、number)


// for simple type@State count: number = 0;// value changing can be observedthis.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.2.1.1.2 使用场景 1、简单类型


@Entry@Componentstruct 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@Componentstruct EntryComponent { build() { Column() { // 此处指定的参数都将在初始渲染时覆盖本地定义的默认值,并不是所有的参数都需要从父组件初始化 MyComponent({ count: 1, increaseBy: 2 }) MyComponent({ title: new Model('Hello, World 2'), count: 7 }) } }}
@Componentstruct 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; }) } }}
复制代码


🌈1.2.1.2 @Prop 父子单向同步



@Prop 变量装饰器只支持 string、number、boolean、enum 类型,以及这些类型的数组。不支持复杂类型(比如 any 类型)


父子组件初始化和传递装饰图如下:



1.2.1.2.1 变化规则 1、简单类型


// 简单类型@Prop count: number;// 赋值的变化可以被观察到this.count = 1;
复制代码


对于 @State 和 @Prop 的同步场景:


  • 使用父组件中 @State 变量的值初始化子组件中的 @Prop 变量。当 @State 变量变化时,该变量值也会同步更新至 @Prop 变量。

  • @Prop 装饰的变量的修改不会影响其数据源 @State 装饰变量的值。

  • 除了 @State,数据源也可以用 @Link 或 @Prop 装饰,对 @Prop 的同步机制是相同的。

  • 数据源和 @Prop 变量的类型需要相同。


🍬1.2.1.2.2 使用场景 1、父组件 @State 到子组件 @Prop 简单数据类型同步


@Componentstruct 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@Componentstruct 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 简单数据类型同步


@Componentstruct Child {  @Prop value: number;
build() { Text(`${this.value}`) .fontSize(50) .onClick(()=>{this.value++}) }}@Entry@Componentstruct 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; }}
@Componentstruct 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@Componentstruct 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 本地初始化不和父组件同步


@Componentstruct 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@Componentstruct 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 }) } } }}
复制代码


🌈1.2.1.3 @Link 父子双向同步父组件中 @State, @StorageLink 和 @Link 和子组件 @Link 可以建立双向数据同步。


@Link 变量装饰器只支持 string、number、boolean、enum 类型,以及这些类型的数组。不支持复杂类型(比如 any 类型)


父子组件初始化和传递装饰图如下:



🍬1.2.1.3.1 变化规则


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

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

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


🍬1.2.1.3.2 使用场景 1、简单类型和类对象类型的 @Link


class GreenButtonState {  width: number = 0;  constructor(width: number) {    this.width = width;  }}@Componentstruct 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);        }      })  }}@Componentstruct YellowButton {  @Link yellowButtonState: number;  build() {    Button('Yellow Button')      .width(this.yellowButtonState)      .height(150.0)      .backgroundColor('#ffff00')      .onClick(() => {        // 子组件的简单类型可以同步回父组件        this.yellowButtonState += 50.0;      })  }}@Entry@Componentstruct 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


@Componentstruct 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@Componentstruct Parent { @State arr: number[] = [1, 2, 3]; build() { Column() { Child({ items: $arr }) ForEach(this.arr, item => { Text(`${item}`) }, item => item.toString() ) } }}
复制代码


🌈1.2.1.4 @Provide/@Consume 与后代组件双向同步



@Prop 变量装饰器只支持 string、number、boolean、enum 类型,以及这些类型的数组。不支持复杂类型(比如 any 类型)


父子组件初始化和传递装饰图如下:



🍬1.2.1.4.1 变化规则


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

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

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


🍬1.2.1.4.2 使用场景


@Componentstruct 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%') }}@Componentstruct CompC { build() { Row({ space: 5 }) { CompD() CompD() } }}@Componentstruct CompB { build() { CompC() }}@Entry@Componentstruct CompA { // @Provide装饰的变量reviewVotes由入口组件CompA提供其后代组件 @Provide reviewVotes: number = 0; build() { Column() { Button(`reviewVotes(${this.reviewVotes}), give +1`) .onClick(() => this.reviewVotes += 1) CompB() } }}
复制代码


🌈1.2.1.5 @Observed/@ObjectLink 嵌套类对象属性变化



类型必须是 @Observed 装饰的 class,可用于初始化常规变量、@State、@Link、@Prop、@Provide


嵌套类对象装饰图如下:



🍬1.2.1.5.1 变化规则


class ClassA {  public c: number;
constructor(c: number) { this.c = c; }}
@Observedclass 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.2.1.5.2 使用场景 1、嵌套对象


// objectLinkNestedObjects.etslet NextID: number = 1;
@Observedclass ClassA { public id: number; public c: number;
constructor(c: number) { this.id = NextID++; this.c = c; }}
@Observedclass ClassB { public a: ClassA;
constructor(a: ClassA) { this.a = a; }}
@Componentstruct 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@Componentstruct 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、对象数组


@Componentstruct 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@Componentstruct 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、二维数组


@Observedclass StringArray extends Array<String> {}
@Observedclass StringArray extends Array<String> {}
@Componentstruct 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@Componentstruct 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 管理应用拥有的状态



🌈1.2.2.1 LocalStorage:页面级 UI 状态存储🍬1.2.2.1.1 变化规则



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

  • LocalStorage 中属性键值 key 对应的数据一旦改变,属性键值 key 绑定的所有的数据(包括双向 @LocalStorageLink 和单向 @LocalStorageProp)都将同步修改;

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


🍬1.2.2.1.2 使用场景 1、应用逻辑使用 LocalStorage


let storage = new LocalStorage({ 'PropA': 47 }); // 创建新实例并使用给定对象初始化let propA = storage.get('PropA') // propA == 47let link1 = storage.link('PropA'); // link1.get() == 47let link2 = storage.link('PropA'); // link2.get() == 47let prop = storage.prop('PropA'); // prop.get() = 47link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48prop.set(1); // one-way sync: prop.get()=1; but link1.get() == link2.get() == 48link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49
复制代码


2、从 UI 内部使用 LocalStorage


// 创建新实例并使用给定对象初始化let storage = new LocalStorage({ 'PropA': 47 });
@Componentstruct 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)@Componentstruct 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)@Componentstruct 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() } }}@Componentstruct 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)@Componentstruct 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 });
@Componentstruct 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)@Componentstruct 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.tsimport 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)@Componentstruct CompA { // can access LocalStorage instance using // @LocalStorageLink/Prop decorated variables @LocalStorageLink('PropA') varA: number = 1;
build() { Column() { Text(`${this.varA}`).fontSize(50) } }}
复制代码


🌈1.2.2.2 AppStorage:AppStorage🍬1.2.2.2.1 变化规则和前面一样传递的参数变成 @StorageProp 和 @StorageLink


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

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

  • 当装饰的对象是 array 时,可以观察到数组添加、删除、更新数组单元的变化。🍬1.2.2.2.2 使用场景 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 == 17var link1: SubscribedAbstractProperty<number> = AppStorage.Link('PropA'); // link1.get() == 47var link2: SubscribedAbstractProperty<number> = AppStorage.Link('PropA'); // link2.get() == 47var prop: SubscribedAbstractProperty<number> = AppStorage.Prop('PropA'); // prop.get() == 47
link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48prop.set(1); // one-way sync: prop.get() == 1; but link1.get() == link2.get() == 48link1.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') // == 49link1.get() // == 49link2.get() // == 49prop.get() // == 49
复制代码


2、从 UI 内部使用 AppStorage 和 LocalStorage


AppStorage.SetOrCreate('PropA', 47);let storage = new LocalStorage({ 'PropA': 48 });
@Entry(storage)@Componentstruct 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.etsclass ViewData {  title: string;  uri: Resource;  color: Color = Color.Black;
constructor(title: string, uri: Resource) { this.title = title; this.uri = uri }}
@Entry@Componentstruct 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') } }}@Componentexport 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.etsimport 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@Componentstruct 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') } }}@Componentexport 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.etsclass ViewData {  title: string;  uri: Resource;  color: Color = Color.Black;
constructor(title: string, uri: Resource) { this.title = title; this.uri = uri }}
@Entry@Componentstruct 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') } }}@Componentexport 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 的双向同步机制实现事件通知。


🌈1.2.2.3 PersistentStorage:持久化存储 UI 状态



🍬1.2.2.3.1 变化规则类似 AppStorage,流程图如下:



🍬1.2.2.3.2 使用场景


PersistentStorage.PersistProp('aProp', 47);
@Entry@Componentstruct Index { @State message: string = 'Hello World' @StorageLink('aProp') aProp: number = 48
build() { Row() { Column() { Text(this.message) // 应用退出时会保存当前结果。重新启动后,会显示上一次的保存结果 Text(`${this.aProp}`) .onClick(() => { this.aProp += 1; }) } } }}
复制代码


🌈1.2.2.4 Environment:设备环境查询 Environment 是 ArkUI 框架在应用程序启动时创建的单例对象。它为 AppStorage 提供了一系列描述应用程序运行状态的属性。Environment 的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型。


🍬1.2.2.4.1 变化规则不可读写


🍬1.2.2.4.2 使用场景 1、从 UI 中访问 Environment 参数


// 将设备languageCode存入AppStorage中Environment.EnvProp('languageCode', 'en');let enable = AppStorage.Get('languageCode');
@Entry@Componentstruct 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.2.2.1 使用场景


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 }) } } }
复制代码


🌈1.2.2.2 $$语法:


内置组件双向同步


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) }) } } }
复制代码


看完三件事❤️

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我四个小忙:

  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

  • 关注公众号 『 蜀道衫 』,不定期分享原创知识。

  • 同时可以期待后续文章 ing🚀

  • 关注后回复【666】扫码即可获取鸿蒙学习资料包


用户头像

蜀道山

关注

欢迎关注作者公众号:【 蜀道衫】 2023-12-29 加入

3年Java后端,5年Android应用开发,精通Java高并发、JVM调优、以及Android开发各种技能。现专研学习鸿蒙HarmonyOS Next操作系统

评论

发布
暂无评论
鸿蒙HarmonyOS实战-ArkTS语言(状态管理)_鸿蒙_蜀道山_InfoQ写作社区