鸿蒙开发-ArkTS 语言

鸿蒙开发-ArkTS 语言

1. 初识 ArkTS 语言

ArkTS 是 HarmonyOS 优选主力开发语言。ArkTS 是基于 TS(TypeScript)扩展的一门语言,继承了 TS 的所以特性,是TS的超集。

主要是扩展了以下几个方面:

  1. 声明式UI描述和自定义组件:

    • ArkTS允许开发者使用声明式的方式描述用户界面(UI),使UI的结构和布局更直观易懂。
    • 提供了自定义组件的能力,开发者可以定义自己的组件,以便在应用中重复使用。
  2. 动态扩展UI元素的能力:

    • ArkTS支持动态扩展UI元素,使开发者能够灵活地向应用中添加新的UI元素。
  3. 多维度的状态管理机制:

    • ArkTS提供了多种方式来管理应用的状态。
    • 支持在组件内使用与UI相关联的数据,实现局部状态管理。
    • 支持在不同组件层级间传递数据,包括父子组件和爷孙组件之间的传递。
    • 全局状态管理允许在应用的整个范围内传递数据,甚至可以跨设备传递。
    • 提供只读的单向数据传递和可变更的双向数据传递,使开发者能够根据需要选择合适的数据传递形式。
  4. 渲染控制的能力:

    • ArkTS提供了灵活的渲染控制功能,使开发者能够根据应用的状态动态渲染UI内容。
    • 条件渲染允许根据不同的应用状态选择性地渲染UI内容。
    • 循环渲染支持从数据源中迭代获取数据,并在每次迭代中创建相应的组件。
    • 数据懒加载允许按需迭代数据,并在每次迭代中创建相应的组件,提高应用性能。

2. 基本语法

2.1 ArkTS的基本组成

概念描述
装饰器用于装饰类、结构、方法以及变量,赋予其特殊含义。例如,@Component 表示自定义组件,@Entry 表示入口组件,@State 表示状态变量。状态变量的变化会触发UI刷新。
UI描述使用声明式的方式描述UI的结构,通常体现在 build() 方法中的代码块。
自定义组件可复用的UI单元,可组合其他组件。被 @Component 装饰的 struct xxx 就是一个自定义组件。
系统组件ArkUI框架中默认内置的基础和容器组件,可以直接调用。 常见系统组件:ColumnTextDividerButton
属性方法组件可以通过链式调用配置多项属性的方法。例如,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 = 1build() {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%')}
}

效果如下:

gif1

2.2 声明式 UI

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';})
    

2.3 自定义组件

在ArkUI中,UI显示的内容由组件构成,其中框架直接提供的组件称为系统组件,而开发者定义的组件则被称为自定义组件。在进行UI界面开发时,通常需要考虑代码的可复用性、业务逻辑与UI的分离以及后续版本演进等因素。因此,将UI和部分业务逻辑封装成自定义组件是不可或缺的能力。

自定义组件具有以下特点:

  1. 可组合: 允许开发者组合使用系统组件及其属性和方法。

  2. 可重用: 自定义组件可以被其他组件重用,作为不同实例在不同的父组件或容器中使用。

  3. 数据驱动UI更新: 通过状态变量的改变来驱动UI的刷新。

以下示例展示了自定义组件的基本用法:

