-
-
[原创]鸿蒙HarmonyOS实战-ArkTS语言(状态管理)
-
发表于: 2024-4-1 22:20 2552
-
????前言
状态管理是指在应用程序中维护和更新应用程序状态的过程。在一个程序中,可能有很多不同的组件和模块,它们需要共享和相互作用的状态。如果没有一个明确的方式来管理这些状态,就会导致代码混乱、不易维护和难以扩展。
状态管理的目标是提供一种机制,使得所有的组件和模块都可以访问和更新同一个状态。这个状态通常是存储在一个中央存储区域中,被称为状态存储或状态容器。状态管理通常与应用程序的响应式设计紧密相连,以便在状态改变时自动更新应用程序的界面。
????一、ArkTS语言状态管理
1.概述
在声明式UI编程框架中,应用程序的UI是由程序状态驱动的。用户构建一个UI模型,其中应用的运行时状态作为参数传递进去。当参数改变时,UI会根据新的参数重新渲染。这个运行时状态的变化是由状态管理机制来处理的,它会监控状态的变化,并自动更新UI的渲染。
在ArkUI中,自定义组件的变量必须被装饰器装饰为状态变量,这样它们的改变才能引起UI的重新渲染。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。状态变量和UI之间的关系如下图所示:
View(UI):UI渲染,指将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面。
State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。
????1.1 基本概念
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @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 管理组件拥有的状态
1.2.1.1 @State 组件内状态
@State变量装饰器只支持Object、class、string、number、boolean、enum类型,以及这些类型的数组。不支持复杂类型(比如Date类型)
更多鸿蒙最新技术知识点,请关注作者博客:https://t.doruo.cn/14DjR1rEY
父子组件初始化和传递装饰图如下:
1.2.1.1.1 变化规则
1、可变类型(boolean、string、number)
1 2 3 4 | / / for simple type @State count: number = 0 ; / / value changing can be observed this.count = 1 ; |
2、可变类型(class、Object)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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、简单类型
1 2 3 4 5 6 7 8 9 10 11 12 | @Entry @Component struct MyComponent { @State count: number = 0 ; build() { Button(`click times: ${this.count}`) .onClick(() = > { this.count + = 1 ; }) } } |
2、其他类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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; }) } } } |
1.2.1.2 @Prop 父子单向同步
@Prop变量装饰器只支持string、number、boolean、enum类型,以及这些类型的数组。不支持复杂类型(比如any类型)
父子组件初始化和传递装饰图如下:
1.2.1.2.1 变化规则
1、简单类型
1 2 3 4 | / / 简单类型 @Prop count: number; / / 赋值的变化可以被观察到 this.count = 1 ; |
对于@State和@Prop的同步场景:
使用父组件中@State变量的值初始化子组件中的@Prop变量。当@State变量变化时,该变量值也会同步更新至@Prop变量。
@Prop装饰的变量的修改不会影响其数据源@State装饰变量的值。
除了@State,数据源也可以用@Link或@Prop装饰,对@Prop的同步机制是相同的。
数据源和@Prop变量的类型需要相同。
更多鸿蒙最新技术知识点,请关注作者博客:https://t.doruo.cn/14DjR1rEY
1.2.1.2.2 使用场景
1、父组件@State到子组件@Prop简单数据类型同步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | @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简单数据类型同步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | @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简单类型的同步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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本地初始化不和父组件同步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | @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 }) } } } } |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | @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() ) } } } |
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 使用场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | @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() } } } |
1.2.1.5 @Observed/@ObjectLink 嵌套类对象属性变化
类型必须是@Observed装饰的class,可用于初始化常规变量、@State、@Link、@Prop、@Provide
嵌套类对象装饰图如下:
1.2.1.5.1 变化规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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.2.1.5.2 使用场景
1、嵌套对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | / / 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、对象数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | @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、二维数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | @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 管理应用拥有的状态
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
1 2 3 4 5 6 7 8 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | / / 创建新实例并使用给定对象初始化 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单向同步的简单场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | / / 创建新实例并使用给定对象初始化 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双向同步的简单场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | / / 构造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、兄弟节点之间同步状态变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | 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共享到一个或多个视图
1 2 3 4 5 6 7 8 9 10 11 12 | / / 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); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | / / 通过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 ) } } } |
1.2.2.2 AppStorage:AppStorage
1.2.2.2.1 变化规则
和前面一样传递的参数变成@StorageProp和@StorageLink
- 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
- 当装饰的数据类型为class或者Object时,可以观察到赋值和属性赋值的变化,即Object.keys(observedObject)返回的所有属性。
- 当装饰的对象是array时,可以观察到数组添加、删除、更新数组单元的变化。
更多鸿蒙最新技术知识点,请关注作者博客:https://t.doruo.cn/14DjR1rEY
1.2.2.2.2 使用场景
1、从应用逻辑使用AppStorage和LocalStorage
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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的双向同步机制实现事件通知
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | / / 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 }) } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | / / 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 }) } } } |
以上通知事件逻辑简化成三元表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | / / 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的双向同步机制实现事件通知。
1.2.2.3 PersistentStorage:持久化存储UI状态
1.2.2.3.1 变化规则
类似AppStorage,流程图如下:
1.2.2.3.2 使用场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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 ; }) } } } } |
1.2.2.4 Environment:设备环境查询
Environment是ArkUI框架在应用程序启动时创建的单例对象。它为AppStorage提供了一系列描述应用程序运行状态的属性。Environment的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型。
1.2.2.4.1 变化规则
不可读写
1.2.2.4.2 使用场景
1、从UI中访问Environment参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | / / 将设备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
1 2 3 4 5 6 7 8 9 10 | / / 使用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和自定义组件更新
1 2 3 | 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组合使用
1 2 3 | 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 $$语法:内置组件双向同步
1 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????,不定期分享原创知识。
- 更多鸿蒙最新技术知识点,请关注作者博客:https://t.doruo.cn/14DjR1rEY
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课