ArkTS 是 HarmonyOS 优选主力开发语言。ArkTS 是基于 TS(TypeScript)扩展的一门语言,继承了 TS 的所以特性,是TS的超集。
主要是扩展了以下几个方面:
声明式UI描述和自定义组件:
动态扩展UI元素的能力:
多维度的状态管理机制:
渲染控制的能力:
概念 | 描述 |
---|---|
装饰器 | 用于装饰类、结构、方法以及变量,赋予其特殊含义。例如,@Component 表示自定义组件,@Entry 表示入口组件,@State 表示状态变量。状态变量的变化会触发UI刷新。 |
UI描述 | 使用声明式的方式描述UI的结构,通常体现在 build() 方法中的代码块。 |
自定义组件 | 可复用的UI单元,可组合其他组件。被 @Component 装饰的 struct xxx 就是一个自定义组件。 |
系统组件 | ArkUI框架中默认内置的基础和容器组件,可以直接调用。 常见系统组件:Column 、Text 、Divider 、Button 。 |
属性方法 | 组件可以通过链式调用配置多项属性的方法。例如,fontSize() 、width() 、height() 、backgroundColor() 可以用于设置组件的样式和尺寸等属性。 |
事件方法 | 组件可以通过链式调用设置多个事件的响应逻辑。例如,在 Button 后面的 onClick() 就是一个事件方法,用于处理按钮被点击时的逻辑。 |
代码示例:
// 1. 按引用传递参数时,传递的参数可为状态变量,
// 且状态变量的改变会引起@Builder方法内的UI刷新
@Builder function MessageBuilder($$:{message:string}) { // 自定义装饰器
Row(){
Text(`Message is ${$$.message}`)
}
}
// 2. 按值传递
@Builder function ValueBuilder(message : string) {
Row() {
Text(`message is ${message}`)
}
}
@Entry // 装饰器
@Component // 装饰器
struct Index { // 使用 struct 关键字定义组件
@State message: string = 'hello' // @State表示组件中的状态变量,状态变量变化会触发UI刷新
@State count: number = 1
build() {
Row() { // 行
Column() { // 列
Text(this.message + this.count)
.fontSize(50) // 属性方法
.fontWeight(FontWeight.Bold)
Button('点我试试')
.onClick(() => {
this.count ++
this.message = '你好'
})
.fontColor('#000')
MessageBuilder({message: this.message}) // 传递参数的引用
ValueBuilder(this.message) // 按值传递,无响应式
}
.width('100%')
}
.height('100%')
}
}
效果如下:
ArkTS采用声明方式组合和扩展组件来描述应用程序的UI,同时提供了基本的属性、事件和子组件配置方法
1. 创建组件:
无参数创建:对于没有必选构造参数的组件,可以直接在组件后面使用空括号,例如 Divider()
。
有参数创建:如果组件包含构造参数,可以在括号内配置相应参数,例如 Image('https://xyz/test.jpg')
。4
Column() {
Text('item 1')
Divider()
Text('item 2')
}
2. 配置属性:
使用属性方法通过“.”链式调用配置系统组件的样式和其他属性,建议每个属性方法单独写一行,如设置字体大小、宽度、高度等。
Text('test')
.fontSize(12)
3. 配置事件:
通过事件方法以“.”链式调用配置系统组件支持的事件,可使用lambda表达式、匿名函数表达式或组件的成员函数来定义
Button('Click me')
.onClick(() => {
this.myText = 'ArkUI';
})
在ArkUI中,UI显示的内容由组件构成,其中框架直接提供的组件称为系统组件,而开发者定义的组件则被称为自定义组件。在进行UI界面开发时,通常需要考虑代码的可复用性、业务逻辑与UI的分离以及后续版本演进等因素。因此,将UI和部分业务逻辑封装成自定义组件是不可或缺的能力。
自定义组件具有以下特点:
可组合: 允许开发者组合使用系统组件及其属性和方法。
可重用: 自定义组件可以被其他组件重用,作为不同实例在不同的父组件或容器中使用。
数据驱动UI更新: 通过状态变量的改变来驱动UI的刷新。
以下示例展示了自定义组件的基本用法:
@Component
struct GreetingComponent {
@State greeting: string = 'Hello, World!';
build() {
// GreetingComponent自定义组件组合系统组件Row和Text
Row() {
Text(this.greeting)
.onClick(() => {
// 状态变量greeting的改变驱动UI刷新
this.greeting = 'Hello, ArkUI!';
});
}
}
}
GreetingComponent
可以在其他自定义组件的 build()
函数中多次创建,实现自定义组件的重用。
@Entry
@Component
struct ParentComponent {
build() {
Column() {
Text('ArkUI Greetings');
GreetingComponent({ greeting: 'Hello, World!' });
Divider();
GreetingComponent({ greeting: '你好!' });
}
}
}
自定义组件的结构:
概念 | 描述 |
---|---|
struct | 基于 struct 实现自定义组件,结构为 struct + 自定义组件名 + {...} 。实例化时可以省略 new 。 |
@Component | 装饰器,仅能装饰使用 struct 关键字声明的数据结构。被装饰后的结构具备组件化能力,需要实现 build 方法描述UI。一个 struct 只能被一个 @Component 装饰。 |
build() 函数 | 用于定义自定义组件的声明式 UI 描述。自定义组件必须实现 build() 函数。 |
@Entry | 装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用一个 @Entry 装饰的自定义组件。可以接受一个可选的 LocalStorage 参数。 |
示例代码:
// 自定义组件基本结构
@Component
struct MyComponent {
build() {
// 在这里描述组件的UI结构
}
}
// @Entry装饰的自定义组件作为UI页面入口
@Entry
@Component
struct MainComponent {
build() {
// UI页面的主要组件结构
}
}
成员函数/变量
自定义组件除了必须要实现build()函数外,还可以实现其他成员函数,成员函数具有以下约束:
自定义组件可以包含成员变量,成员变量具有以下约束:
build()函数
@Entry装饰的自定义组件,必须要有且仅有一个 build() 函数,且必须为容器组件,其中ForEach禁止作为根节点,他会产生多节点。
@Component装饰的自定义组件,其build()函数下的根节点唯一且必要,可以为非容器组件,其中ForEach禁止作为根节点。
代码示例:
@Entry
@Component
struct MyComponent {
build() {
// 根节点唯一且必要,必须为容器组件
Row() {
ChildComponent()
}
}
}
@Component
struct ChildComponent {
build() {
// 根节点唯一且必要,可为非容器组件
Image('test.jpg')
}
}
在 build()
函数内,不允许以下几点:
build() {
// 反例:不允许声明本地变量
let a: number = 1;
// 反例:不允许console.info
console.info('print debug log');
// 反例:不允许本地作用域
{
...
}
// 不允许switch语法,如果需要使用条件判断,请使用if。反例如下。
switch (expression) {
case 1:
Text('...')
break;
case 2:
Image('...')
break;
default:
Text('...')
break;
}
// 不允许使用表达式,反例如下。
(this.aVar > 10) ? Text('...') : Image('...')
}
另外:
不允许调用除了被@Builder装饰以外的方法,允许系统组件的参数是TS方法的返回值
@Component
struct ParentComponent {
doSomeCalculations() {
}
calcTextValue(): string {
return 'Hello World';
}
@Builder doSomeRender() {
Text(`Hello World`)
}
build() {
Column() {
// 反例:不能调用没有用@Builder装饰的方法
this.doSomeCalculations();
// 正例:可以调用
this.doSomeRender();
// 正例:参数可以为调用TS方法的返回值
Text(this.calcTextValue())
}
}
}
自定义组件通过“.”链式调用的形式设置通用样式。
@Component
struct MyComponent2 {
build() {
Button(`Hello World`)
}
}
@Entry
@Component
struct MyComponent {
build() {
Row() {
MyComponent2() // 为自定义组件添加通用样式
.width(200)
.height(300)
.backgroundColor(Color.Red)
}
}
}
可以由一个或者多个自定义组件组成,@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry装饰的组件才可以调用页面的生命周期。
图示:
图片来源:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-page-custom-components-lifecycle-0000001524296665-V2
生命周期接口 | 描述 |
---|---|
onPageShow | 每次页面显示时触发。 |
onPageHide | 每次页面隐藏时触发一次。 |
onBackPress | 当用户点击返回按钮时触发。 |
aboutToAppear | 组件即将出现时回调,具体时机为在创建新实例后,在执行 build() 函数之前执行。 |
aboutToDisappear | 在自定义组件即将析构销毁时执行。 |
示例代码:
// 包含两个自定义组件,一个是被@Entry装饰的MyComponent,也是页面的入口组件,即页面的根节点;一个是Child,是MyComponent的子组件。
// 只有@Entry装饰的节点才可以生效页面的生命周期方法,所以MyComponent中声明了当前Index页面的页面生命周期函数。MyComponent和其子组件Child也同时也声明了组件的生命周期函数。
import router from '@ohos.router';
@Entry
@Component
struct MyComponent {
@State showChild: boolean = true;
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onPageShow() {
console.info('Index onPageShow');
}
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onPageHide() {
console.info('Index onPageHide');
}
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onBackPress() {
console.info('Index onBackPress');
}
// 组件生命周期
aboutToAppear() {
console.info('MyComponent aboutToAppear');
}
// 组件生命周期
aboutToDisappear() {
console.info('MyComponent aboutToDisappear');
}
build() {
Column() {
// this.showChild为true,创建Child子组件,执行Child aboutToAppear
if (this.showChild) {
Child()
}
// this.showChild为false,删除Child子组件,执行Child aboutToDisappear
Button('create or delete Child').onClick(() => {
this.showChild = false;
})
// push到Page2页面,执行onPageHide
Button('push to next page')
.onClick(() => {
router.pushUrl({ url: 'pages/Page2' });
})
}
}
}
@Component
struct Child {
@State title: string = 'Hello World';
// 组件生命周期-在自定义组件即将析构销毁时执行
aboutToDisappear() {
console.info('[lifeCycle] Child aboutToDisappear')
}
// 组件生命周期-组件即将出现时回调
aboutToAppear() {
console.info('[lifeCycle] Child aboutToAppear')
}
build() {
Text(this.title).fontSize(50).onClick(() => {
this.title = 'Hello ArkUI';
})
}
}
@Builder装饰的函数也称为“自定义构建函数”。
自定义组件内自定义构建函数
定义的语法:
@Builder MyBuilderFunction({ ... })
使用方法:
this.MyBuilderFunction({ ... })
示例如下:
@Builder function GlobalBuilder() {
Text('我是全局装饰器')
}
@Component
struct Children1 { // 子组件
@Builder DoNothing(){}
@BuilderParam aBuilder: () => void = this.DoNothing; // 定义局部的装饰器
@BuilderParam bBuilder: () => void = GlobalBuilder; // 定义全局的装饰器
build(){
Column() {
GlobalBuilder()
}
}
}
@Component
struct Children2 {
@BuilderParam paramsBuilder: () => void; // 声明装饰器
build(){
Column() {
this.paramsBuilder()
}
}
}
@Component
@Entry
struct Index {
@Builder componentBuilder() {
Text('我是父组件的 builder')
}
build() {
Column() {
Children1()
Children2({ paramsBuilder: this.componentBuilder}) // 传入一个 builder 给子组件
}.width('100%')
}
}
尾随闭包初始化组件
在初始化自定义组件时,紧跟一个大括号“{}”形成尾随闭包场景,将尾随闭包内的内容看做@Builder装饰的函数传给@BuilderParam
// 尾随闭包初始化组件
@Component
struct CustomContainer {
@Prop header: string;
@BuilderParam closer: () => void
build() {
Column() {
Text(this.header)
.fontSize(30)
this.closer()
}
}
}
@Builder function specificParam(label1: string, label2: string) {
Column() {
Text(label1)
.fontSize(30)
Text(label2)
.fontSize(30)
}
}
@Entry
@Component
struct Index {
@State text: string = 'header';
build() {
Column() {
// 在创建CustomContainer时,通过其后紧跟一个大括号“{}”形成尾随闭包
// 用内部的 this.text 作为参数
CustomContainer({ header: this.text }) {
Column() {
specificParam('testA', 'testB')
}.backgroundColor(Color.Yellow)
.onClick(() => {
this.text = 'changeHeader';
})
}
}
}
}
如果每个组件的样式都需要单独设置,在开发过程中会出现大量代码在进行重复样式设置,虽然可以复制粘贴,但为了代码简洁性和后续方便维护,我们推出了可以提炼公共样式进行复用的装饰器@Styles。
@Styles装饰器可以将多条样式设置提炼成一个方法,直接在组件声明的位置调用。通过@Styles装饰器可以快速定义并复用自定义样式。用于快速定义并复用自定义样式。
使用方法:
// 反例: @Styles不支持参数
// @Styles function globalFancy (value: number) {
// .width(value)
// }
// @Styles可以定义在组件内或全局,在全局定义时需在方法名前面添加function关键字,
// 组件内定义时则不需要添加function关键字。
// 定义在组件内的@Styles可以通过this访问组件的常量和状态变量,
// 并可以在@Styles里通过事件来改变状态变量的值
// 组件内@Styles的优先级高于全局@Styles。
@Component
@Entry
struct Index {
@State heightValue: number = 100
@Styles fancy() {
.height(this.heightValue)
.backgroundColor(Color.Yellow)
.onClick(() => {
this.heightValue = 200
})
}
build() {
Column(){
Row(){
Text('自定义样式,点我也能改变样式')
.fancy()
}
Divider()
Row(){
Text('点我也一样,也会跟着改变样式')
.fancy()
}
}
}
}
使用规则
// 和@Styles不同,@Extend仅支持定义在全局,不支持在组件内部定义。
// 和@Styles不同,@Extend支持封装指定的组件的私有属性和私有事件和预定义相同组件的@Extend的方法。
// 和@Styles不同,@Extend装饰的方法支持参数,开发者可以在调用时传递参数,调用遵循TS方法传值调用。
// @Extend的参数可以为状态变量,当状态变量改变时,UI可以正常的被刷新渲染
@Extend(Text) function fancy (fontSize: number) { // 只给 Text 继承了 fancy
.fontColor(Color.Red)
.fontSize(fontSize)
}
// @Extend装饰的方法的参数可以为function,作为Event事件的句柄。
@Extend(Text) function makeMeClick(onClick: () => void) {
.backgroundColor(Color.Blue)
.onClick(onClick)
}
@Entry
@Component
struct Index {
@State label: string = 'Hello World';
@State fontSizeValue: number = 58;
onClickHandler() {
this.label = 'Hello ArkUI';
this.fontSizeValue = 108
}
build(){
Column(){
Row({space: 10}) {
Text('测试')
.fancy(this.fontSizeValue) // 传入可响应数据,后续函数执行,字号也会发生变化
// Span('span无效')
// .fancy() // Property 'fancy' does not exist on type 'SpanAttribute'.
}
Row({ space: 10 }) {
Text(`${this.label}`)
.makeMeClick(this.onClickHandler.bind(this)) // bind 绑定当前作用域
.fancy(109)
}
}
}
}
// 多态样式
// stateStyles是属性方法,可以根据UI内部状态来设置样式,类似于css伪类,但语法不同。ArkUI提供以下四种状态:
//
// 1. focused:获焦态。
// 2. normal:正常态。
// 3. pressed:按压态。
// 4. disabled:不可用态。
// 基础场景
// 下面的示例展示了stateStyles最基本的使用场景。Button处于第一个组件,默认获焦,
// 生效focused指定的粉色样式。按压时显示为pressed态指定的黑色。
// 如果在Button前再放一个组件,使其不处于获焦态,就会生效normal态的黄色。
// Styles也可和stateStyles联合使用
@Entry
@Component
struct Index {
@Styles normalStyle() {
.backgroundColor(Color.Gray)
}
@Styles pressedStyle() {
.backgroundColor(Color.Red)
}
build() {
Column() {
Button('Click me')
.stateStyles({
focused: {
.backgroundColor(Color.Pink)
},
pressed: {
.backgroundColor(Color.Black)
},
normal: {
.backgroundColor(Color.Yellow)
}
})
Column() {
Text('Text1')
.fontSize(50)
.fontColor(Color.White)
.stateStyles({
normal: this.normalStyle,
pressed: this.pressedStyle,
})
}
}.margin('30%')
}
}
变量必须被装饰器装饰才能成为状态变量,状态变量的改变才能导致 UI 界面重新渲染
概念 | 描述 |
---|---|
状态变量 | 被状态装饰器装饰的变量,改变会引起UI的渲染更新。 |
常规变量 | 没有状态的变量,通常应用于辅助计算。它的改变永远不会引起UI的刷新。 |
数据源/同步源 | 状态变量的原始来源,可以同步给不同的状态数据。通常意义为父组件传给子组件的数据。 |
命名参数机制 | 父组件通过指定参数传递给子组件的状态变量,为父子传递同步参数的主要手段。示例:CompA: ({ aProp: this.aProp })。 |
从父组件初始化 | 父组件使用命名参数机制,将指定参数传递给子组件。本地初始化的默认值在有父组件传值的情况下,会被覆盖。 |
Components部分的装饰器为组件级别的状态管理,Application部分为应用的状态管理。可以通@StorageLink/@LocalStorageLink和@StorageProp/@LocalStorageProp实现应用和组件状态的双向和单向同步。图中箭头方向为数据同步方向,单箭头为单向同步,双箭头为双向同步。
图片来源:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-state-management-overview-0000001524537145-V2
状态管理分三种:
组件级别,即components级别:
应用级别,即 Application 级别:
其他状态管理功能
@Watch用于监听状态变量的变化。
$$ 运算符:给内置组件提供TS变量的引用,使得TS变量和内置组件的内部状态保持同步。
@State装饰的变量,是私有的,只能从组件内部访问,在声明时必须指定其类型和本地初始化。初始化也可选择使用命名参数机制从父组件完成初始化。
示例如下:
@Entry
@Component
struct Index {
build() {
Column() {
Parent()
}
}
}
@Component
struct MyComponent {
@State count: number = 0;
private increaseBy: number = 1;
build() {
Column(){
Row() {
Text(`${this.count}`)
.fontSize(108)
}
Row() {
Button('click me hh').onClick(() => {
this.count += this.increaseBy
})
}
}
}
}
@Component
struct Parent {
build() {
Column() {
// 从父组件初始化,覆盖本地定义的默认值
MyComponent({ count: 2, increaseBy: 2 })
}
}
}
效果如下:
3.1.2 能观察的数据类型
@State count: number = 0;
this.count = 1; // 这种修改可以被观察到
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;
}
}
@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';
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));
可以使用 @Prop 定义要从父级接受的变量。注意以下两点:
@Prop变量可以在子组件内修改,但修改后的变化不会同步回父组件中。
万但当父组件中的数据源更改时,与之相关的@Prop装饰的变量都会自动更新。如果子组件已经在本地修改了@Prop装饰的相关变量值,而在父组件中对应的@State装饰的变量被修改后,子组件本地修改的@Prop装饰的相关变量值将被覆盖。
允许装饰的变量的类型:string、number、boolean、enum类型。
不允许的类型:any,undefined和null。
@Prop 初始化规则:
图示:
图片来源:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-prop-0000001473537702-V2
代码示例:
// 框架行为
// 要理解@Prop变量值初始化和更新机制,有必要了解父组件和拥有@Prop变量的子组件初始渲染和更新流程。
//
// 初始渲染:
// 执行父组件的build()函数将创建子组件的新实例,将数据源传递给子组件;
// 初始化子组件@Prop装饰的变量。
// 更新:
// 子组件@Prop更新时,更新仅停留在当前子组件,不会同步回父组件;
// 当父组件的数据源更新时,子组件的@Prop装饰的变量将被来自父组件的数据源重置,所有@Prop装饰的本地的修改将被父组件的更新覆盖。
// 父组件@State到子组件@Prop简单数据类型同步
// 以下示例是@State到子组件@Prop简单数据同步,
// 父组件ParentComponent的状态变量countDownStartValue初始化子组件CountDownComponent中@Prop装饰的count,点击“Try again”,
// count的修改仅保留在CountDownComponent,不会同步给父组件ParentComponent。
@Component
struct CountDownComponent {
@Prop count: number;
costOfOneAttempt: number = 1;
build() {
Column() {
if (this.count > 0) {
Text(`儿子还有 ${this.count} 个萝卜头`)
} else {
Text('吃完了,要挨打了')
}
// @Prop装饰的变量不会同步给父组件
Button(`儿子偷吃了${this.costOfOneAttempt}个萝卜头`).onClick(() => {
this.count -= this.costOfOneAttempt;
})
}
}
}
@Entry
@Component
struct ParentComponent {
@State countDownStartValue: number = 10;
build() {
Column() {
Text(`老爸还有 ${this.countDownStartValue} 个萝卜头`)
// 父组件的数据源的修改会同步给子组件
Button(`一起买了1个萝卜头`).onClick(() => {
this.countDownStartValue += 1;
})
// 父组件的修改会同步给子组件
Button(`一起吃了1个萝卜头`).onClick(() => {
this.countDownStartValue -= 1;
})
CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 })
}
}
}
效果如下:
可以使用 @Link 装饰器进行数据双向同步:
@Link初识化规则:
图示:
图片来源:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-link-0000001524297305-V2
代码示例:
class GreenButtonState {
width: number = 0;
constructor(width: number) {
this.width = width;
}
}
// class类型的子组件GreenButton,通过@Link装饰变量greenButtonState同步到父组件Index中的@State变量greenButtonState
@Component
struct GreenButton {
@Link greenButtonState: GreenButtonState;
build() {
Button('绿色按钮')
.width(this.greenButtonState.width)
.height(150.0)
.fontWeight(FontWeight.Bold)
.backgroundColor('#ff81c123')
.onClick(() => {
if (this.greenButtonState.width < 700) {
// 更新class的属性,变化可以被观察到同步回父组件
this.greenButtonState.width += 125;
} else {
// 更新class,变化可以被观察到同步回父组件
this.greenButtonState = new GreenButtonState(100);
}
})
}
}
// 简单类型的子组件YellowButton,通过@Link装饰变量yellowButtonState同步到父组件Index中的@State变量yellowButtonProp
@Component
struct YellowButton {
@Link yellowButtonState: number;
build() {
Button('黄色按钮')
.width(this.yellowButtonState)
.height(150.0)
.fontWeight(FontWeight.Bold)
.backgroundColor('#ffdb944d')
.onClick(() => {
// 子组件的简单类型可以同步回父组件
this.yellowButtonState += 50.0;
})
}
}
// 父组件Index,初始化并管理子组件的状态变量
@Entry
@Component
struct Index {
@State greenButtonState: GreenButtonState = new GreenButtonState(300);
@State yellowButtonProp: number = 100;
build() {
Column() {
// 按钮:父组件修改greenButtonState,同步到GreenButton子组件
Button('父视图: 设置 绿色按钮')
.onClick(() => {
this.greenButtonState.width = (this.greenButtonState.width < 700) ? this.greenButtonState.width + 100 : 100;
})
// 按钮:父组件修改yellowButtonProp,同步到YellowButton子组件
Button('父视图: 设置 黄色按钮')
.onClick(() => {
this.yellowButtonProp = (this.yellowButtonProp < 700) ? this.yellowButtonProp + 100 : 100;
})
// 子组件:GreenButton,通过@Link同步更新父组件@State的greenButtonState
GreenButton({ greenButtonState: $greenButtonState })
// 子组件:YellowButton,通过@Link同步更新父组件@State的yellowButtonProp
YellowButton({ yellowButtonState: $yellowButtonProp })
}
}
}
图示:
为了避免在组件中多次传递变量,推出了一种使某些变量能被所有后代组件使用的装饰器 @Provide,后代组件可以使用 @Consume去获取 @Provide 的值,而@State和@Link 只能在父子组件中传递。
// 通过相同的变量名绑定
@Provide a: number = 0;
@Consume a: number;
// 通过相同的变量别名绑定
@Provide('a') b: number = 0;
@Consume('a') c: number;
@Provide 和 @Consume 是双向同步的。
代码示例:
@Component
struct CompD {
// @Consume装饰的变量通过相同的属性名绑定其祖先组件CompA内的@Provide装饰的变量
@Consume tickets: number;
build() {
Column() {
Text(`评审投票数(${this.tickets})`)
Button(`评审投票数(${this.tickets}),+1`)
.onClick(() => this.tickets += 1)
}
.width('50%')
}
}
@Component
struct CompC {
build() {
Row({ space: 5 }) {
CompD()
CompD()
}
}
}
@Component
struct CompB {
build() {
CompC()
}
}
@Entry
@Component
struct Index {
// @Provide装饰的变量reviewVotes由入口组件CompA提供其后代组件
@Provide tickets: number = 0;
build() {
Column() {
Button(`评审投票数(${this.tickets}),+1`)
.onClick(() => this.tickets += 1)
CompB()
}
}
}
图示:
对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink装饰器。
@ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步:
初始化图示:
图片来源:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-observed-and-objectlink-0000001473697338-V2#section2976114355019
以二维数组监听为例:
@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 Index {
@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('!');
}
})
}
}
}
效果如下:
ArkTS可以实现多种应用状态管理的能力,具体表现在以下几点:
UIAblility
内,也可以用于页面间状态共享有关 LocalStorage 介绍:
LocalStorage
实例,这些实例可以在页面内共享,也可以通过 GetShared
接口在 UIAbility
中创建的实例实现跨页面和 UIAbility
内的共享。@Entry
装饰的 @Component
实例可以被分配一个 LocalStorage
实例,而该组件的所有子组件实例将自动获得对该 LocalStorage
实例的访问权限。@Component
装饰的组件最多可以访问一个 LocalStorage
实例和 AppStorage
,而未被 @Entry
装饰的组件无法独立分配 LocalStorage
实例,只能接受父组件通过 @Entry
传递来的 LocalStorage
实例。LocalStorage
实例在组件树上可以被分配给多个组件,而其中的所有属性都是可变的。介绍两个用于属性绑定的装饰器:
@LocalStorageProp(key)
是一个装饰器,用于在 ArkUI 组件框架中建立自定义组件的属性与 LocalStorage
中特定键对应属性之间的单向数据同步。以下是一些关键信息:
初始化与绑定: 在自定义组件初始化时,被 @LocalStorageProp(key)
装饰的变量通过给定的 key
绑定到相应的 LocalStorage
属性上,完成初始化。本地初始化是必要的,因为不能保证在组件初始化之前 LocalStorage
中是否存在给定的 key
。
本地修改和同步: 对于使用 @LocalStorageProp(key)
装饰的变量,本地的修改是允许的。但是需要注意,本地的修改永远不会同步回 LocalStorage
中。相反,如果 LocalStorage
中给定 key
的属性发生改变,这个改变会被同步给被 @LocalStorageProp(key)
装饰的变量,并覆盖本地的修改。
@LocalStorageLink
是一个装饰器,用于在 ArkUI 组件框架中建立自定义组件的状态变量与 LocalStorage
中特定键对应属性之间的双向数据同步。以下是相关信息的概述:
@LocalStorageLink(key)
与 LocalStorage
中给定 key
对应的属性建立双向数据同步。这包括本地的修改会同步回 LocalStorage
中,以及 LocalStorage
中的修改会被同步到所有绑定了相同 key
的属性上。@LocalStorageLink
不支持从父节点初始化,只能从 LocalStorage
中的 key
对应的属性初始化。如果没有对应的 key
,则使用本地默认值初始化。可用于初始化 @State
、@Link
、@Prop
、@Provide
,但不支持组件外访问语法示例:
let storage = new LocalStorage({ 'KeyA': 47 }); // 创建新实例并使用给定对象初始化
let keyA = storage.get('KeyA'); // keyA == 47
let link1 = storage.link('KeyA'); // link1.get() == 47
let link2 = storage.link('KeyA'); // link2.get() == 47
let prop = storage.prop('KeyA'); // prop.get() == 47
link1.set(48); // 双向绑定 link1.get() == link2.get() == prop.get() == 48
prop.set(1); // 单向绑定 prop.get() == 1;但是 link1.get() == link2.get() == 48
link1.set(49); // 双向绑定 link1.get() == link2.get() == prop.get() == 49
案例理解:
// 创建新实例并使用给定对象初始化
let localStorageInstance = new LocalStorage({ 'PropertyA': 47 });
@Component
struct ChildComponent {
// 与LocalStorage中的'PropertyA'属性建立双向绑定
@LocalStorageLink('PropertyA') linkedProperty2: number = 1;
// 与LocalStorage中的'PropertyA'属性建立单向绑定
@LocalStorageProp('PropertyA') linkedProperty3: number = 1;
build() {
Row() {
Button(`子组件的值 ${this.linkedProperty2}`)
// 更改将同步至LocalStorage中的'PropertyA'以及ParentComponent.linkedProperty1
.onClick(() => this.linkedProperty2 += 1)
// Local 中值变化,linkedProperty3 也会变,反之不会
Button(`子组件的值-单向 Prop ${this.linkedProperty3}`)
.onClick(() => this.linkedProperty3 += 1)
}
}
}
// 使LocalStorage可被@Component组件访问
@Entry(localStorageInstance)
@Component
struct ParentComponent {
// @LocalStorageLink变量装饰器与LocalStorage中的'PropertyA'属性建立双向绑定
@LocalStorageLink('PropertyA') linkedProperty1: number = 1;
build() {
Column({ space: 15 }) {
Button(`父组件的值 ${this.linkedProperty1}`) // 初始化值从LocalStorage中获取,因为'PropertyA'已经初始化为47
.onClick(() => this.linkedProperty1 += 1)
// @Component子组件自动获得对ParentComponent LocalStorage实例的访问权限。
ChildComponent()
}
}
}
效果如下:
概述
AppStorage是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。AppStorage是应用级的全局状态共享,相当于整个应用的“中枢”,持久化数据PersistentStorage和 环境变量Environment都是通过AppStorage的中转,才可以和UI交互。AppStorage中的属性可以被双向同步,数据可以是存在于本地或远程设备上,并具有不同的功能,比如数据持久化。
基本用法:
// 在AppStorage中设置或创建 'PropA' 属性,初始值为 47
AppStorage.SetOrCreate('PropA', 47);
let localStorageInstance: LocalStorage = new LocalStorage({ 'PropA': 17 });
// 从AppStorage中获取 'PropA' 的值,此时 propA 在 AppStorage 中为 47,在 LocalStorage 中为 17
let propAFromAppStorage: number = AppStorage.Get('PropA');
// 使用AppStorage的Link方法创建两个链接(link1和link2),以同步 'PropA' 的值
let link1: SubscribedAbstractProperty<number> = AppStorage.Link('PropA');
let link2: SubscribedAbstractProperty<number> = AppStorage.Link('PropA');
// 使用AppStorage的Prop方法创建一个属性(prop),以单向同步 'PropA' 的值
let prop: SubscribedAbstractProperty<number> = AppStorage.Prop('PropA');
// 修改 link1,演示双向绑定的效果,所有其他绑定到相同键的变量都同步更新
link1.set(48); // link1.get() == link2.get() == prop.get() == 48
// 修改属性 prop,演示单向绑定的效果,只有属性本身更新,其他变量不受影响
prop.set(1); // prop.get() == 1;但是 link1.get() == link2.get() == 48
// 再次修改 link1,验证双向绑定,所有绑定到相同键的变量都同步更新
link1.set(49); // link1.get() == link2.get() == prop.get() == 49
// 使用 'PropA' 的值从 LocalStorage 中获取,此时为 17
let valueFromLocalStorage: number = localStorageInstance.get('PropA');
// 设置 'PropA' 的值为 101
localStorageInstance.set('PropA', 101);
// 从 LocalStorage 中获取 'PropA' 的值,此时为 101
let valueFromLocalStorageAfterSet: number = localStorageInstance.get('PropA');
// 从 AppStorage 中获取 'PropA' 的值,此时为 49
let valueFromAppStorage: number = AppStorage.Get('PropA');
// 获取 link1 的值,此时为 49
let valueFromLink1: number = link1.get();
// 获取 link2 的值,此时为 49
let valueFromLink2: number = link2.get();
// 获取 prop 的值,此时为 49
let valueFromProp: number = prop.get();
LocalStorage和AppStorage都是运行时的内存,但是在应用退出再次启动后,依然能保存选定的结果,是应用开发中十分常见的现象,这就需要用到PersistentStorage。
注意:
代码示例:
PersistentStorage.PersistProp('userScore', 100);
@Entry
@Component
struct Game {
@State message: string = 'Welcome to the Game'
@StorageLink('userScore') userScore: number = 50
build() {
Row() {
Column() {
Text(this.message)
Text(`你的得分: ${this.userScore}`)
.onClick(() => {
this.userScore += 10;
})
}
}
}
}
Environment设备环境查询用于查询设备运行环境参数,是ArkUI框架在应用程序启动时创建的单例对象,它为AppStorage提供了一系列描述应用程序运行状态的属性。Environment的所有属性都是不可变的(即应用不可写入),所有的属性都是简单类型。
使用场景
Environment.EnvProp
将设备的语言设置为英语 'zh'
。然后,@StorageProp
将设备语言与AppStorage
中的 deviceLanguage
建立了单向同步。
// 将设备的语言code存入AppStorage
Environment.EnvProp('deviceLanguage', 'zh');
@Entry
@Component
struct Main {
// 使用@StorageProp链接到Component中
@StorageProp('deviceLanguage') selectedLanguage: string = 'en';
build() {
Row() {
Column() {
Text('Device Language:')
Text(this.selectedLanguage)
}
}
}
}
// 应用逻辑使用Environment
// 从AppStorage获取单向绑定的languageCode的变量
const lang: SubscribedAbstractProperty<string> = AppStorage.Prop('deviceLanguage');
if (lang.get() === 'zh') {
console.info('你好');
} else {
console.info('Hello!');
}
@Watch用于监听状态变量的变化,当状态变量变化时,@Watch的回调方法将被调用。@Watch在ArkUI框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范。当在严格相等为false的情况下,就会触发@Watch的回调。
观察变化和行为表现
注意点:
示例:
@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 })
}
}
}
$$运算符为系统内置组件提供TS变量的引用,使得TS变量和系统内置组件的内部状态保持同步。
内部状态具体指什么取决于组件。
@Entry
@Component
struct bindPopupPage {
@State customPopup: boolean = false;
build() {
Column() {
Button('Popup')
.margin(20)
.onClick(() => {
this.customPopup = !this.customPopup
})
.bindPopup($$this.customPopup, {
message: 'showPopup'
})
}
}
}
tch方法在自定义组件的属性变更之后同步执行;
3. 如果在@Watch的方法里改变了其他的状态变量,也会引起状态变更和@Watch的执行;
4. 在第一次初始化的时候,@Watch装饰的方法不会被调用,即认为初始化不是状态变量的改变。只有在后续状态改变时,才会调用@Watch回调方法。
注意点:
示例:
@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 })
}
}
}
$$运算符为系统内置组件提供TS变量的引用,使得TS变量和系统内置组件的内部状态保持同步。
内部状态具体指什么取决于组件。
@Entry
@Component
struct bindPopupPage {
@State customPopup: boolean = false;
build() {
Column() {
Button('Popup')
.margin(20)
.onClick(() => {
this.customPopup = !this.customPopup
})
.bindPopup($$this.customPopup, {
message: 'showPopup'
})
}
}
}
对于 UI 渲染,可以基于数据结构选择一些内置方法(例如:ForEach)快速渲染 UI 结构。
ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,使用if、else和else if渲染对应状态下的UI内容。
条件渲染语句允许基于条件在组件内构建不同子组件,支持if、else、else if语句,条件可以使用状态变量,但需遵循父子组件关系规则,确保每个分支内创建至少一个组件,且子组件类型和数量需符合父组件限制。
代码示例:
@Entry
@Component
struct ViewA {
@State count: number = 0;
build() {
Column() {
Text(`count=${this.count}`)
if (this.count >= 0) {
Text(`count 为正数`)
.fontColor(Color.Green)
} else {
Text(`count 为负数`)
.fontColor(Color.Red)
}
Button('增 count')
.onClick(() => {
this.count++;
})
Button('减 count')
.onClick(() => {
this.count--;
})
}
}
}
图示:
必须使用数组(允许空数组),设置的循环函数不允许改变源数组。
ForEach组件接受arr
属性作为数组输入,必须是数组类型,允许为空数组,可用返回数组的函数。数组为空时,不会创建子组件。
itemGenerator
是必需的,是一个lambda函数,根据数组中的每个数据项生成一个或多个子组件。生成的子组件类型必须符合ForEach的父容器组件的规定。
可选的keyGenerator
是一个匿名函数,用于生成数组中每个数据项的唯一键值。虽然可选,但建议提供以优化性能和识别数组更改。在没有提供键值生成器的情况下,反转数组可能导致ForEach中的所有节点重建。
简单的示例:
@Entry
@Component
struct MyComponent {
@State arr: number[] = [10, 20, 30];
build() {
Column({ space: 5 }) {
Button('翻转数组')
.onClick(() => {
this.arr.reverse();
})
ForEach(this.arr, (item: number) => {
Text(`此项值: ${item}`).fontSize(18)
Divider().strokeWidth(2)
}, (item: number) => item.toString())
}
}
}
效果如下:
配合 @ObjectLInk 的 ForEach 示例
let NextID: number = 0;
@Observed
class MyCounter {
public id: number;
public c: number;
constructor(c: number) {
this.id = NextID++;
this.c = c;
}
}
@Component
struct CounterView {
@ObjectLink counter: MyCounter;
label: string = '计数器视图';
build() {
Button(`计数器视图 [${this.label}] this.counter.c=${this.counter.c} +1`)
.width(400).height(50)
.onClick(() => {
this.counter.c += 1;
})
}
}
@Entry
@Component
struct MainView {
@State firstIndex: number = 0;
@State counters: Array<MyCounter> = [new MyCounter(0), new MyCounter(0), new MyCounter(0),
new MyCounter(0), new MyCounter(0)];
build() {
Column() {
ForEach(this.counters.slice(this.firstIndex, this.firstIndex + 3),
(item) => {
CounterView({ label: `计数器项目 #${item.id}`, counter: item })
},
(item) => item.id.toString()
)
Button(`计数器:向上移动`)
.width(200).height(50)
.onClick(() => {
this.firstIndex = Math.min(this.firstIndex + 1, this.counters.length - 3);
})
Button(`计数器:向下移动`)
.width(200).height(50)
.onClick(() => {
this.firstIndex = Math.max(0, this.firstIndex - 1);
})
}
}
}
图示:
LazyForEach是一个用于按需迭代数据并创建组件的接口,适用于滚动容器以提高性能。它接受数据源、子组件生成函数和可选的键值生成函数作为参数。在每次迭代中,子组件生成函数生成一个子组件,并且键值生成函数可选地用于为数据项生成唯一的键值。
注意,键值生成器应确保生成的键值唯一,以避免框架忽略相同键值的UI组件。数据源接口(IDataSource)定义了操作数据的方法,包括获取总数、获取特定索引的数据以及注册和注销数据变化监听器。
同时,数据变化监听器(DataChangeListener)提供了各种通知方法,如重新加载数据、数据添加、数据移动、数据删除和数据变化。在使用LazyForEach时,需要注意它必须在支持懒加载的容器组件内使用,且生成的子组件必须符合容器组件的规定。
IDataSource类型说明
interface IDataSource {
totalCount(): number; // 获得数据总数
getData(index: number): any; // 获取索引值对应的数据
registerDataChangeListener(listener: DataChangeListener): void; // 注册数据改变的监听器
unregisterDataChangeListener(listener: DataChangeListener): void; // 注销数据改变的监听器
}
DataChangeListener类型说明
interface DataChangeListener {
onDataReloaded(): void; // 重新加载数据时调用
onDataAdded(index: number): void; // 添加数据时调用
onDataMoved(from: number, to: number): void; // 数据移动起始位置与数据移动目标位置交换时调用
onDataDeleted(index: number): void; // 删除数据时调用
onDataChanged(index: number): void; // 改变数据时调用
onDataAdd(index: number): void; // 添加数据时调用
onDataMove(from: number, to: number): void; // 数据移动起始位置与数据移动目标位置交换时调用
onDataDelete(index: number): void; // 删除数据时调用
onDataChange(index: number): void; // 改变数据时调用
}
懒加载示例:
// 实现基本的IDataSource以处理数据监听器
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = [];
// 获取数据总数
public totalCount(): number {
return 0;
}
// 获取特定索引的数据
public getData(index: number): any {
return undefined;
}
// 注册数据变化监听器
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('添加监听器');
this.listeners.push(listener);
}
}
// 注销数据变化监听器
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('移除监听器');
this.listeners.splice(pos, 1);
}
}
// 通知数据重新加载
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
});
}
// 通知数据添加
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
});
}
// 通知数据变化
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
});
}
// 通知数据删除
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
});
}
// 通知数据移动
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to);
});
}
}
class MyDataSource extends BasicDataSource {
private dataArray: string[] = [];
// 获取数据总数
public totalCount(): number {
return this.dataArray.length;
}
// 获取特定索引的数据
public getData(index: number): any {
return this.dataArray[index];
}
// 添加数据
public addData(index: number, data: string): void {
this.dataArray.splice(index, 0, data);
this.notifyDataAdd(index);
}
// 推送数据
public pushData(data: string): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
}
@Entry
@Component
struct MyComponent {
aboutToAppear() {
for (var i = 100; i >= 80; i--) {
this.data.pushData(`Hello ${i}`);
}
}
private data: MyDataSource = new MyDataSource();
build() {
List({ space: 3 }) {
// 使用LazyForEach按需迭代数据
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Text(item).fontSize(50)
.onAppear(() => {
console.info("出现:" + item);
});
}.margin({ left: 10, right: 10 });
}
.onClick(() => {
this.data.pushData(`Hello ${this.data.totalCount()}`);
});
}, item => item);
}.cachedCount(5); // 设置缓存的数据数量
}
}
图示:
文章参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-get-started-0000001504769321-V2