5.状态管理
1.概述
1.基本概念
前文的描述中,我们构建的页面多为静态界面。如果希望构建一个动态的、有交互的界面,就需要引入“状态”的概念。
图1 效果图
上面的示例中,用户与应用程序的交互触发了文本状态变更,状态变更引起了UI渲染,UI从“Hello World”变更为“Hello ArkUI”。
在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。
自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。 下图展示了State和View(UI)之间的关系。
- View(UI):UI渲染,指将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面。
- State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。
在阅读状态管理文档前,开发者需要对UI范式基本语法有基本的了解。建议提前阅读:基本语法概述,声明式UI描述,自定义组件-创建自定义组件。
名词介绍
- 状态变量:被状态装饰器装饰的变量,状态变量值的改变会引起UI的渲染更新。示例:@State num: number = 1,其中,@State是状态装饰器,num是状态变量。
- 常规变量:没有被状态装饰器装饰的变量,通常应用于辅助计算。它的改变永远不会引起UI的刷新。以下示例中increaseBy变量为常规变量。
- 数据源/同步源:状态变量的原始来源,可以同步给不同的状态数据。通常意义为父组件传给子组件的数据。以下示例中数据源为count: 1。
- 命名参数机制:父组件通过指定参数传递给子组件的状态变量,为父子传递同步参数的主要手段。示例:CompA({ aProp: this.aProp })。
- 从父组件初始化:父组件使用命名参数机制,将指定参数传递给子组件。子组件初始化的默认值在有父组件传值的情况下,会被覆盖。示例:
@Component
struct MyComponent {
@State count: number = 0;
private increaseBy: number = 1;
build() {
}
}
@Entry
@Component
struct Parent {
build() {
Column() {
// 从父组件初始化,覆盖本地定义的默认值
MyComponent({ count: 1, increaseBy: 2 })
}
}
}
- 初始化子组件:父组件中状态变量可以传递给子组件,初始化子组件对应的状态变量。示例同上。
- 本地初始化:在变量声明的时候赋值,作为变量的默认值。示例:@State count: number = 0。
说明
当前状态管理的功能仅支持在UI主线程使用,不能在子线程、worker、taskpool中使用。
一句话数据驱动视图,视图通过组件的事件以及方法改变数据,从而引发页面的重新渲染。
2. 状态管理 V1 概述
装饰器总览
ArkUI状态管理V1提供了多种装饰器,通过使用这些装饰器,状态变量不仅可以观察在组件内的改变,还可以在不同组件层级间传递,比如父子组件、跨组件层级,也可以观察全局范围内的变化。根据状态变量的影响范围,将所有的装饰器可以大致分为:
- 管理组件内状态的装饰器:组件级别的状态管理,可以观察同一个组件树上(即同一个页面内)组件内或不同组件层级的变量变化。
- 管理应用级状态的装饰器:应用级别的状态管理,可以观察不同页面,甚至不同UIAbility的状态变化,是应用内全局的状态管理。
从数据的传递形式和同步类型层面看,装饰器也可分为:
- 只读的单向传递;
- 可变更的双向传递。
图示如下,具体装饰器的介绍,可详见管理组件拥有的状态和管理应用拥有的状态。开发者可以灵活地利用这些能力来实现数据和UI的联动。
上图中,Components部分的装饰器为组件级别的状态管理,Application部分为应用的状态管理。开发者可以通过@StorageLink/@LocalStorageLink实现应用和组件状态的双向同步,通过@StorageProp/@LocalStorageProp实现应用和组件状态的单向同步。
管理组件拥有的状态,即图中Components级别的状态管理:
- @State:@State装饰的变量拥有其所属组件的状态,可以作为其子组件单向和双向同步的数据源。当其数值改变时,会引起相关组件的渲染刷新。
- @Prop:@Prop装饰的变量可以和父组件建立单向同步关系,@Prop装饰的变量是可变的,但修改不会同步回父组件。
- @Link:@Link装饰的变量可以和父组件建立双向同步关系,子组件中@Link装饰变量的修改会同步给父组件中建立双向数据绑定的数据源,父组件的更新也会同步给@Link装饰的变量。
- @Provide/@Consume:@Provide/@Consume装饰的变量用于跨组件层级(多层组件)同步状态变量,可以不需要通过参数命名机制传递,通过alias(别名)或者属性名绑定。
- @Observed:@Observed装饰class,需要观察多层嵌套场景的class需要被@Observed装饰。单独使用@Observed没有任何作用,需要和@ObjectLink、@Prop联用。
- @ObjectLink:@ObjectLink装饰的变量接收@Observed装饰的class的实例,应用于观察多层嵌套场景,和父组件的数据源构建双向同步。
说明
仅@Observed/@ObjectLink可以观察嵌套场景,其他的状态变量仅能观察第一层,详情见各个装饰器章节的“观察变化和行为表现”小节。
管理应用拥有的状态,即图中Application级别的状态管理:
- AppStorage是应用程序中的一个特殊的单例LocalStorage对象,是应用级的数据库,和进程绑定,通过@StorageProp和@StorageLink装饰器可以和组件联动。
- AppStorage是应用状态的“中枢”,将需要与组件(UI)交互的数据存入AppStorage,比如持久化数据PersistentStorage和环境变量Environment。UI再通过AppStorage提供的装饰器或者API接口,访问这些数据。
- 框架还提供了LocalStorage,AppStorage是LocalStorage特殊的单例。LocalStorage是应用程序声明的应用状态的内存“数据库”,通常用于页面级的状态共享,通过@LocalStorageProp和@LocalStorageLink装饰器可以和UI联动。
其他状态管理V1功能
@Watch用于监听状态变量的变化。
$$运算符:给内置组件提供TS变量的引用,使得TS变量和内置组件的内部状态保持同步。
2.管理组件拥有的状态(V1)
1.@State-组件内状态
@State 装饰器是管理组件内部状态的
@State装饰的变量,或称为状态变量
但是,并不是状态变量的所有更改都会引起UI的刷新,只有可以被框架观察到的修改才会引起UI刷新。
观察变化注意点:
●当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
●当装饰的数据类型为Object或数组时
○可以观察到自身的赋值的变化
○可以观察到对象属性赋值的变化,即Object.keys(observedObject)返回的所有属性
■注意:嵌套属性的赋值观察不到
○可以观察到数组本身的赋值和添加、删除、更新数组项的变化
■注意:数组项中属性的赋值观察不到
2.@Prop-父子单向同步
@Prop 装饰的变量可以和父组件建立单向的同步关系
@Prop 装饰的变量是可变的,但是变化不会同步回其父组件
注意:
- 修改父组件数据,会同步更新子组件
- 修改子组件@Prop 修饰的数据,子组件 UI 更新,更新后的数据不会同步给父组件
- 通过回调函数的方式修改父组件的数据,然后触发@Prop数据的更新
3.@Link-父子双向同步
使用@Link 可以实现父组件和子组件的双向同步:
@Link修饰的状态变量也能监听到浅层的数据变化
4.@Provide与@Consume-后代组件双向同步
将数据传递给后代,和后代的数据进行双向同步
// 写法 1:通过相同的变量名绑定
@Provide a: number = 0;
@Consume a: number;
// 写法 2通过相同的变量别名绑定
@Provide b: number = 0;
@Consume('b') c: number;
interface Person {
name: string
age: number
}
@Entry
@Component
// 顶级组件
struct RootComponent {
@Provide
p: Person = {
name: 'jack',
age: 18
}
@Provide
food: string = '西兰花炒蛋'
build() {
Column() {
Text('顶级组件')
.fontSize(30)
.fontWeight(900)
Text(JSON.stringify(this.p))
Text(this.food)
ParentComponent()
}
.padding(10)
.height('100%')
.backgroundColor('#ccc')
.width('100%')
.alignItems(HorizontalAlign.Center)
.padding({ top: 100 })
}
}
@Component
// 二级组件
struct ParentComponent {
// 编写 UI
build() {
Column({ space: 20 }) {
Text('我是二级组件')
.fontSize(22)
.fontWeight(900)
SonComponent()
}
.backgroundColor('#a6c398')
.alignItems(HorizontalAlign.Center)
.width('90%')
.margin({ top: 50 })
.padding(10)
.borderRadius(10)
}
}
@Component
// 内层组件
struct SonComponent {
// 相同变量名
@Consume p: Person
@Consume('food') f: string
// 编写 UI
build() {
Column({ space: 20 }) {
Text('我是内层组件')
.fontSize(20)
.fontWeight(900)
Text(JSON.stringify(this.p))
Text(this.f)
Button('修改')
.onClick(() => {
this.p.name = 'rose'
this.p.age = 99
this.f += '!'
})
}
.backgroundColor('#bf94e4')
.alignItems(HorizontalAlign.Center)
.width('90%')
.margin({ top: 50 })
.padding(10)
.borderRadius(10)
}
}
5.@Observed 与 ObjectLink
问题:@State装饰器仅能观察到第一层的变化,对于多层嵌套的情况(比如对象数组等),他们的第二层属性变化是无法观察到的
解决:@Observed&@ObjectLink可以解决上述问题
@Observed&@ObjectLink作用:用于父子组件场景下,在涉及嵌套对象或数组中进行双向数据同步
●使用new创建被@Observed装饰的类,可以被观察到属性的变化
注意:ObjectLink修饰符不能用在Entry修饰的组件中
//【固定】1. @Observed 装饰类
@Observed
class ClassA{
// 略
}
//2. 父组件
@Component
struct ParentCom{
// 【非固定写法】根据需求来定义数据
@State 变量名: ClassA[] = [ new ClassA(),new ClassA(), ]
@State 变量名:ClassA = new ClassA()
@State 变量名: ClassA[][] = [[new ClassA(),new ClassA()],[new ClassA(),new ClassA()] ]
}
// 【固定】3. 子组件
@Component
struct ChildCom{
@ObjectLink 变量名: ClassA
}
使用步骤:
- interface 改为 类 class 并使用 @Observed 修饰
- 通过 new 的方式完成数据的创建(通过 new 的方式来监测数据的改变)
- 状态修饰符改为 @ObjectLink
-
- 在父组件修改数据:不需要 splice
- 在子组件修改数据
interface Person {
id: number
name: string
age: number
}
@Entry
@Component
struct ObservedAndLink {
@State personList: Person[] = [
{
id: 1,
name: '张三',
age: 18
},
{
id: 2,
name: '李四',
age: 19
},
{
id: 3,
name: '王五',
age: 20
}
]
build() {
Column({ space: 20 }) {
Text('父组件')
.fontSize(30)
List({ space: 10 }) {
ForEach(this.personList, (item: Person, index: number) => {
ItemCom({
info: item,
addAge: () => {
item.age++
this.personList.splice(index, 1, item)
}
})
})
}
}
.backgroundColor('#cbe69b')
.width('100%')
.height('100%')
.padding(20)
}
}
@Component
struct ItemCom {
@Prop info: Person
addAge = () => {
}
build() {
ListItem() {
Row({ space: 10 }) {
Text('姓名:' + this.info.name)
Text('年龄:' + this.info.age)
Blank()
Button('修改数据')
.onClick(() => {
this.addAge()
})
}
.backgroundColor(Color.Pink)
.padding(10)
.width('100%')
}
}
}
@Observed
class Person {
id: number
name: string
age: number
constructor(id: number, name: string, age: number) {
this.id = id
this.name = name
this.age = age
}
}
@Entry
@Component
struct ObservedAndLink {
@State personList: Person[] = [
new Person(1, '张三', 18),
new Person(2, '李四', 18),
new Person(3, '王五', 18),
]
build() {
Column({ space: 20 }) {
Text('父组件')
.fontSize(30)
List({ space: 10 }) {
ForEach(this.personList, (item: Person, index: number) => {
ItemCom({
info: item,
addAge: () => {
item.age++
}
})
})
}
}
.backgroundColor('#cbe69b')
.width('100%')
.height('100%')
.padding(20)
}
}
@Component
struct ItemCom {
@ObjectLink info: Person
addAge = () => {
}
build() {
ListItem() {
Row({ space: 10 }) {
Text('姓名:' + this.info.name)
Text('年龄:' + this.info.age)
Blank()
Button('修改数据')
.onClick(() => {
this.addAge()
// 这种写法也可以
// this.info.age++
})
}
.backgroundColor(Color.Pink)
.padding(10)
.width('100%')
}
}
}
3.管理应用拥有的状态(V1)
TIP
关于应用状态相关的内容需要使用模拟器或真机调试
到目前为止咱们学习的各种装饰器可以实现在一个组件树上共享状态。如果开发者要实现应用级的,或者多个页面的状态数据共享,就需要用到应用级别的状态管理的概念。ArkTS根据不同特性,提供了多种【应用状态管理】的能力:
- LocalStorage:页面级UI状态共享(内存-非持久化状态,退出消失)
- AppStorage:应用内状态-多 UIAbility 共享(内存-非持久化状态,退出消失)
- PersistentStorage:全局持久化状态(磁盘-持久化状态,退出应用 数据同样存在)
- Environment:应用程序运行的设备的环境参数(可读不可写)
-
- 暗黑模式
- 语言模式
- 网络环境
1. LocalStorage
UIAbility内的状态管理
LocalStorage 是页面级的UI状态存储,通过 @Entry 装饰器接收的参数可以在页面内共享同一个 LocalStorage 实例。 LocalStorage 也可以在 UIAbility 内,页面间共享状态。
1.用法
import { router } from '@kit.ArkUI'
// class UserInfo {
// name: string = ''
// age: number = 0
// }
export interface UserInfo{
name: string
age: number
}
let userData: Record<string, UserInfo> = {
'user': {
name: 'jack',
age: 18
}
}
// ✨✨1. 实例化LocalStorage
export const storage = new LocalStorage(userData)
//✨✨ 2. 将LocalStorage实例设置给@Entry的参数
@Entry(storage)
@Component
struct Index {
//✨✨ 3. 页面使用LocalStorage中的数据
// 3.1 使用LocalStorageLink进行双向同步
@LocalStorageLink('user')
user: UserInfo = {
name: 'rose',
age: 18
}
// 3.2 使用LocalStorageProp进行单向同步
// @LocalStorageProp('user')
// user: UserInfo = {
// name: 'rose',
// age: 18
// }
build() {
Column({ space: 15 }) {
Text('我是首页:')
.fontSize(40)
// ✨✨✨4. UI使用LocalStorage的数据
Text(JSON.stringify(this.user)).fontSize(20)
Button('点击+1')
.onClick(() => {
this.user.age++
})
ChildA()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
@Component
struct ChildA {
@LocalStorageLink('user')
user: UserInfo = {
name: '',
age: 0
}
build() {
Column() {
Text('我是子组件A')
.fontSize(30)
Button(this.user.name)
.onClick(() => {
this.user.name += '!'
})
}
.backgroundColor(Color.Orange)
.width('100%')
}
}
2.AppStorage
模块页面数据共享
LocalStorage是针对UIAbility的状态共享- 一个UIAbility有多个页面,这些页面都可以通过 LocalStorage 共享数据
一个应用可能有若干个UIAbility,如果要在多个 UIAbility 共享数据,就可以使用 AppStorage
class User {
name: string = ''
age: number = 0
}
// ✨✨1. 给AppStorage设置初始数据
AppStorage.setOrCreate<User>('user', { name: 'jack', age: 18 })
@Entry
@Component
struct AppStorageUse01 {
//✨✨ 2. 组件内获取AppStorage的数据
@StorageLink('user')
user: User = {
name: '',
age: 0
}
build() {
Column({ space: 15 }) {
Text(JSON.stringify(this.user))
.fontSize(20)
//✨✨ 3. 使用AppStorage数据
Button('点击修改名字')
.fontSize(30)
.onClick(() => {
this.user.name += '!'
})
Button('点击修改年龄')
.fontSize(30)
.onClick(() => {
this.user.age++
})
Divider()
ChildA()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
@Component
struct ChildA {
@StorageProp('user')
user: User = {
name: '',
age: 0
}
build() {
Column({ space: 15 }) {
Text('子组件 A:')
.fontColor(Color.White)
Text(JSON.stringify(this.user))
Button('点击修改名字')
.fontColor(Color.White)
.onClick(() => {
this.user.name += '!'
})
Button('点击修改年龄')
.fontColor(Color.White)
.onClick(() => {
this.user.age++
})
}
.width('100%')
.padding(10)
.backgroundColor(Color.Orange)
}
}
如用不需要持久化我们可以直接省略第一步
@StorageLink('isShow')
isShow: boolean = true
3.PersistentStorage
状态持久化
模拟器或者真机中查看效果-> 查看路径如下
LocalStorage和AppStorage都是运行时的内存,在应用退出后就没有了,如果要在应用退出后再次启动AppStorage依然能保存选定的结果,这就需要用到PersistentStorage。
用法:
PersistentStorage.PersistProp('属性名', 值)
后续直接通过 AppStorage 的 Api 来获取并修改即可,AppStorage 的修改会自动同步到PersistentStorage中
保存简单类型
number, string, boolean, enum 等简单类型都支持
核心步骤:
- 初始化PersistentStorage
- 通过 AppStorage 获取并修改数据
- 重启应用,检测结果
PersistentStorage.persistProp<string>('info','感觉自己闷闷哒')
@Entry
@Component
struct PersistentStoragePage01 {
@StorageLink('info')
info:string=''
build() {
Row() {
Column() {
Text(this.info)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(()=>{
this.info+='!'
})
}
.width('100%')
}
.height('100%')
}
}
保存复杂类型
接下来看看复杂类型
支持的复杂类型:
- 可以被JSON.stringify()和JSON.parse()重构的对象。例如Date, Map, Set等内置类型则不支持,以及对象的属性方法不支持持久化。
划重点:自己定义的 class、interface 基本都是支持的
不允许的类型和值有:
- 不支持嵌套对象(对象数组,对象的属性是对象等)。因为目前框架无法检测AppStorage中嵌套对象(包括数组)值的变化,所以无法写回到PersistentStorage中。
- 不支持 undefined 和 null
interface FoodInfo{
name:string
price:number
}
PersistentStorage.persistProp('foods',[
{name:'西兰花炒蛋',price:10},
{name:'猪脚饭',price:15},
{name:'剁椒鱼头',price:14},
])
1
@Entry
@Component
struct PersistentStoragePage02 {
@StorageLink('foods')
foods:FoodInfo[]=[]
build() {
Row() {
Column() {
Text(JSON.stringify(this.foods))
.fontSize(20)
.fontWeight(FontWeight.Bold)
Button('点击添加新的菜')
.onClick(() => {
this.foods.push({
name:'红烧肉',
price:20
})
})
}
.width('100%')
}
.height('100%')
}
}
说在最后:
- 持久化变量最好是小于2kb的数据,如果开发者需要存储大量的数据,建议使用数据库api。
4.Environment
设备环境查询
开发者如果需要应用程序运行的设备的环境参数,以此来作出不同的场景判断,比如多语言,暗黑模式等,需要用到Environment设备环境查询。
使用步骤:
- Environment.envProp('languageCode', 默认值); // 将设备信息存入 AppStorage 中,如果读取不到使用默认值
- 通过 AppStorage 获取 并使用即可
注:只能读取,无法修改
键 | 数据类型 | 描述 |
accessibilityEnabled | boolean | 获取无障碍屏幕读取是否启用。 |
colorMode | ColorMode | 色彩模型类型:选项为ColorMode.LIGHT: 浅色,ColorMode.DARK: 深色。 |
fontScale | number | 字体大小比例,范围: [0.85, 1.45]。 |
fontWeightScale | number | 字体粗细程度,范围: [0.6, 1.6]。 |
layoutDirection | LayoutDirection | 布局方向类型:包括LayoutDirection.LTR: 从左到右,LayoutDirection.RTL: 从右到左。 |
languageCode | string | 当前系统语言值,取值必须为小写字母, 例如zh。 |
Environment.envProp("accessibilityEnabled", 'aaaaaaaa')
Environment.envProp("fontScale", 'bbbb')
Environment.envProp("languageCode", "cccc")
Environment.envProp("colorMode", "ddddd")
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
aboutToAppear(): void {
console.log('测试','获取无障碍屏幕读取是否启用', AppStorage.get("accessibilityEnabled"))
console.log("测试",'字体大小比例', AppStorage.get("fontScale"))
console.log('测试','当前系统语言值', AppStorage.get("languageCode"))
console.log('测试','色彩模型类型', AppStorage.get("colorMode"))
}
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
4.其他状态管理
1.@Watch装饰器:状态变量更改通知
如果开发者需要关注某个状态变量的值改变后,触发一段逻辑的执行,可以使用 @Watch 为状态变量设置回调函数。
注意:
Watch无法单独使用,必须配合状态装饰器,比如@State、@Prop、@Link
@State、@Prop、@Link 等装饰器在 @Watch 装饰之前
@State / @Prop / @Link 其中之一
@Watch('方法名')
info:string=''
方法名(){
// 数据改变之后会触发
}
/*
* 1. 定义一个状态变量
* 2. 在这个状态变量上使用@Watch来修饰
* 3. 定义一个函数来配合@Watch的使用,至于这个函数的方法体写什么逻辑不固定
* */
@Entry
@Component
struct Index {
@State
@Watch('loadData')
count:number = 100
@State
@Watch('loadData')
msg:string = 'hello'
// source参数表示是哪个状态变量触发的这个函数
// 进来可以用在判断不同的状态变量引起的回调,就处理不同的逻辑
loadData(source:string){
AlertDialog.show({message:this.count.toString() + source})
}
build() {
Column() {
Button('count='+this.count)
.onClick(()=>{
// this.count++
this.msg = 'hi'
})
}
.height('100%')
.width('100%')
.backgroundColor(Color.Pink)
}
}
2.$$语法:内置组件双向同步
$$运算符为系统内置组件提供TS变量的引用,使得TS变量和系统内置组件的内部状态保持同步。
内部状态具体指什么取决于组件。例如,TextInput组件的text参数。
说明
$$还用于@Builder装饰器的按引用传递参数,开发者需要注意两种用法的区别。
使用规则
组件 | 支持的参数/属性 | 起始API版本 |
select | 10 | |
selectAll | 10 | |
selected | 10 | |
selected | 10 | |
selected | 10 | |
mode | 10 | |
checked | 10 | |
rating | 10 | |
value | 10 | |
showSideBar | 10 | |
value | 10 | |
index | 10 | |
index | 10 | |
index | 10 | |
text | 10 | |
text | 10 | |
selected、value | 10 | |
isOn | 10 | |
selected | 10 | |
selected、value | 10 | |
isShow | 10 | |
isShow | 10 | |
refreshing | 8 | |
selected | 10 | |
selected | 10 |
- $$绑定的变量变化时,会触发UI的同步刷新。
使用示例
以TextInput方法的text参数为例:
// xxx.ets
@Entry
@Component
struct TextInputExample {
@State text: string = ''
controller: TextInputController = new TextInputController()
build() {
Column({ space: 20 }) {
Text(this.text)
TextInput({ text: $$this.text, placeholder: 'input your word...', controller: this.controller })
.placeholderColor(Color.Grey)
.placeholderFont({ size: 14, weight: 400 })
.caretColor(Color.Blue)
.width(300)
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}