@Component
struct GreetingComponent {@State greeting: string = 'Hello, World!';build() {// GreetingComponent自定义组件组合系统组件Row和TextRow() {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.infoconsole.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())}}
}

2.4 自定义组件通用样式

自定义组件通过“.”链式调用的形式设置通用样式。

@Component
struct MyComponent2 {build() {Button(`Hello World`)}
}@Entry
@Component
struct MyComponent {build() {Row() {MyComponent2() // 为自定义组件添加通用样式.width(200).height(300).backgroundColor(Color.Red)}}
}

2.5. 页面和自定义组件生命周期

可以由一个或者多个自定义组件组成,@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry装饰的组件才可以调用页面的生命周期。

图示:

图片来源:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-page-custom-components-lifecycle-0000001524296665-V2

img01

生命周期接口描述
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 aboutToAppearif (this.showChild) {Child()}// this.showChild为false,删除Child子组件,执行Child aboutToDisappearButton('create or delete Child').onClick(() => {this.showChild = false;})// push到Page2页面,执行onPageHideButton('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';})}
}

2.6. @Builder装饰器:自定义构建函数

@Builder装饰的函数也称为“自定义构建函数”。

自定义组件内自定义构建函数

定义的语法:

@Builder MyBuilderFunction({ ... })

使用方法:

this.MyBuilderFunction({ ... })
  • 允许在自定义组件内定义一个或多个自定义构建函数,该函数被认为是该组件的私有、特殊类型的成员函数。
  • 自定义构建函数可以在所属组件的build方法和其他自定义构建函数中调用,但不允许在组件外调用。
  • 在自定义函数体中,this指代当前所属组件,组件的状态变量可以在自定义构建函数内访问。建议通过this访问自定义组件的状态变量而不是参数传递。

示例如下:

@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%')}
}

img02

尾随闭包初始化组件

在初始化自定义组件时,紧跟一个大括号“{}”形成尾随闭包场景,将尾随闭包内的内容看做@Builder装饰的函数传给@BuilderParam

// 尾随闭包初始化组件
@Component
struct CustomContainer {@Prop header: string;@BuilderParam closer: () => voidbuild() {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';})}}}
}

gif02

2.7 @Styles装饰器:定义组件重用样式

如果每个组件的样式都需要单独设置,在开发过程中会出现大量代码在进行重复样式设置,虽然可以复制粘贴,但为了代码简洁性和后续方便维护,我们推出了可以提炼公共样式进行复用的装饰器@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()}}}
}

gif03

2.8 @Extend装饰器:定义扩展组件样式

使用规则

  • 和@Styles不同,@Extend仅支持定义在全局,不支持在组件内部定义。
  • 和@Styles不同,@Extend支持封装指定的组件的私有属性和私有事件和预定义相同组件的@Extend的方法
// 和@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'. <tsCheck>}Row({ space: 10 }) {Text(`${this.label}`).makeMeClick(this.onClickHandler.bind(this)) // bind 绑定当前作用域.fancy(109)}}}
}

gif04

2.9 stateStyles:多态样式

// 多态样式
// 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%')}
}

gif05

3. 状态管理

变量必须被装饰器装饰才能成为状态变量,状态变量的改变才能导致 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

img03

状态管理分三种:

  1. 组件级别,即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的实例,应用于观察多层嵌套场景,和父组件的数据源构建双向同步。
  2. 应用级别,即 Application 级别:

    • AppStorage是应用程序中的一个特殊的单例LocalStorage对象,是应用级的数据库,和进程绑定,通过@StorageProp和@StorageLink装饰器可以和组件联动。
    • AppStorage是应用状态的“中枢”,需要和组件(UI)交互的数据存入AppStorage,比如持久化数据PersistentStorage和环境变量Environment。UI再通过AppStorage提供的装饰器或者API接口,访问这些数据;
    • 框架还提供了LocalStorage,AppStorage是LocalStorage特殊的单例。LocalStorage是应用程序声明的应用状态的内存“数据库”,通常用于页面级的状态共享,通过@LocalStorageProp和@LocalStorageLink装饰器可以和UI联动。
  3. 其他状态管理功能

    • @Watch用于监听状态变量的变化。

    • $$ 运算符:给内置组件提供TS变量的引用,使得TS变量和内置组件的内部状态保持同步。

3.1 组件级别的状态管理

3.1.1 @State 组件内状态

@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 })}}
}

效果如下:

gif06

3.1.2 能观察的数据类型

  1. 观察简单类型的变化:
    • 当状态变量是boolean、string、number类型时,修改这些变量的数值可以被观察到,从而引起UI的刷新。
@State count: number = 0;
this.count = 1; // 这种修改可以被观察到
  1. 观察类和对象类型的变化:
    • 对于class或Object类型,可以观察到自身的赋值变化以及其属性的赋值变化。但是嵌套属性的赋值观察不到。
class ClassA {public value: string;constructor(value: string) {this.value = value;}
}class Model {public value: string;public name: ClassA;constructor(value: string, a: ClassA) {this.value = value;this.name = a;}
}@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';
  1. 观察数组类型的变化:
    • 当状态变量是数组时,可以观察到数组本身的赋值以及对数组的添加、删除、更新操作。
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));
3.1.2 @Prop装饰器:父子单向通信

