系列文章目录
HarmonyOS应用开发02-程序框架UIAbility、启动模式与路由跳转
HarmonyOS应用开发03-基础组件-让我们来码出复杂酷炫的UI
上海今天好冷啊(多想有人可以拥抱在一起啊,哈哈哈)
前言
ArkUI开发框架提供了多维度的状态管理机制,和UI相关联的数据,不仅可以在组件内使用,还可以在不同组件层级间传递,比如父子组件之间,爷孙组件之间等,也可以是全局范围内的传递,还可以是跨设备传递。另外,从数据的传递形式来看,可以分为只读的单向传递和可变更的双向传递。
先上图:
一、组件的状态管理
ArkUI作为一种声明式UI,具有状态驱动UI更新的特点。当用户进行界面交互或有外部事件引起状态改变时,状态的变化会触发组件自动更新。所以在ArkUI中,我们只需要通过一个变量来记录状态。当改变状态的时候,ArkUI就会自动更新界面中受影响的部分。
1、ArkUI框架提供了多种管理状态的装饰器来修饰变量,使用这些装饰器修饰的变量即称为状态变量。
2、在组件范围传递的状态管理常见的场景如下:
场景 | 装饰器 |
---|---|
组件内的状态管理 | @State |
与父组件双向同步状态 | @Link |
跨组件层级双向同步状态 | @Provide 和@Consume |
(1)、在组件内使用@State
装饰器来修饰变量,可以使组件根据不同的状态来呈现不同的效果。
(2)、若当前组件的状态需要通过其父组件传递而来,此时需要使用@Prop
装饰器;
(3)、若是父子组件状态需要相互绑定进行双向同步,则需要使用@Link
装饰器。
(4)、使用@Provide
和@Consume
装饰器可以实现跨组件层级双向同步状态。
(5)、在实际应用开发中,应用会根据需要封装数据模型。如果需要观察嵌套类对象属性变化,需要使用@Observed
和@ObjectLink
装饰器,因为上述表格中的装饰器只能观察到对象的第一层属性变化。
(6)、当状态改变,需要对状态变化进行监听做一些相应的操作时,可以使用@Watch
装饰器来修饰状态。
二、应用实例
1、组件内的状态管理:@State
实际开发中由于交互,组件的内容呈现可能产生变化。当需要在组件内使用状态来控制UI的不同呈现方式时,可以使用
@State
装饰器。
@State
状态数据的特征:
- 支持多种数据类型:允许
class
、number
、boolean
、string
强类型的按值和按引用类型。允许这些强类型构成的数组,即Array<class>
、Array<string>
、Array<boolean>
、Array<number>
。不允许object
和any
。 - 内部私有:标记为
@State
的属性是私有变量,只能在组件内访问
。 - 支持多个实例:组件不同实例的内部状态数据独立。
需要本地初始化
:必须为所有@State
变量分配初始值,将变量保持未初始化可能导致框架行为未定义,初始值需要是有意义的值,比如设置class
类型的值为null
就是无意义的,会导致编译报错。- 创建自定义组件时支持通过状态变量名设置初始值:在创建组件实例时,可以通过变量名显式指定
@State
状态属性的初始值。
使用
@State
装饰符改变组件状态,声明式UI的特点就是UI是随数据更改而自动刷新的,我们这里定义了一个类型为 boolean 的变量 isChecked,其被@State
装饰后,框架内建立了数据和视图之间的绑定,其值的改变影响UI的显示。
typescript
@State isChecked: boolean = false;
核心代码:
typescript
@Component
export default struct StudentListItem {
@State isChecked: boolean = false;
private name?: string;
@Builder checkIcon(icon: Resource) {
Image(icon)
.objectFit(ImageFit.Contain)
.width($r('app.float.checkbox_width'))
.height($r('app.float.checkbox_height'))
.margin($r('app.float.checkbox_margin'))
}
build() {
Row() {
if (this.isChecked) {
this.checkIcon($r('app.media.ic_checked'))
} else {
this.checkIcon($r('app.media.ic_unchecked'))
}
Text(this.name)
.fontColor(this.isChecked ? Color.Red : Color.Black)
.fontSize(this.isChecked ? $r('app.float.item_checked_font_size') : $r('app.float.item_font_size'))
.fontWeight(500)
.opacity(this.isChecked ? 0.5 : 1.0)
.decoration({ type: this.isChecked ? TextDecorationType.LineThrough : TextDecorationType.None })
}
.borderRadius(22)
.backgroundColor($r('app.color.start_window_background'))
.width('100%')
.height($r('app.float.list_item_height'))
.onClick(() => {
this.isChecked = !this.isChecked;
})
}
}
2、从父组件单向同步状态:@Prop
当子组件中的状态依赖从父组件传递而来时,需要使用
@Prop
装饰器,@Prop
修饰的变量可以和其父组件中的状态建立单向同步关系。当父组件中状态变化时,该状态值也会更新至@Prop
修饰的变量;对@Prop
修饰的变量的修改不会影响其父组件中的状态。
@Prop
与 @State
有相同的语义,但初始化方式不同,@Prop
装饰的变量可以和父组件的@State
变量建立单向的数据绑定
。即@Prop
修饰的变量必须使用其父组件提供的@State
变量进行初始化,允许组件内部修改@Prop
变量值但更改不会通知给父组件。
@Prop
状态数据具有以下特征:
- 支持简单数据类型:仅支持
number
、string
、boolean
简单类型; - 内部私有:标记为
@Prop
的属性是私有变量,只能在组件内访问。 - 支持多个实例:组件不同实例的内部状态数据独立。
不支持内部初始化
:在创建组件的新实例时,必须将值传递给@Prop
修饰的变量进行初始化,不支持在组件内部进行初始化。
在示例应用中,当用户点击切换子学生列表的 Toggle({ type: ToggleType.Switch, isOn: true })
按钮状态为false时,列表进入Grid
网格模式,点击切换为true状态时,列表切换为List
线性列表模式。整个列表是自定义组件StudentListPage
。中间区域则是用来显示每个目标项,目标项是自定义组件StudentListItem
。从图中可以看出,StudentListItem
是StudentListPage
的子组件。StudentListPage
是 StudentListItem
的父组件。
-
对于父组件
StudentListPage
,其列表显示模式会随Switch
模式的变化而变化,因此父组件拥有编辑模式状态。 -
对于子组件
StudentListItem
,其高度和宽度也会随编辑模式变化,因此子组件也拥有编辑模式状态。 -
但是是否进入编辑模式,其触发点是在用户点击切换列表的
Toggle({ type: ToggleType.Switch, isOn: true })
,状态变化的源头仅在于父组件StudentListPage
。当父组件StudentListPage
中的编辑模式变化时,子组件StudentListItem
的编辑模式状态需要随之变化。
实现思路:
1、在父组件
StudentListPage
中可以定义一个是否切换显示模式的状态,@State isListModel: boolean = true;
即用@State
修饰isListModel。@State
修饰的变量不仅是组件内部的状态,也可以作为子组件单向或双向同步的数据源。2、ArkUI提供了
@Prop
装饰器,@Prop
修饰的变量可以和其父组件中的状态建立单向同步关系,所以用@Prop
修饰子组件StudentListItem
中的isListModel变量@Prop isListModel: boolean;
。3、在父组件
StudentListPage
中,用@State
修饰isListModel,定义列表显示模式状态。然后利用条件渲染实现根据是否切换显示模式,显示不同的列表显示模式。同时,在父组件中需要在用户点击时切换改变状态,触发界面更新。
代码如下:
typescript
import router from '@ohos.router';
import CommonConstants from '../common/constants/CommonConstants';
import { DataItemBean } from '../viewmodel/DataItemBean';
import prompt from '@system.prompt';
// import DataItemBean from '../viewmodel/DataItemBean';
@Component
export default struct StudentListItem {
// 从父组件单向同步状态:@Prop
@Prop isListModel: boolean;
build() {
if (this.isListModel) {
Column() {
Row() {
Row() {
if (this.isChecked) {
this.checkIcon($r('app.media.ic_checked'))
} else {
this.checkIcon($r('app.media.ic_unchecked'))
}
}
.margin(10)
Text(this.studentData.title)
.fontColor(this.isChecked ? Color.Red : Color.Black)
.fontSize(this.isChecked ? $r('app.float.item_checked_font_size') : $r('app.float.item_font_size'))
.fontWeight(this.isChecked ? FontWeight.Bolder : FontWeight.Bold)
.opacity(this.isChecked ? 0.5 : 1.0)
.decoration({ type: this.isChecked ? TextDecorationType.Underline : TextDecorationType.None })
Blank()
Image($r('app.media.ic_arrow_next'))
.width('30vp')
.height('30vp')
.margin({ right: '10vp' })
.onClick(() => {
// console.log('Next Click' + this.name);
console.log('Next Click' + this.studentData.title);
console.log('Next Click' + this.studentData.image);
router.pushUrl({
// url: 'pages/StudentDetailPage',
url: CommonConstants.STUDENT_DETAIL_URL,
params: {
// name: this.name,
studentData: this.studentData
}
}).catch((error) => {
console.log('Next Click', 'IndexPage push error' + JSON.stringify(error));
})
})
}
.width('100%')
.height($r('app.float.list_item_height'))
}
.borderRadius(22)
.backgroundColor($r('app.color.start_window_background'))
.width('100%')
.height(this.isChecked ? $r('app.float.list_item_expand_height') : $r('app.float.list_item_height'))
.onClick(() => {
this.isChecked = !this.isChecked;
})
} else {
Column() {
Row() {
Blank()
Image($r('app.media.ic_arrow_next'))
.width('15vp')
.height('15vp')
.margin(10)
.onClick(() => {
// console.log('Next Click' + this.name);
console.log('Next Click' + this.studentData.title);
console.log('Next Click' + this.studentData.image);
router.pushUrl({
// url: 'pages/StudentDetailPage',
url: CommonConstants.STUDENT_DETAIL_URL,
params: {
// name: this.name,
studentData: this.studentData
}
}).catch((error) => {
console.log('Next Click', 'IndexPage push error' + JSON.stringify(error));
})
})
}
.width('100%')
Blank()
Row() {
Row() {
if (this.isChecked) {
this.checkIcon($r('app.media.ic_checked'), '16vp', '16vp')
} else {
this.checkIcon($r('app.media.ic_unchecked'), '16vp', '16vp')
}
}
.onClick(() => {
...
})
.margin(5)
Blank()
Row() {
Text(this.studentData.title)
.fontColor(this.isChecked ? Color.Yellow : Color.White)
.fontSize('12vp')
.fontWeight(500)
.opacity(this.isChecked ? 0.5 : 1.0)
.decoration({ type: this.isChecked ? TextDecorationType.LineThrough : TextDecorationType.None })
}
.backgroundColor($r('app.color.transparent_backgroundColor'))
.borderRadius({ topLeft: 8, bottomRight: 10 })
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
}
.alignItems(VerticalAlign.Bottom)
.width('100%')
}
.borderRadius(10)
.width('100%')
.height($r('app.float.grid_item_height'))
.backgroundImage(this.studentData.image)
.backgroundImageSize(ImageSize.Cover)
.onClick(...)
}
}
}
3、与父组件双向同步状态:@Link
若是父子组件状态需要相互绑定进行双向同步时,可以使用
@Link
装饰器。父组件中用于初始化子组件@Link
变量的必须是在父组件中定义的状态变量。
@Link
与@State
有相同的语义,但初始化方式不同,@Link
装饰的变量可以和父组件的 @State
变量建立双向的数据绑定
。即@Link
修饰的变量必须使用其父组件提供的@State
变量进行初始化,允许组件内部修改@Link
变量值且更改会通知给父组件。
@Link
状态数据具有以下特征:
- 支持多种数据类型:
@Link
变量的值与@State
变量的类型相同,即class
、number
、string
、boolean
或这些类型的数组。 - 内部私有:标记为
@Link
的属性是私有变量,只能在组件内访问。 - 支持多个实例:组件不同实例的内部状态数据独立。
不支持内部初始化
:在创建组件的新实例时,必须将值传递给@Link
修饰的变量进行初始化,不支持在组件内部进行初始化。初始化使用$
符号。
如上图所示,在示例应用中,当用户点击同一个目标,目标项会展开或者收起。当用户点击不同的目标项时,除了被点击的目标项展开,同时前一次被点击的目标项会收起。
-
在子目标列表中,每个列表项都有其位置索引值
index
属性,表示目标项在列表中的位置。index从0开始,即第一个目标项的索引值为0,第二个目标项的索引值为1,以此类推。此外,clickIndex用来记录被点击的目标项索引。当点击目标一时,clickIndex为0,点击目标三时,clickIndex为2。 -
在父组件子目标列表和每个子组件目标项中都拥有
clickIndex
状态。当目标一展开时,clickIndex为0。此时点击目标三,目标三的clickIndex变为2,只要其父组件子目标列表感知到clickIndex状态变化,同时将此变化传递给目标一。目标一的clickIndex即可同步改变为2,即目标一感知到此时点击了目标三。
实现思路:
1、将列表和目标项对应到列表组件
StudentListPage
和列表项StudentListItem
。首先,需要在父组件StudentListPage
中定义clickIndex状态。2、若此时子组件中的clickIndex用
@Prop
装饰器修饰,当子组件中clickIndex变化时,父组件无法感知,因为@Prop
装饰器建立的是从父组件到子组件的单向同步关系。
ArkUI提供了@Link装饰器,用于与父组件双向同步状态。当子组件 StudentListItem
中的clickIndex用@Link
修饰,可与父组件 StudentListPage
中的clickIndex建立双向同步关系。
在父组件中使用子组件时,将父组件的clickIndex传递给子组件的clickIndex。其中父组件的clickIndex加上
$
表示传递的是引用。
代码示例:
(1)、StudentListPage
typescript
import DataModel from '../viewmodel/DataModel';
import StudentListItem from '../view/StudentListItem';
import router from '@ohos.router';
import prompt from '@system.prompt';
import { DataItemBean } from '../viewmodel/DataItemBean';
import CommonConstants from '../common/constants/CommonConstants';
import { StudentDetailDialog } from '../view/StudentDetailDialog';
const TAG = '[StudentListPage]';
@Entry
@Component
export struct StudentListPage {
...
// 是否是List组件模式
// 组件内的状态管理:@State
@State isListModel: boolean = true;
// 是否点击当前Item
@State clickIndex: number = 0;
...
build() {
Navigation() {
Row() {
if (this.isListModel) {
Scroll() {
Column() {
// Swiper组件
this.SwiperBuilder(this.studentList2)
List({ space: 16 }) {
ForEach(this.studentList2, (item: DataItemBean, index: number) => {
ListItem() {
// StudentListItem({ studentData: item, isListModel: true })
// 将父组件的列表显示模式状态this.isListModel传递给子组件的编辑模式状态isListModel
// 此处指定的参数都将在初始渲染时覆盖本地定义的默认值,并不是所有的参数都需要从父组件初始化
StudentListItem({
studentData: item,
index: index,
isListModel: this.isListModel,
clickIndex: $clickIndex, // 带有"@Link"装饰的属性必须初始化为"$"
})
}
}, (item, index) => JSON.stringify(item) + index)
}
// .width('90%')
.divider({ strokeWidth: 1, color: Color.Gray, startMargin: 30, endMargin: 0 })
// .listDirection(Axis.Horizontal)
Text('---没有更多了---').fontSize('22vp').margin('30vp')
}
}
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
} else {
Scroll() {
Column() {
// Swiper组件
this.SwiperBuilder(this.studentList2)
Grid() {
ForEach(this.studentList2, (item: DataItemBean) => {
GridItem() {
StudentListItem({
studentData: item,
isListModel: this.isListModel,
clickIndex: $clickIndex, // 带有"@Link"装饰的属性必须初始化为"$"
})
}
}, (item: string) => JSON.stringify(item))
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr 1fr')
.columnsGap('10vp')
.rowsGap('10vp')
.height('640vp')
// .layoutDirection(GridDirection.Row)
Text('---没有更多了---').fontSize('22vp').margin('30vp')
}
}
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}
}
.width('90%')
// .margin({ left: 10, right: 10 })
}
.title('学生名单')
.size({ width: '100%', height: '100%' })
.titleMode(NavigationTitleMode.Mini)
.hideBackButton(true)
.menus(this.NavigationMenus())
.backgroundColor($r('app.color.page_background'))
}
}
4、@Watch
@Watch
用来监听状态变量的变化,当它修饰的状态变量发生变更时,回调相应的方式
📢: 注意: @Watch
装饰器只能监听@State
、@Prop
、@Link
、@ObjectLink
、@Provide
、@Consume
、@StorageProp
以及@StorageLink
装饰的变量。
(2)、StudentListItem
ArkUI中监听状态变化@Watch
的能力。用@Watch修饰的状态,当状态发生变化时,会触发声明时定义的回调。给StudentListItem
的中的clickIndex状态加上@Watch("onClickIndexChanged")
。这表示需要监听clickIndex状态的变化。当clickIndex状态变化时,将触发onClickIndexChanged回调:如果点击的列表项索引不等于当前列表项索引,则将isExpanded状态置为false,从而收起该目标项。
typescript
import router from '@ohos.router';
import CommonConstants from '../common/constants/CommonConstants';
import { DataItemBean } from '../viewmodel/DataItemBean';
import prompt from '@system.prompt';
// import DataItemBean from '../viewmodel/DataItemBean';
@Component
export default struct StudentListItem {
...
private index: number;
// @State isListModel: boolean = true;
// 从父组件单向同步状态:@Prop
@Prop isListModel: boolean;
// 是否选中当前Item
@State isChecked: boolean = false;
// 1、ArkUI提供了@Link装饰器,用于与父组件双向同步状态。当子组件StudentListItem中的clickIndex用@Link修饰,可与父组件StudentListPage中的clickIndex建立双向同步关系。
// 2、ArkUI中监听状态变化@Watch的能力。用@Watch修饰的状态,当状态发生变化时,会触发声明时定义的回调。
// 给StudentListItem的中的clickIndex状态加上@Watch("onClickIndexChanged")。这表示需要监听clickIndex状态的变化。
@Link @Watch('onClickIndexChanged') clickIndex: number;
// 当clickIndex状态变化时,将触发onClickIndexChanged回调:如果点击的列表项索引不等于当前列表项索引,则将isExpanded状态置为false,从而收起该目标项。
onClickIndexChanged() {
if (this.clickIndex != this.index) {
this.isChecked = false;
} else {
this.isChecked = true;
}
}
...
build() {
if (this.isListModel) {
Column() {
Row() {
...
Blank()
if (this.isChecked) {
Text("爆")
.margin({ right: 10 })
.textAlign(TextAlign.Center)
.fontWeight(600)
.fontSize('14fp')
.backgroundColor(Color.Red)
.fontColor(Color.White)
.borderRadius(5)
.width(22)
.height(22)
}
...
}
.width('100%')
.height($r('app.float.list_item_height'))
if (this.isChecked) {
Row() {
...
Image(this.studentData.image)
.width('50vp')
.height('50vp')
.border({ width: 3 })
.borderColor(Color.Yellow)
.borderRadius(100)
.borderStyle(BorderStyle.Dotted)
.onClick(this.onItemChildImageClick)
}
.width('100%')
.padding({ left: 10, right: 10 })
.justifyContent(FlexAlign.SpaceAround)
}
}
.borderRadius(22)
.backgroundColor($r('app.color.start_window_background'))
.width('100%')
.height(this.isChecked ? $r('app.float.list_item_expand_height') : $r('app.float.list_item_height'))
.onClick(() => {
this.isChecked = !this.isChecked;
this.clickIndex = this.index;
})
} else {
Column() {
Row() {
Blank()
...
}
.width('100%')
Blank()
Row() {
Row() {
if (this.isChecked) {
this.checkIcon($r('app.media.ic_checked'), '16vp', '16vp')
} else {
this.checkIcon($r('app.media.ic_unchecked'), '16vp', '16vp')
}
}
.onClick(() => {
this.isChecked = !this.isChecked;
})
.margin(5)
Blank()
Row() {
Text(this.studentData.title)
.fontColor(this.isChecked ? Color.Yellow : Color.White)
.fontSize('12vp')
.fontWeight(500)
.opacity(this.isChecked ? 0.5 : 1.0)
.decoration({ type: this.isChecked ? TextDecorationType.LineThrough : TextDecorationType.None })
}
.backgroundColor($r('app.color.transparent_backgroundColor'))
.borderRadius({ topLeft: 8, bottomRight: 10 })
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
}
.alignItems(VerticalAlign.Bottom)
.width('100%')
}
.borderRadius(10)
.width('100%')
.height($r('app.float.grid_item_height'))
.backgroundImage(this.studentData.image)
.backgroundImageSize(ImageSize.Cover)
.onClick(this.onItemChildImageClick)
}
}
}
总结
在实际开发过程中,我们可以根据具体的业务场景选择不同的状态管理模式进行开发。选择合适的状态管理模式对应相应的业务场景,这一点才是相对最重要的。