目录
一.ArkTs语言介绍
1.ArkTs语言概述
2.ArkTs语言特性
二.基本UI描述
1.基本概念
2.UI描述规范
无参数构造配置
有参数构造配置
属性配置
事件配置
子组件配置
三.状态管理
1.基本概念
2.页面级状态管理
@State
@Prop
@Link
@Observed和ObjectLink数据管理
@Provide和@Consume
@Watch
3.应用级变量的状态管理
AppStorage
LocalStorage
PersistentStorage
Environment
四.动态构建UI元素
@Builder
@BuilderParam8+
引入动机
参数初始化组件
@Styles
@Extend
@CustomDialog
五.渲染控制
条件渲染
循环渲染
数据懒加载
IDataSource类型说明
DataChangeListener类型说明
ArkTs是目前鸿蒙开发的主要语言,目前鸿蒙的主推模型Stage的新版本(3.1.0(API 9))开发不支持Java和JavaScript了,所以开发鸿蒙应用学习ArkTs是很有必要的。ArkTs是Ts(TypeScript)的扩展,后面的介绍就是介绍ArkTs的特性,Ts部分可以参考:5分钟上手TypeScript · TypeScript中文网 · TypeScript——JavaScript的超集
当前的ArkTs语言是在原先的Ts基础上扩展了声明式UI特性,目的就是简化构建和更新UI,主要有以下特性:
下面我们以一个具体的示例来说明ArkTS的基本组成。如下图所示的代码示例,UI界面包含一段文本和一个按钮,当开发者点击按钮时,文本内容会从'Hello World'变为 'Hello ArkUI'。
这个示例中所包含的ArkTS声明式开发范式的基本组成说明如下:
ArkTs通过装饰器@Component与@Entry装饰struct关键字生命的数据结构,构造一个自定义组件。自定义组件中提供一个build函数,开发者需在该函数内以链式调用的方式进行基本的UI描述。
@Component:装饰struct,结构体在装饰后具有基于组件的能力,需要实现build方法来创建UI。
@Entry: 装饰struct,组件被装饰后作为页面的入口,页面加载时将被渲染显示。
链式调用:以 "." 链式调用的方式配置UI组件的属性方法、事件方法等。
如果组件的接口定义中不包含必选构造参数,组件后面的“()”中不需要配置任何内容。例如,Column和Divider组件不包含构造参数:
Column() { Text('Hello World') .fontSize(50) .fontWeight(FontWeight.Bold) Divider().width(200).height(10).color($r('app.color.button_next_background')) Text('Hello ArkTs') .fontSize(50) .fontWeight(FontWeight.Bold) } |
如果组件的接口定义中包含构造参数,则在组件后面的“()”中可配置相应参数,参数可以使用常量进行赋值。
例如:
Image组件的必选参数src:
Image($r('app.media.icon')) |
Text('test') |
变量或表达式也可以用于参数赋值,其中表达式返回的结果类型必须满足参数类型要求,变量的定义详见页面级变量的状态管理与应用级变量的状态管理。例如,设置变量或表达式来构造Image和Text组件的参数:
Image(this.imagePath) Image('https://' + this.imageUrl) Text(`count: ${this.count}`) |
使用属性方法配置组件的属性,属性方法紧随组件,并用"."运算符连接。
配置Text组件的字体大小属性:
Text('test') .fontSize(12) |
Text("你好").fontSize(20).fontColor($r('app.color.button_next_background')).textAlign(TextAlign.End).width(200) |
Text('hello') .fontSize(this.fontSize) Image('test.jpg') .width(this.count % 2 === 0 ? 100 : 200) .height(this.offsetTest + 100) |
Text('hello') .fontSize(20) .fontColor(Color.Red) .fontWeight(FontWeight.Bold) |
通过事件方法可以配置组件支持的事件,事件方法紧随组件,并用"."运算符连接。
使用lambda表达式配置组件的事件方法:
Button() { Text('Add') .fontSize(45) .fontColor($r('app.color.start_window_background')) } .onClick(() => { this.messageNum++ }) |
Button() { Text('Add'+this.messageNum) .fontSize(45) .fontColor($r('app.color.start_window_background')) } .onClick(function (){ this.messageNum++ }.bind(this)) |
Button() { Text('Add' + this.messageNum) .fontSize(45) .fontColor($r('app.color.start_window_background')) } .onClick(this.myClick.bind(this)) |
对于支持子组件配置的组件,例如容器组件,在"{ ... }"里为组件添加子组件的UI描述。Column、Row、Stack、Grid、List等组件都是容器组件。
以下是简单的Column示例:
Column() { Button() { Text('Add' + this.messageNum) .fontSize(45) .fontColor($r('app.color.start_window_background')) } .onClick(this.myClick.bind(this)) Text("你好").fontSize(20).fontColor($r('app.color.button_next_background')).textAlign(TextAlign.End).width(200) } |
Row() { Column() { this.TextBuild() Child({ count: this.num }) Child2({ count: $num }) Button() { Text('Back') .fontSize(45) .fontColor($r('app.color.start_window_background')) } .type(ButtonType.Capsule) .width('60%') .height('10%') .backgroundColor($r('app.color.button_next_background')) .onClick(() => { router.back() }) Button() { Text(`Num++ ${this.num}`) .fontSize(45) .fontColor($r('app.color.start_window_background')) } .type(ButtonType.Capsule) .width('60%') .height('10%') .backgroundColor($r('app.color.button_next_background')) .margin({ top: 20 }) .onClick(() => { this.num++ }) } .width('100%') } .height('100%') |
ArkTS提供了多维度的状态管理机制,在ArkUI开发框架中,和UI相关联的数据,不仅可以在组件内使用,还可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,也可以是应用全局范围内的传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活地利用这些能力来实现数据和UI的联动。
页面级状态管理对比介绍
装饰器 | 装饰内容 | 说明 |
---|---|---|
@state | 基本数据类型,类,数组 | 修改后的状态数据被执行时会执行自定义组件中与此状态变量相关的UI元素,更新UI界面 |
@Prop | 基本数据类型,类,数组 | 修改后的状态数据用于父组件与子组件之间建立单项数据流的绑定关系,当父组件中状态数据发生变化时子组件与之相关的UI元素也会进行页面更新 |
@Link | 基本数据类型,类,数组 | 父组件与子组件建立双向的数据流绑定关系,当任意一方数据发生改变另一方与之相关的UI元素都会进行页面更新,将最新的数据状态展示出来 |
@Observed | 类 | @Observed应用于类,表示该类中的数据变更被UI页面管理 |
@ObjectLink | 被@Observed所装饰类的对象 |
@ObjectLink装饰的状态数据被修改时,在父组件或者其他兄弟组件内与它关联的状态数据所在的组件都会重新渲染。 |
@Provide | 基本数据类型,类,数组 | @Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面重新渲染。 |
@Consume | 基本数据类型,类,数组 | @Consume装饰的变量在感知到@Provide装饰的变量更新后,会触发当前自定义组件的重新渲染。 |
@State装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法中的部分UI描述(使用该状态变量的UI组件相关描述)进行UI刷新。
@State状态数据具有以下特征:
现在我们来做一个示例,主要逻辑是在自定义组件MyComponent设置两个状态值为state的值count与title,如下:
// xxx.ets @Entry @Component struct EntryComponent { build() { Column() { MyComponent({ count: 1, increaseBy: 2 }) // 第1个MyComponent实例 MyComponent({ title: 'Hello World 2', count: 7 }) // 第2个MyComponent实例 } } } @Component struct MyComponent { @State count: number = 0 @State title: string = 'Hello World' private increaseBy: number = 1 build() { Column() { Text(this.title) Button('Click to change title') .margin(20) .onClick(() => { // 修改内部状态变量title this.title = (this.title != 'Hello World') ? 'Hello World' : 'Hello ArkUI' }) Button(`Click to increase count=${this.count}`) .margin(20) .onClick(() => { // 修改内部状态变量count this.count += this.increaseBy }) } } } |
上面的代码放在编辑器中尝试是可以看出点击Click to change title事件MyComponent是进行界面更新的并且两个MyComponent是相互不干扰的,同理点击Click to increase count=也是如此
@Prop与@State有相同的语义,但初始化方式不同。@Prop装饰的变量必须使用其父组件提供的@State变量进行初始化,允许组件内部修改@Prop变量,但变量的更改不会通知给父组件,父组件变量的更改会同步到@prop装饰的变量,即@Prop属于单向数据绑定。
@Prop状态数据具有以下特征:
此处示例与下一个@Link装饰符一起演示,可以看出明显区别
@Link装饰的变量可以和父组件的@State变量建立双向数据绑定:
下面的示例是演示Child组件中的count变量是@Prop修饰的,Child2组件中的count变量是@Link修饰的,当点击Child组件的按钮时会发现只有Child组件count数据更新并更新了界面,当点击Child2组件的按钮时会发现所有的count数据都更新并且界面更新了,当点击父组件的按钮也会发现所有的组件中count数据发生了更新并且界面发生更新了
import router from '@ohos.router'; @Entry @Component struct Second { @State message: string = 'Hello World Second' @State num: number = 1 build() { Row() { Column() { this.TextBuild() Child({ count: this.num }) Child2({ count: $num }) Button() { Text('Back') .fontSize(45) .fontColor($r('app.color.start_window_background')) } .type(ButtonType.Capsule) .width('60%') .height('10%') .backgroundColor($r('app.color.button_next_background')) .onClick(() => { router.back() }) Button() { Text(`Num++ ${this.num}`) .fontSize(45) .fontColor($r('app.color.start_window_background')) } .type(ButtonType.Capsule) .width('60%') .height('10%') .backgroundColor($r('app.color.button_next_background')) .margin({ top: 20 }) .onClick(() => { this.num++ }) } .width('100%') } .height('100%') } @Builder TextBuild(){ Text(this.message) .align(Alignment.Center) .fontSize(40) .fontWeight(FontWeight.Bold) } } @Component struct Child { //父组件可以改变子组件,子组件不能改变父组件 @Prop count: number build() { Column() { Text(`You have ${this.count} Nuggets left`).fontSize(18) Button('count - costOfOneAttempt') .margin(15) .onClick(() => { this.count-- }) } } } @Component struct Child2 { //父组件与子组件双向绑定 @Link count: number build() { Column() { Text(`You have ${this.count} Nuggets left`).fontSize(18) Button('count - costOfOneAttempt') .margin(15) .onClick(() => { this.count-- }) } } } |
当开发者需要在子组件中针对父组件的一个变量(parent_a)设置双向同步时,开发者可以在父组件中使用@State装饰变量(parent_a),并在子组件中使用@Link装饰对应的变量(child_a)。这样不仅可以实现父组件与单个子组件之间的数据同步,也可以实现父组件与多个子组件之间的数据同步。如下图所示,可以看到,父子组件针对ClassA类型的变量设置了双向同步,那么当子组件1中变量对应的属性c的值变化时,会通知父组件同步变化,而当父组件中属性c的值变化时,会通知所有子组件同步变化。而当需要传递数组其中一个实例时,使用@Link就不能满足要求。如果这些部分信息是一个类对象,就可以使用@ObjectLink配合@Observed来实现
设置要求:
@Observed用于类,@ObjectLink用于变量。
@ObjectLink装饰的变量类型必须为类(class type)。
@ObjectLink装饰的变量是不可变的。
@ObjectLink装饰的变量不可设置默认值。
@ObjectLink装饰的变量是私有变量,只能在组件内访问。
示例与下面的修饰符一起演示
@Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。@Consume在感知到@Provide数据的更新后,会触发当前自定义组件的重新渲染。
@Provide
@Consume
示例如下:
import router from '@ohos.router'; @Entry @Component struct Third { //因为是数组所以需要用到@ObjectLink和@Observed @State arrA: ClassA[] = [new ClassA(2), new ClassA(0)] //此修饰符与@Consume组合可以让其与孙子节点数据双向绑定 @Provide("reviewVote") reviewVotes: number = 0; build() { Row() { Column() { ForEach(this.arrA, (item) => { TextChild({ a: item }) }, (item) => item.id.toString()) ForEach(this.arrA, (item) => { Child({ a: item }) }, (item) => item.id.toString()) Button() { Text('Back') .fontSize(45) .fontColor($r('app.color.start_window_background')) } .type(ButtonType.Capsule) .width('60%') .height('10%') .backgroundColor($r('app.color.button_next_background')) .onClick(() => { router.back() }) Button() { Text('Add') .fontSize(45) .fontColor($r('app.color.start_window_background')) } .type(ButtonType.Capsule) .width('60%') .height('5%') .backgroundColor($r('app.color.button_next_background')) .margin({ top: 20 }) .onClick(() => { this.arrA[0].c++ }) Button() { Text('AddChildChild'+this.reviewVotes) .fontSize(25) .fontColor($r('app.color.start_window_background')) } .type(ButtonType.Capsule) .width('60%') .height('10%') .backgroundColor($r('app.color.button_next_background')) .margin({ top: 20 }) .onClick(() => { this.reviewVotes++ }) } .width('100%') } .height('100%') } } @Component struct TextChild { @ObjectLink a: ClassA build() { Column() { Text(this.a.c + "TextChild") .align(Alignment.Center) .fontSize(40) .fontWeight(FontWeight.Bold) TextChildChild() } } } @Component struct TextChildChild { //此修饰符与爷爷组件的@Provide组合可以与爷爷组件双向绑定 @Consume("reviewVote") reviewVotes: number build() { Column() { Button() { Text('RemoveChildChild'+this.reviewVotes) .fontSize(20) .fontColor($r('app.color.start_window_background')) } .type(ButtonType.Capsule) .width('60%') .height('5%') .backgroundColor($r('app.color.button_next_background')) .margin({ top: 20 }) .onClick(() => { this.reviewVotes-- }) Text(this.reviewVotes + "TextChildChild") .align(Alignment.Center) .fontSize(40) .fontWeight(FontWeight.Bold) } } } @Component struct Child { @ObjectLink a: ClassA build() { Column() { Text(this.a.c + "Child") .align(Alignment.Center) .fontSize(40) .fontWeight(FontWeight.Bold) Button('count - costOfOneAttempt') .margin(15) .onClick(() => { this.a.c-- }) } } } var nextID: number = 0 @Observed class ClassA { public name: string public c: number public id: number constructor(c: number, name: string = 'OK') { this.name = name this.c = c this.id = nextID++ } } |
@Watch用于监听状态变量的变化,语法结构为:
如上所示,给状态变量增加一个@Watch装饰器,通过@Watch注册一个回调方法onChanged, 当状态变量count被改变时, 触发onChanged回调。
装饰器@State、@Prop、@Link、@ObjectLink、@Provide、@Consume、@StorageProp以及@StorageLink所装饰的变量均可以通过@Watch监听其变化。
import router from '@ohos.router'; // xxx.ets @Entry @Component struct CompA { @State @Watch('onBasketUpdated') shopBasket: Array |
AppStorage是应用程序中的单例对象,由UI框架在应用程序启动时创建,在应用程序退出时销毁,为应用程序范围内的可变状态属性提供中央存储。
AppStorage包含整个应用程序中需要访问的所有状态属性,只要应用程序保持运行,AppStorage就会保存所有属性及属性值,属性值可以通过唯一的键值进行访问。
组件可以通过装饰器将应用程序状态数据与AppStorage进行同步,应用业务逻辑的实现也可以通过接口访问AppStorage。
AppStorage的选择状态属性可以与不同的数据源或数据接收器同步,这些数据源和接收器可以是设备上的本地或远程,并具有不同的功能,如数据持久性。这样的数据源和接收器可以独立于UI在业务逻辑中实现。
默认情况下,AppStorage中的属性是可变的,AppStorage还可使用不可变(只读)属性。
@StorageLink装饰器
组件通过使用@StorageLink(key)装饰的状态变量,与AppStorage建立双向数据绑定,key为AppStorage中的属性键值。当创建包含@StorageLink的状态变量的组件时,该状态变量的值将使用AppStorage中的值进行初始化。在UI组件中对@StorageLink的状态变量所做的更改将同步到AppStorage,并从AppStorage同步到任何其他绑定实例中,如PersistentStorage或其他绑定的UI组件。
@StorageProp装饰器
组件通过使用@StorageProp(key)装饰的状态变量,与AppStorage建立单向数据绑定,key标识AppStorage中的属性键值。当创建包含@StorageProp的状态变量的组件时,该状态变量的值将使用AppStorage中的值进行初始化。AppStorage中属性值的更改会导致绑定该状态变量的UI组件进行状态更新。
示例
每次用户单击Count按钮时,this.varA变量值都会增加1,此变量与AppStorageA中的varA同步。每次用户单击language按钮时,修改AppStorageA中的languageCode,此修改会同步给this.languageCode变量。点击跳转到AppStorageB,会发现varA的值和languageCode的值是传递过去了。
import router from '@ohos.router'; // xxx.ets @Entry @Component struct AppStorageA { @StorageLink('varA') varA: number = 2 @StorageProp('languageCode') languageCode: string = 'en' @State private label: string = 'count' build() { Column() { Row({ space: 20 }) { Button(`${this.label}: ${this.varA}`) .onClick(() => { AppStorage.Set |
@Entry @Component struct AppStorageB { @StorageLink('varA') varA: number = 2 @StorageProp('languageCode') languageCode: string = 'en' @State private label: string = 'count' build() { Column() { Text(`language: ${this.languageCode}`).margin({top:100}) Text(`varA: ${this.varA}`).margin({top:100}) }.height('100%').width('100%') } } |
LocalStorage是应用程序中的存储单元,生命周期跟随其关联的Ability。在Stage模型下,LocalStorage解决AppStorage共享范围过大的问题,提供Ability之间全局数据的隔离。同时,LocalStorage为应用程序范围内的可变状态属性和非可变状态属性提供存储,可变状态属性和非可变状态属性是构建应用程序UI的一部分,如一个Ability的UI。解决App与Ability之间数据互相干扰问题,多实例场景下同一个Ability类的不同Ability实例之间的数据互相干扰问题。在分布式迁移的场景下,Ability是系统调度的最小单元,配合LocalStorage更方便实现组件的数据迁移。
应用层:一个应用程序可以创建多个LocalStorage实例,应用程序的每一个Ability对应一个LocalStorage实例。
Ability:一个应用程序可以拥有多个Ability,一个Ability中的所有子组件最多可以分配一个LocalStorage实例。并且,Ability中的所有子组件都将继承对此LocalStorage实例存储对象的访问权。
一个组件最多可以访问一个LocalStorage实例,一个LocalStorage对象可以分配给多个组件。
@LocalStorageLink装饰器
组件通过使用@LocalStorageLink(key)装饰的状态变量,key值为LocalStorage中的属性键值,与LocalStorage建立双向数据绑定。当创建包含@LocalStorageLink的状态变量的组件时,该状态变量的值将会使用LocalStorage中的值进行初始化。如果LocalStorage中未定义初始值,将使用@LocalStorageLink定义的初始值。在UI组件中对@LocalStorageLink的状态变量所做的更改将同步到LocalStorage中,并从LocalStorage同步到Ability下的组件中。
@LocalStorageProp装饰器
组件通过使用LocalStorageProp(key)装饰的状态变量,key值为LocalStorage中的属性键值,与LocalStorage建立单向数据绑定。当创建包含@LocalStorageProp的状态变量的组件时,该状态变量的值将使用LocalStorage中的值进行初始化。LocalStorage中的属性值的更改会导致当前Ability下的所有UI组件进行状态更新。
具体用法参考:
import UIAbility from '@ohos.app.ability.UIAbility'; import hilog from '@ohos.hilog'; import window from '@ohos.window'; export default class EntryAbility extends UIAbility { storage: LocalStorage onCreate(want, launchParam) { this.storage = new LocalStorage() this.storage.setOrCreate('storageSimpleProp', 8) hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); } onDestroy() { hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); } onWindowStageCreate(windowStage: window.WindowStage) { // Main window is created, set main page for this ability hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); windowStage.loadContent('pages/LocalStorageComponent', this.storage) } } |
import router from '@ohos.router'; // xxx.ets // Index.ets let storage = LocalStorage.GetShared() @Entry(storage) @Component struct LocalStorageComponent { @LocalStorageLink('storageSimpleProp') simpleVarName: number = 0 build() { Column() { Button(`LocalStorageLink: ${this.simpleVarName.toString()}`) .margin(20) .onClick(() => { this.simpleVarName += 1 }) Text(JSON.stringify(this.simpleVarName)) .fontSize(50) LocalStorageComponentProp() }.width('100%') } } @Component struct LocalStorageComponentProp { @LocalStorageProp('storageSimpleProp') simpleVarName: number = 0 build() { Column() { Button(`LocalStorageProp: ${this.simpleVarName.toString()}`) .margin(20) .onClick(() => { this.simpleVarName += 1 }) Text(JSON.stringify(this.simpleVarName)) .fontSize(50) }.width('100%') } } |
PersistentStorage提供了一些静态方法用来管理应用持久化数据,可以将特定标记的持久化数据链接到AppStorage中,并由AppStorage接口访问对应持久化数据,或者通过@StorageLink装饰器来访问对应key的变量。
// xxx.ets PersistentStorage.PersistProp('highScore', 10) @Entry @Component struct PersistentComponent { @StorageLink('highScore') highScore: number = 0 @State currentScore: number = 0 build() { Column() { if (this.currentScore === Number(this.highScore)) { Text(`new highScore : ${this.highScore}`).fontSize(18) } Button(`goal!, currentScore : ${this.currentScore}`) .margin(20) .onClick(() => { this.currentScore++ if (this.currentScore > Number(this.highScore)) { this.highScore = this.currentScore } }) }.width('100%') } } |
Environment是框架在应用程序启动时创建的单例对象,它为AppStorage提供了一系列应用程序需要的环境状态数据,这些数据描述了应用程序运行的设备环境,包括系统语言、深浅色模式等等。Environment及其属性是不可变的,所有数据类型均为简单类型。如下示例展示了从Environment获取系统是否开启无障碍屏幕朗读:
Environment.EnvProp('accessibilityEnabled', 'default') var enable = AppStorage.Get('accessibilityEnabled') |
accessibilityEnabled是Environment提供的系统默认变量识别符。首先需要将对应系统属性绑定到AppStorage上,再通过AppStorage中的方法或者装饰器访问对应的系统属性数据。
基本UI描述介绍的是如何创建一个内部UI结构固定的自定义组件,为了满足开发者自定义组件内部UI结构的需求,ArkTS同时提供了动态构建UI元素的能力。
可通过@Builder装饰器进行描述,该装饰器可以修饰一个函数,此函数可以在build函数之外声明,并在build函数中或其他@Builder修饰的函数中使用,从而实现在一个自定义组件内快速生成多个布局内容。使用方式如下面示例所示。
@Entry @Component struct Second { @State message: string = 'Hello World Second' @State num: number = 1 build() { Row() { Column() { this.TextBuild() } .width('100%') } .height('100%') } @Builder TextBuild(){ Text(this.message) .align(Alignment.Center) .fontSize(40) .fontWeight(FontWeight.Bold) } } |
@BuilderParam装饰器用于修饰自定义组件内函数类型的属性(例如:@BuilderParam noParam: () => void),并且在初始化自定义组件时被@BuilderParam修饰的属性必须赋值。
当开发者创建自定义组件,并想对该组件添加特定功能时(例如在自定义组件中添加一个点击跳转操作)。若直接在组件内嵌入事件方法,将会导致所有引入该自定义组件的地方均增加了该功能。为解决此问题,引入了@BuilderParam装饰器,此装饰器修饰的属性值可为@Builder装饰的函数,开发者可在初始化自定义组件时对此属性进行赋值,为自定义组件增加特定的功能。
通过参数初始化组件时,将@Builder装饰的函数赋值给@BuilderParam修饰的属性,并在自定义组件内调用该属性值。若@BuilderParam修饰的属性在进行赋值时不带参数(如:noParam: this.specificNoParam),则此属性的类型需定义为无返回值的函数(如:@BuilderParam noParam: () => void);若带参数(如:withParam: this.SpecificWithParam('WithParamA')),则此属性的类型需定义成any(如:@BuilderParam withParam: any)。
// xxx.ets @Component struct CustomContainer { header: string = '' @BuilderParam noParam: () => void @BuilderParam withParam: any footer: string = '' build() { Column() { Text(this.header) .fontSize(30) this.noParam() this.withParam() Text(this.footer) .fontSize(30) } } } @Entry @Component struct CustomContainerUser { @Builder specificNoParam() { Column() { Text('noParam').fontSize(30) } } @Builder SpecificWithParam(label: string) { Column() { Text(label).fontSize(30) } } build() { Column() { CustomContainer({ header: 'HeaderA', noParam: this.specificNoParam, withParam: this.SpecificWithParam('WithParamA'), footer: 'FooterA' }) Divider().color(Color.Black) .strokeWidth(3) .margin(10) CustomContainer({ header: 'HeaderB', noParam: this.specificNoParam, withParam: this.SpecificWithParam('WithParamB'), footer: 'FooterB' }) } } } |
ArkTS为了避免开发者对重复样式的设置,通过@Styles装饰器可以将多个样式设置提炼成一个方法,直接在组件声明时调用,通过@Styles装饰器可以快速定义并复用自定义样式。当前@Styles仅支持通用属性。
@Styles可以定义在组件内或组件外,在组件外定义时需在方法名前面添加function关键字,组件内定义时则不需要添加function关键字。
// xxx.ets @Styles function globalFancy () { .width(150) .height(100) .backgroundColor(Color.Pink) } @Entry @Component struct FancyUse { @Styles componentFancy() { .width(100) .height(200) .backgroundColor(Color.Yellow) } build() { Column({ space: 10 }) { Text('FancyA') .globalFancy() .fontSize(30) Text('FancyB') .globalFancy() .fontSize(20) Text('FancyC') .componentFancy() .fontSize(30) Text('FancyD') .componentFancy() .fontSize(20) } } } |
@Styles还可以在StateStyles属性内部使用,在组件处于不同的状态时赋予相应的属性。
在StateStyles内可以直接调用组件外定义的@Styles方法,但需要通过this关键字调用组件内定义的@Styles方法。
// xxx.ets @Styles function globalFancy () { .width(120) .height(120) .backgroundColor(Color.Green) } @Entry @Component struct FancyUse { @Styles componentFancy() { .width(80) .height(80) .backgroundColor(Color.Red) } build() { Row({ space: 10 }) { Button('Fancy') .stateStyles({ normal: { .width(100) .height(100) .backgroundColor(Color.Blue) }, disabled: this.componentFancy, pressed: globalFancy }) } } } |
@Extend装饰器将新的属性方法添加到Text、Column、Button等内置组件上,通过@Extend装饰器可以快速地扩展原生组件。@Extend不能定义在自定义组件struct内。
// xxx.ets @Extend(Text) function fancy (fontSize: number) { .fontColor(Color.Red) .fontSize(fontSize) .fontStyle(FontStyle.Italic) .fontWeight(600) } @Entry @Component struct FancyUse { build() { Row({ space: 10 }) { Text("Fancy") .fancy(16) Text("Fancy") .fancy(24) Text("Fancy") .fancy(32) } } } |
@CustomDialog装饰器用于装饰自定义弹窗组件,使得弹窗可以动态设置内容及样式。
import router from '@ohos.router'; // xxx.ets @CustomDialog struct DialogExample { controller: CustomDialogController action: () => void build() { Row() { Button('Close CustomDialog') .onClick(() => { this.controller.close() this.action() }) }.padding(20) } } @Entry @Component struct CustomDialogUser { dialogController: CustomDialogController = new CustomDialogController({ builder: DialogExample({ action: this.onAccept }), cancel: this.existApp, autoCancel: true }); onAccept() { console.info('onAccept'); } existApp() { console.info('Cancel dialog!'); } build() { Column() { Button('Click to open Dialog') .onClick(() => { this.dialogController.open() }) } } } |
ArkTS也提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。
使用if/else进行条件渲染。
f/else语句的每个分支都包含一个构建UI的描述,此类描述中必须创建一个或多个子组件。在初始渲染时,if语句会执行其中的一个分支并将生成的子组件添加到if/else的父组件中。
每当if或else if条件语句中使用的状态变量发生变化时,条件渲染都会更新并重新进行条件评估,如果条件分支发生了变化,这意味着需要构建另一个条件分支,此时框架将:
在以下示例中,如果count从0增加到1,那么条件渲染会进行更新并计算各个条件分支值:
@Entry @Component struct MyComponent { @State count: number = 300 build() { Column() { if (this.count < 0) { Text('count is negative').fontSize(14) } else if (this.count % 2 === 0) { Text('count is even').fontSize(14) } else { Text('count is odd').fontSize(14) } } } } |
通过循环渲染(ForEach)从数组中获取数据,并为每个数据项创建相应的组件,可减少代码复杂度。
ForEach( arr: any[], itemGenerator: (item: any, index?: number) => void, keyGenerator?: (item: any, index?: number) => string ) |
参数:
参数名 |
参数类型 |
必填 |
参数描述 |
---|---|---|---|
arr |
any[] |
是 |
必须是数组,允许设置为空数组,空数组场景下将不会创建子组件。同时允许设置返回值为数组类型的函数,例如arr.slice(1, 3),设置的函数不得改变包括数组本身在内的任何状态变量,如Array.splice、Array.sort或Array.reverse这些改变原数组的函数。 |
itemGenerator |
(item: any, index?: number) => void |
是 |
生成子组件的lambda函数,为数组中的每一个数据项创建一个或多个子组件,单个子组件或子组件列表必须包括在大括号“{...}”中。 |
keyGenerator |
(item: any, index?: number) => string |
否 |
匿名函数,用于给数组中的每一个数据项生成唯一且固定的键值。当数据项在数组中的位置更改时,其键值不得更改,当数组中的数据项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,不提供时框架默认使用index+JSON.stringify(item)的方式生成。 为了使开发框架能够更好地识别数组更改,提高性能,建议开发者提供自定义的键值生成器。如将数组反向时,如果没有提供键值生成器,则ForEach中的所有节点都将重建。 |
示例:
@Entry @Component struct MyComponent { @State arr: number[] = [10, 20, 30] build() { Column({ space: 5 }) { Button('Reverse Array') .onClick(() => { this.arr.reverse() }) ForEach(this.arr, (item: number) => { Text(`item value: ${item}`).fontSize(18) Divider().strokeWidth(2).color(Color.Black) }, (item: number) => item.toString()) } } } |
通过数据懒加载(LazyForEach)从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。
LazyForEach( dataSource: IDataSource, itemGenerator: (item: any) => void, keyGenerator?: (item: any) => string ): void interface IDataSource { totalCount(): number; getData(index: number): any; registerDataChangeListener(listener: DataChangeListener): void; unregisterDataChangeListener(listener: DataChangeListener): void; } interface DataChangeListener { onDataReloaded(): void; onDataAdd(index: number): void; onDataMove(from: number, to: number): void; onDataDelete(index: number): void; onDataChange(index: number): void; } |
参数:
参数名 |
参数类型 |
必填 |
参数描述 |
---|---|---|---|
dataSource |
IDataSource |
是 |
实现IDataSource接口的对象,需要开发者实现相关接口。 |
itemGenerator |
(item: any, index?: number) => void |
是 |
生成子组件的lambda函数,为数组中的每一个数据项创建一个或多个子组件,单个子组件或子组件列表必须包括在大括号“{...}”中。 |
keyGenerator |
(item: any, index?: number) => string |
否 |
匿名函数,用于给数组中的每一个数据项生成唯一且固定的键值。当数据项在数组中的位置更改时,其键值不得更改,当数组中的数据项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,不提供时框架默认使用index+JSON.stringify(item)的方式生成。 为了使开发框架能够更好地识别数组更改,提高性能,建议提供自定义的键值生成器。如将数组反向时,如果没有提供键值生成器,则LazyForEach中的所有节点都将重建。 |
名称 |
描述 |
---|---|
totalCount(): number |
获取数据总数。 |
getData(index: number): any |
获取索引值index对应的数据。 |
registerDataChangeListener(listener:DataChangeListener): void |
注册数据改变的监听器。 |
unregisterDataChangeListener(listener:DataChangeListener): void |
注销数据改变的监听器。 |
名称 |
描述 |
---|---|
onDataReloaded(): void |
重新加载所有数据。 |
onDataAdded(index: number): voiddeprecated |
通知组件index的位置有数据添加。从API Version 8开始废弃,建议使用onDataAdd。 |
onDataMoved(from: number, to: number): voiddeprecated |
通知组件数据从from的位置移到to的位置。从API Version 8开始废弃,建议使用onDataMove。 |
onDataDeleted(index: number): voiddeprecated |
通知组件index的位置有数据删除。从API Version 8开始废弃,建议使用onDataDelete。 |
onDataChanged(index: number): voiddeprecated |
通知组件index的位置有数据变化。 从API Version 8开始废弃,建议使用onDataChange。 |
onDataAdd(index: number): void8+ |
通知组件index的位置有数据添加。 |
onDataMove(from: number, to: number): void8+ |
通知组件数据从from的位置移到to的位置。 |
onDataDelete(index: number): void8+ |
通知组件index的位置有数据删除。 |
onDataChange(index: number): void8+ |
通知组件index的位置有数据变化。 |
示例:
// xxx.ets 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('add listener') this.listeners.push(listener) } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { console.info('remove listener') 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[] = ['/path/image0.png', '/path/image1.png', '/path/image2.png', '/path/image3.png'] 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 { private data: MyDataSource = new MyDataSource() build() { List({ space: 3 }) { LazyForEach(this.data, (item: string) => { ListItem() { Row() { Image(item).width(50).height(50) Text(item).fontSize(20).margin({ left: 10 }) }.margin({ left: 10, right: 10 }) } .onClick(() => { // 每点击一次列表项,数据增加一项 this.data.pushData('/path/image' + this.data.totalCount() + '.png') }) }, item => item) } } } |