可以使用 @Prop 定义要从父级接受的变量。注意以下两点:

  1. @Prop变量可以在子组件内修改,但修改后的变化不会同步回父组件中。

  2. 万但当父组件中的数据源更改时,与之相关的@Prop装饰的变量都会自动更新。如果子组件已经在本地修改了@Prop装饰的相关变量值,而在父组件中对应的@State装饰的变量被修改后,子组件本地修改的@Prop装饰的相关变量值将被覆盖。

  3. 允许装饰的变量的类型:string、number、boolean、enum类型。

  4. 不允许的类型:any,undefined和null。

@Prop 初始化规则:

图示:

图片来源:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-prop-0000001473537702-V2

img04

代码示例:

// 框架行为
// 要理解@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 })}}
}

效果如下:

gif07

3.1.3 @Link: 父子双向通信

可以使用 @Link 装饰器进行数据双向同步:

  • @Link 是双向同步的装饰器,父组件中@State, @StorageLink和@Link 和子组件@Link可以建立双向数据同步,反过来也是可以的。
  • 允许装饰的类型有:Object、class、string、number、boolean、enum类型,同时类型必须被指定,且和双向绑定状态变量的类型相同。不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null。
  • 当装饰的数据类型为boolean、string、number时,可以同步观察到数值的变化;为class或Object时,可以观察到赋值和属性赋值的变化;为array时,可以观察到数组添加、删除、更新数组单元的变化。

@Link初识化规则:

图示:

图片来源:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-link-0000001524297305-V2

img05

代码示例:

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的greenButtonStateGreenButton({ greenButtonState: $greenButtonState })// 子组件:YellowButton,通过@Link同步更新父组件@State的yellowButtonPropYellowButton({ yellowButtonState: $yellowButtonProp })}}
}

图示:

gif08

3.1.4 @Provide/@Consume 装饰器

为了避免在组件中多次传递变量,推出了一种使某些变量能被所有后代组件使用的装饰器 @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()}}
}

图示:

img06

3.1.5 @Observed 装饰器和@ObjectLink装饰器

对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink装饰器。

@ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步:

  • 被@Observed装饰的类,可以被观察到属性的变化;
  • 子组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被@Observed装饰。
  • 单独使用@Observed是没有任何作用的,需要搭配@ObjectLink或者@Prop使用。
  • 不要用@Observed和其他类装饰器装饰同一个class,@Observed会改变class的原型链

初始化图示:

图片来源:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-observed-and-objectlink-0000001473697338-V2#section2976114355019

img07

以二维数组监听为例:

@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('!');}})}}
}

效果如下:

gif09

3.2 App级别的状态管理

ArkTS可以实现多种应用状态管理的能力,具体表现在以下几点:

  • LocalStorage: 是页面级的状态储存,通常用于 UIAblility内,也可以用于页面间状态共享
  • AppStorage: 特殊的单例 LocalStorage 对象,为程序的 UI 状态属性提供中央储存
  • PersistentStorage: 持久化 UI 状态储存,通常于 AppStorage 一起使用,可以把 AppStorage 存储的数据写入磁盘,确保数据在重启前后保持一致
  • Environment: 应用程序运行的设备环境参数,会同步到 AppStorage 中,可以和AppStorage 搭配使用
3.2.1 LocalStorage: 页面级 UI 状态存储

有关 LocalStorage 介绍:

  • 一个应用程序可以创建多个 LocalStorage 实例,这些实例可以在页面内共享,也可以通过 GetShared 接口在 UIAbility 中创建的实例实现跨页面和 UIAbility 内的共享。
  • 根据组件树的结构,被 @Entry 装饰的 @Component 实例可以被分配一个 LocalStorage 实例,而该组件的所有子组件实例将自动获得对该 LocalStorage 实例的访问权限。
  • @Component 装饰的组件最多可以访问一个 LocalStorage 实例和 AppStorage,而未被 @Entry 装饰的组件无法独立分配 LocalStorage 实例,只能接受父组件通过 @Entry 传递来的 LocalStorage 实例。
  • 一个 LocalStorage 实例在组件树上可以被分配给多个组件,而其中的所有属性都是可变的。
  • 当应用释放最后一个指向LocalStorage的引用时,比如销毁最后一个自定义组件,LocalStorage将被JS Engine垃圾回收。
  • 被绑定的属性值变化(无论父子级关系如何),都会引起依赖此变量的 UI 刷新渲染。

介绍两个用于属性绑定的装饰器

  • @LocalStorageProp: @LocalStorageProp(key) 是一个装饰器,用于在 ArkUI 组件框架中建立自定义组件的属性与 LocalStorage 中特定键对应属性之间的单向数据同步。以下是一些关键信息:
    • 初始化与绑定: 在自定义组件初始化时,被 @LocalStorageProp(key) 装饰的变量通过给定的 key 绑定到相应的 LocalStorage 属性上,完成初始化。本地初始化是必要的,因为不能保证在组件初始化之前 LocalStorage 中是否存在给定的 key

    • 本地修改和同步: 对于使用 @LocalStorageProp(key) 装饰的变量,本地的修改是允许的。但是需要注意,本地的修改永远不会同步回 LocalStorage 中。相反,如果 LocalStorage 中给定 key 的属性发生改变,这个改变会被同步给被 @LocalStorageProp(key) 装饰的变量,并覆盖本地的修改。

  • @LocalStorageLink@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 == 47let link1 = storage.link('KeyA'); // link1.get() == 47
let link2 = storage.link('KeyA'); // link2.get() == 47let prop = storage.prop('KeyA'); // prop.get() == 47link1.set(48); // 双向绑定 link1.get() == link2.get() == prop.get() == 48prop.set(1); // 单向绑定 prop.get() == 1;但是 link1.get() == link2.get() == 48link1.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()}}
}

效果如下:

gif10

3.2.2 AppStorage:应用全局的UI状态存储

概述

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();
3.2.3 PersistentStorage:持久化存储UI状态

LocalStorage和AppStorage都是运行时的内存,但是在应用退出再次启动后,依然能保存选定的结果,是应用开发中十分常见的现象,这就需要用到PersistentStorage。

注意:

  • 持久化数据操作相对较慢,应避免持久化大型数据集和经常变化的变量。
  • PersistentStorage的持久化变量最好是小于2kb的数据,以避免影响UI渲染性能。
  • PersistentStorage只能在UI页面内使用,否则无法持久化数据。
  • 不在调用 PersistentStorage 前调用 AppStorage,会导致上次退出应用保存的值丢失。

代码示例:

PersistentStorage.PersistProp('userScore', 100);@Entry
@Component
struct Game {@State message: string = 'Welcome to the Game'@StorageLink('userScore') userScore: number = 50build() {Row() {Column() {Text(this.message)Text(`你的得分: ${this.userScore}`).onClick(() => {this.userScore += 10;})}}}
}
3.2.4 Environment:设备环境查询

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

使用场景

Environment.EnvProp将设备的语言设置为英语 'zh'。然后,@StorageProp将设备语言与AppStorage中的 deviceLanguage 建立了单向同步。

// 将设备的语言code存入AppStorage
Environment.EnvProp('deviceLanguage', 'zh');@Entry
@Componentstruct 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!');
}

3.3 其他状态管理

3.3.1 @Watch 监听

@Watch用于监听状态变量的变化,当状态变量变化时,@Watch的回调方法将被调用。@Watch在ArkUI框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范。当在严格相等为false的情况下,就会触发@Watch的回调。

观察变化和行为表现

  1. 当观察到状态变量的变化(包括双向绑定的AppStorage和LocalStorage中对应的key发生的变化)的时候,对应的@Watch的回调方法将被触发;
  2. @Watch方法在自定义组件的属性变更之后同步执行;
  3. 如果在@Watch的方法里改变了其他的状态变量,也会引起状态变更和@Watch的执行;
  4. 在第一次初始化的时候,@Watch装饰的方法不会被调用,即认为初始化不是状态变量的改变。只有在后续状态改变时,才会调用@Watch回调方法。

注意点:

  • 避免无限循环。循环可能是因为在@Watch的回调方法里直接或者间接地修改了同一个状态变量引起的。为了避免循环的产生,建议不要在@Watch的回调方法里修改当前装饰的状态变量;
  • 开发者应关注性能,属性值更新函数会延迟组件的重新渲染(具体请见上面的行为表现),因此,回调函数应仅执行快速运算;
  • 不建议在@Watch函数中调用async await,因为@Watch设计的用途是为了快速的计算,异步行为可能会导致重新渲染速度的性能问题。

示例:

@Component
struct TotalView {@Prop @Watch('onCountUpdated') count: number;@State total: number = 0;// @Watch cbonCountUpdated(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 })}}
}
3.3.2 $$语法:内置组件双向同步

$$运算符为系统内置组件提供TS变量的引用,使得TS变量和系统内置组件的内部状态保持同步。

内部状态具体指什么取决于组件。

  • 当前$$支持基础类型变量,以及@State、@Link和@Prop装饰的变量。
  • 当前$$仅支持 bindPopup 属性方法的show参数,Radio组件的checked属性,Refresh组件的refreshing参数。
  • $$绑定的变量变化时,会触发UI的同步刷新。

@Entry
@Component
struct bindPopupPage {@State customPopup: boolean = false;build() {Column() {Button('Popup').margin(20).onClick(() => {this.customPopup = !this.customPopup}).bindPopup($$this.customPopup, {message: 'showPopup'})}}
}

4. 渲染控制

tch方法在自定义组件的属性变更之后同步执行;
3. 如果在@Watch的方法里改变了其他的状态变量,也会引起状态变更和@Watch的执行;
4. 在第一次初始化的时候,@Watch装饰的方法不会被调用,即认为初始化不是状态变量的改变。只有在后续状态改变时,才会调用@Watch回调方法。

注意点:

  • 避免无限循环。循环可能是因为在@Watch的回调方法里直接或者间接地修改了同一个状态变量引起的。为了避免循环的产生,建议不要在@Watch的回调方法里修改当前装饰的状态变量;
  • 开发者应关注性能,属性值更新函数会延迟组件的重新渲染(具体请见上面的行为表现),因此,回调函数应仅执行快速运算;
  • 不建议在@Watch函数中调用async await,因为@Watch设计的用途是为了快速的计算,异步行为可能会导致重新渲染速度的性能问题。

示例:

@Component
struct TotalView {@Prop @Watch('onCountUpdated') count: number;@State total: number = 0;// @Watch cbonCountUpdated(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 })}}
}
3.3.2 $$语法:内置组件双向同步

$$运算符为系统内置组件提供TS变量的引用,使得TS变量和系统内置组件的内部状态保持同步。

内部状态具体指什么取决于组件。

  • 当前$$支持基础类型变量,以及@State、@Link和@Prop装饰的变量。
  • 当前$$仅支持 bindPopup 属性方法的show参数,Radio组件的checked属性,Refresh组件的refreshing参数。
  • $$绑定的变量变化时,会触发UI的同步刷新。

@Entry
@Component
struct bindPopupPage {@State customPopup: boolean = false;build() {Column() {Button('Popup').margin(20).onClick(() => {this.customPopup = !this.customPopup}).bindPopup($$this.customPopup, {message: 'showPopup'})}}
}

4. 渲染控制

对于 UI 渲染,可以基于数据结构选择一些内置方法(例如:ForEach)快速渲染 UI 结构。

4.1 if-else条件渲染

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--;})}}
}

图示:

gif11

4.2 ForEach:循环渲染

必须使用数组(允许空数组),设置的循环函数不允许改变源数组。

  1. ForEach组件接受arr属性作为数组输入,必须是数组类型,允许为空数组,可用返回数组的函数。数组为空时,不会创建子组件。

  2. itemGenerator是必需的,是一个lambda函数,根据数组中的每个数据项生成一个或多个子组件。生成的子组件类型必须符合ForEach的父容器组件的规定。

  3. 可选的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())}}
}

效果如下:

gif12

配合 @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);})}}
}

图示:

gif13

4.3 LazyForEach:数据懒加载

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); // 设置缓存的数据数量}
}

图示:

gif14

文章参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/arkts-get-started-0000001504769321-V2

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/163268.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

labview 安捷伦 34970A 采集温度等

本文详细描述了怎么用安捷伦34970A采集温度&#xff0c;并列出了labview的下载链接&#xff0c;具有一定的参考价值。 1.必要条件&#xff1a; RS-232电缆一根 IO Libraries Suite 软件 BenchLink Data Logger 软件 软件可以在http://www.keysight.com.cn下载 检查RS-232…

SQLite3 数据库学习(五):Qt 数据库高级操作

参考引用 SQLite 权威指南&#xff08;第二版&#xff09;SQLite3 入门 1. Qt 数据库密码加密 MD5 加密在线工具 1.1 加密流程 加密后的密码都是不可逆的 1.2 代码实现 loginsqlite.h #ifndef LOGINSQLITE_H #define LOGINSQLITE_H#include <QWidget> #include <Q…

通过Whisper模型将YouTube播放列表中的视频转换成高质量文字稿的项目

项目简介 一个通过Whisper模型将YouTube播放列表中的视频转换成高质量文字稿的项目。 这个基于 Python 的工具旨在将 YouTube 视频和播放列表转录为文本。它集成了多种技术&#xff0c;例如用于转录的 Fast-Whisper、用于自然语言处理的 SpaCy 以及用于 GPU 加速的 CUDA&…

pytest测试框架介绍(1)

又来每天进步一点点啦~~~ 一、Pytest介绍&#xff1a; pytest 是一个非常成熟的全功能的Python测试框架&#xff1b; pytest 简单、灵活、易上手&#xff1b; 支持参数化 能够支持简单的单元测试和复杂的功能测试&#xff0c;可以做接口自动化测试&#xff08;pytestrequests&…

根据商品链接获取拼多多商品详情数据接口|拼多多商品详情价格数据接口|拼多多API接口

拼多多&#xff0c;作为中国最大的社交电商之一&#xff0c;为卖家提供了丰富的商品详情接口。这些接口可以帮助卖家快速获取商品信息&#xff0c;提高销售效率。本文将详细介绍如何使用拼多多商品详情接口&#xff0c;以及它的优势和注意事项。 一、拼多多商品详情接口概述 …

大宗商品贸易集团数据治理实践,夯实数字基座 | 数字化标杆

某大型央企是首批全国供应链创新与应用示范企业&#xff0c;在“十四五”规划期内以聚焦供应链管理核心主业作为主要战略发展方向。供应链运营管理以大宗商品贸易为主&#xff0c;其交易往往具有交易量巨大、交易环节复杂、风险交易难识别、风险客商难管控等痛点。 随着集团数…

五分钟搭建开源ERP:Odoo,并实现公网远程访问

文章目录 前言1. 下载安装Odoo&#xff1a;2. 实现公网访问Odoo本地系统&#xff1a;3. 固定域名访问Odoo本地系统 前言 Odoo是全球流行的开源企业管理套件&#xff0c;是一个一站式全功能ERP及电商平台。 开源性质&#xff1a;Odoo是一个开源的ERP软件&#xff0c;这意味着企…

小猪优版的前世今生:从籍籍无名到行业瞩目,再到骤变的风暴中心

1. 前世&#xff1a;籍籍无名到行业新星的崛起 小猪优版在初创时期&#xff0c;并不被大众所知。然而&#xff0c;它凭借对短视频行业的深度洞察&#xff0c;以及独特的商业模式&#xff0c;开始在这个领域崭露头角。它提供了一个平台&#xff0c;不仅助力内容创作者更好地展现…

使用whisper实现语音转文本

项目地址&#xff1a;GitHub - openai/whisper: Robust Speech Recognition via Large-Scale Weak Supervision 1、需要py3.8环境 conda activate p38 2、安装 pip install -U openai-whisper 3、下载项目 pip install githttps://github.com/openai/whisper.git 4、安装…

如何避免Steam搬砖项目中账号被盗

购买steam余额有风险吗&#xff1f;及N种被红锁的情况 相信最近很多人都已经听说过steam游戏搬砖这个项目&#xff0c;也叫CSGO游戏搬砖项目&#xff0c;还有人叫它&#xff1a;国外steam游戏汇率差项目&#xff0c;无论怎么称呼&#xff0c;都是同一个项目。 那么什么是stea…

泼天的富贵来啦,快带着你的PMP证书一起迎接

考过PMP认证的威宝们&#xff0c;这波泼天的富贵大家一定要接住呀&#xff01; 很多威宝们在学习PMP之前都在担心&#xff0c;这个证书含金量高吗&#xff1f;转岗跳槽用得上吗&#xff1f;有必要考吗&#xff1f;今天&#xff0c;喜番大声地告诉大家&#xff1a;含金量高&…

Class文件转Java文件

目录 1、下载一个反编译工具2、在文件夹下打开命令窗口3、在此目录下随意建一个文件夹4、在打开的命令窗口输入命令5、返回解压目录下 1、下载一个反编译工具 下载链接&#xff1a;https://varaneckas.com/jad/ 下载的是第一个 下载后放至任意目录下解压即可 2、在文件夹下打…

JetLinks设备接入的认识与理解【woodwhales.cn】

为了更好的阅读体验&#xff0c;建议移步至笔者的博客阅读&#xff1a;JetLinks设备接入的认识与理解 1、认识 JetLinks 1.1、官网文档 官网&#xff1a;https://www.jetlinks.cn/ JetLinks 有两个产品&#xff1a;JetLinks-lot和JetLinks-view 官方文档&#xff1a; JetLi…

没有PDF密码,如何解密?

PDF文件有两种密码&#xff0c;一个打开密码、一个限制编辑密码&#xff0c;因为PDF文件设置了密码&#xff0c;那么打开、编辑PDF文件就会受到限制。忘记了PDF密码该如何解密&#xff1f; PDF和office一样&#xff0c;可以对文件进行加密&#xff0c;但是没有提供恢复密码的功…

mac电脑文件比较工具 UltraCompare 中文for mac

UltraCompare是一款功能强大的文件和文件夹比较工具&#xff0c;用于比较和合并文本、二进制和文件夹。它提供了丰富的功能和直观的界面&#xff0c;使用户能够轻松地比较和同步文件内容&#xff0c;查找差异并进行合并操作。 以下是UltraCompare软件的一些主要特点和功能&…

为什么程序员不直接用线上环境写代码呢?

为什么程序员不直接用线上环境写代码呢&#xff1f; 有的&#xff0c;我就是直接用Linux作为主力电脑使用&#xff0c;大概从201 6年起&#xff0c;我就开始这样干了。无论是编 程、画电路板、画UI、剪视频.... 都在Linux上面完成。 编程工具大部分都有Linux版本&#xff0c;…

【【Linux 常用命令学习 之 一 】】

Linux 常用命令学习 之 一 打开终端之后的 我们会了解 所使用的 字符串含义 其中前面的 zhuxushuai 是 当前的用户名字 接下来的 zhuxushuai-virtual-machine 是 机器名字 最后的符号 $表示 当前是普通用户 输入指令 ls 是打印出当前所在目录中所有文件和文件夹 shell 操…

多个视频怎么生成一个二维码?二维码看视频的制作方法

二维码能放入多个视频吗&#xff1f;现在用二维码看视频是很流行的一种方式&#xff0c;不仅符合现在人的行为习惯&#xff0c;而且还不需要占用自身的容量空间&#xff0c;能够即时的获取视频内容。那么当有多个视频需要展示&#xff0c;但是想要放到一个二维码中&#xff0c;…

集团投融资大数据平台解决方案

一、项目背景 项目为集团型公司大数据平台项目&#xff0c;整个项目周期约为6个月&#xff0c;整体呈现了对外的数据大屏驾驶仓和对内的看板报表&#xff0c;减少了客户内部数据上报和报表制作的重复工作量&#xff0c;为集团数据决策奠定基础。 二、项目目标 战略层&#xff…

咖啡馆管理系统点餐外卖小程序效果如何

咖啡一直是很多人喜欢的饮品&#xff0c;比如有些地区的人非常喜欢&#xff0c;熬夜加班醒脑等&#xff0c;咖啡领域市场规模逐年增加&#xff0c;相应的从业商家也在增加&#xff0c;近些年随着线上生态崛起&#xff0c;传统线下咖啡馆经营痛点显露出来。 通过【雨科】平台搭建…