002 Learn the ArkTs language of Hongmeng application development

Table of contents

1. ArkTs language introduction

1. ArkTs language overview

2. ArkTs language features

2. Basic UI description

1. Basic concepts

2. UI description specification

Construct configuration without parameters

Construct configuration with parameters

attribute configuration

event configuration

subcomponent configuration

3. State management

1. Basic concepts

2. Page-level state management

@State

@Prop

@Observed and ObjectLink data management

@Provide和@Consume

@Watch

3. State management of application-level variables

AppStorage

LocalStorage

PersistentStorage

Environment

4. Dynamically build UI elements

@Builder

@BuilderParam8+

Introduce motivation

Parameter initialization component

@Styles

@Extend

@CustomDialog

5. Rendering control

conditional rendering

Loop rendering

data lazy loading

IDataSource type description

DataChangeListener type description


1. ArkTs language introduction

1. ArkTs language overview

ArkTs is currently the main language developed by Hongmeng. Currently, the development of the new version of Stage (3.1.0 (API 9)), the main model of Hongmeng, does not support Java and JavaScript , so it is necessary to develop Hongmeng applications and learn ArkTs. ArkTs is an extension of Ts (TypeScript). The following introduction is to introduce the characteristics of ArkTs. For the Ts part, you can refer to: Get started with TypeScript in 5 minutes TypeScript Chinese Network TypeScript——a superset of JavaScript

2. ArkTs language features

The current ArkTs language extends the declarative UI features on the basis of the original Ts, the purpose is to simplify the construction and update of the UI, mainly has the following features:

  • Basic UI description: ArkTs defines various decorators, custom components, UI descriptions, and event methods and property methods of built-in components of the UI framework to build the main UI architecture.
  • State management: ArkTs provides a multi-dimensional state management mechanism. In the UI development framework, the data bound to the interface can be used not only in the same component, but also in different components. For example: parent-child components, grandparents and grandchildren components, can also be transferred globally in the application, and even data can be transferred across devices. From the perspective of data flow, the distinction is between readable single-item data transfer and bidirectional change data transfer. Developers can flexibly use existing capabilities to link data and UI.
  • Dynamic UI construction: ArkTs provides dynamic UI construction capabilities, which can not only customize the internal structure of components, but also reuse component styles and extend native components
  • Rendering control: ArkTs mainly provides conditional rendering control and loop rendering control. Conditional rendering is mainly to render different UI content according to conditions. Loop rendering can iteratively obtain data from the data source, and create UI components in the iterative process
  • Use restrictions and extensions: ArkTS has restrictions and constraints in the use process, and it also expands the two-way binding and other capabilities.

Below we use a specific example to illustrate the basic composition of ArkTS. In the code example shown in the figure below, the UI interface contains a piece of text and a button. When the developer clicks the button, the text content will change from 'Hello World' to 'Hello ArkUI'.

The basic components of the ArkTS declarative development paradigm contained in this example are described as follows:

  • Decorator: Used to decorate classes, structures, methods, and variables to give them special meanings, such as @Entry, @Component, and @State in the above example are all decorators. Specifically, @Component indicates that this is a custom component; @Entry indicates that this is an entry component; @State indicates that this is a state variable in the component, and a change in this variable will trigger a UI refresh.
  • Custom components: reusable UI units that can be combined with other components, such as the above-mentioned struct Hello decorated by @Component.
  • UI Description: A declarative approach to describe the structure of the UI, such as code blocks in the build() method.
  • Built-in components: The default built-in basic components, container components, media components, drawing components, canvas components and other components in ArkTS can be directly called by developers, such as Column, Text, Divider, Button, etc. in the example.
  • Property method: used to configure component properties, such as fontSize(), width(), height(), color(), etc. Multiple properties can be set through chain calls.
  • Event method: used to add component response logic to events, such as onClick() following Button, and multiple event response logics can also be set through chain calls.
  • State modification: It is used to modify the data bound to the UI. After the @state modifier is modified, the corresponding UI interface will also change when the state changes.

2. Basic UI description

ArkTs decorates the data structure of the struct keyword life through the decorators @Component and @Entry to construct a custom component. A build function is provided in the custom component, and the developer needs to perform a basic UI description in a chained call within this function.

1. Basic concepts

  • Struct: Custom components can be implemented based on struct and cannot be used for inheritance.
  • Decorator: A decorator endows the currently decorated object with certain capabilities. It can not only decorate a structure or class, but also decorate the attributes of a class. Multiple decorators recommend multi-line writing to be more standardized .
  • build function: custom components must define a build function, and custom constructors are prohibited . The build function satisfies the definition of the Builder constructor interface and is used to define the declarative UI description of the component.
  • @Component: Decorate struct, the structure has component-based capabilities after decoration , and needs to implement the build method to create the UI.

  • @Entry: Decorate the struct, the component is decorated as the entry of the page , and will be rendered and displayed when the page is loaded.

  • Chain call: Configure the property method, event method, etc. of the UI component in the way of "." chain call.

2. UI description specification

Construct configuration without parameters

If the interface definition of the component does not contain mandatory construction parameters, there is no need to configure anything in the "()" behind the component. For example, the Column and Divider components do not contain constructor parameters:

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

Construct configuration with parameters

If the interface definition of the component contains construction parameters, the corresponding parameters can be configured in the "()" behind the component, and the parameters can be assigned with constants.

For example:

  • The required parameter src of the Image component:

Image($r('app.media.icon'))
  • The parameter content of the Text component, this parameter is optional, that is, it can be configured or not configured:
Text('test')

Variables or expressions can also be used for parameter assignment, and the result type returned by the expression must meet the parameter type requirements. For the definition of variables, see page-level variable state management and application-level variable state management. For example, setting variables or expressions to construct parameters for Image and Text components:

Image(this.imagePath)
Image('https://' + this.imageUrl)
Text(`count: ${this.count}`)

attribute configuration

Use the property method to configure the properties of the component. The property method is followed by the component and connected with the "." operator.

  • Configure the font size property of the Text component:

Text('test')
  .fontSize(12)
  • Use the "." operator to chain calls and configure multiple properties of the component at the same time, as follows:
Text("你好").fontSize(20).fontColor($r('app.color.button_next_background')).textAlign(TextAlign.End).width(200)
  • In addition to passing constant parameters directly, you can also pass variables or expressions, as follows:
Text('hello')
  .fontSize(this.fontSize)
Image('test.jpg')
  .width(this.count % 2 === 0 ? 100 : 200)    
  .height(this.offsetTest + 100)

  • For system built-in components, the framework also predefines some enumeration types for its properties for developers to call. The enumeration types can be passed as parameters and must meet the parameter type requirements. For example, the color and font properties of a Text component can be configured as follows
Text('hello')
  .fontSize(20)
  .fontColor(Color.Red)
  .fontWeight(FontWeight.Bold)

event configuration

The events supported by the component can be configured through the event method. The event method follows the component and is connected with the "." operator.

  • Use lambda expressions to configure event methods for components:

Button() {
          Text('Add')
            .fontSize(45)
            .fontColor($r('app.color.start_window_background'))
        }
        .onClick(() => {
          this.messageNum++
        })
  • Use an anonymous function expression to configure the event method of the component, requiring the use of bind (the click event is invalid without using bind) to ensure that this in the function body refers to the contained component:
Button() {
          Text('Add'+this.messageNum)
            .fontSize(45)
            .fontColor($r('app.color.start_window_background'))
        }
        .onClick(function (){
          this.messageNum++
        }.bind(this))
  • Configure the component's event methods using the component's member functions:
Button() {
          Text('Add' + this.messageNum)
            .fontSize(45)
            .fontColor($r('app.color.start_window_background'))
        }
        .onClick(this.myClick.bind(this))

subcomponent configuration

For components that support subcomponent configuration, such as container components, add the UI description of subcomponents to the component in "{ ... }". Components such as Column, Row, Stack, Grid, and List are all container components.

  • Here is a simple Column example:

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

三.状态管理

1.基本概念

ArkTS提供了多维度的状态管理机制,在ArkUI开发框架中,和UI相关联的数据,不仅可以在组件内使用,还可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,也可以是应用全局范围内的传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活地利用这些能力来实现数据和UI的联动。

2.页面级状态管理

页面级状态管理对比介绍

装饰器 装饰内容 说明
@state 基本数据类型,类,数组 修改后的状态数据被执行时会执行自定义组件中与此状态变量相关的UI元素,更新UI界面
@Prop 基本数据类型,类,数组 修改后的状态数据用于父组件与子组件之间建立单项数据流的绑定关系,当父组件中状态数据发生变化时子组件与之相关的UI元素也会进行页面更新
@Link 基本数据类型,类,数组 父组件与子组件建立双向的数据流绑定关系,当任意一方数据发生改变另一方与之相关的UI元素都会进行页面更新,将最新的数据状态展示出来
@Observed @Observed应用于类,表示该类中的数据变更被UI页面管理
@ObjectLink

被@Observed所装饰类的对象

@ObjectLink装饰的状态数据被修改时,在父组件或者其他兄弟组件内与它关联的状态数据所在的组件都会重新渲染。
@Provide 基本数据类型,类,数组 @Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面重新渲染。
@Consume 基本数据类型,类,数组 @Consume装饰的变量在感知到@Provide装饰的变量更新后,会触发当前自定义组件的重新渲染。

@State

@State装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法中的部分UI描述(使用该状态变量的UI组件相关描述)进行UI刷新。

@State状态数据具有以下特征:

  • 支持多种类型数据:支持class、number、boolean、string强类型数据的值类型和引用类型,以及这些强类型构成的数组,即Array<class>、Array<string>、Array<boolean>、Array<number>。不支持any。
  • 支持多实例:组件不同实例的内部状态数据独立。
  • 内部私有:标记为@State的属性是私有变量,只能在组件内访问。
  • 需要本地初始化:必须为所有@State变量分配初始值,变量未初始化可能导致未定义的框架异常行为。
  • 创建自定义组件时支持通过状态变量名设置初始值:在创建组件实例时,可以通过变量名显式指定@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

@Prop与@State有相同的语义,但初始化方式不同。@Prop装饰的变量必须使用其父组件提供的@State变量进行初始化,允许组件内部修改@Prop变量,但变量的更改不会通知给父组件,父组件变量的更改会同步到@prop装饰的变量,即@Prop属于单向数据绑定。

@Prop状态数据具有以下特征:

  • 支持多种类型数据:支持class、number、boolean、string强类型数据的值类型和引用类型,以及这些强类型构成的数组,即Array<class>、Array<string>、Array<boolean>、Array<number>。不支持any。
  • 私有:仅支持组件内访问;
  • 单个数据源:父组件中用于初始化子组件@Prop变量的必须是父组件定义的状态变量;
  • 单向通信:子组件对@Prop变量的更改将不会同步修改父组件中的@State变量,父组件中@State的变量修改将会同步给子组件中的@Prop变量;
  • 创建自定义组件时将按值传递方式给@Prop变量进行初始化:在创建组件的新实例时,必须初始化所有@Prop变量,不支持在组件内部进行初始化。

此处示例与下一个@Link装饰符一起演示,可以看出明显区别

@Link装饰的变量可以和父组件的@State变量建立双向数据绑定:

  • 支持多种类型:@Link支持的数据类型与@State相同,即class、number、string、boolean或这些类型的数组;
  • 私有:仅支持组件内访问;
  • 单个数据源:父组件中用于初始化子组件@Link变量的必须是父组件定义的状态变量;
  • 双向通信:子组件对@Link变量的更改将同步修改父组件中的@State变量;
  • 创建自定义组件时需要将变量的引用传递给@Link变量,在创建组件的新实例时,必须使用命名参数初始化所有@Link变量。@Link变量可以使用@State变量或@Link变量的引用进行初始化,@State变量可以通过'$'操作符创建引用。
  • @Link变量不能在组件内部进行初始化。

下面的示例是演示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--
        })
    }
  }
}

@Observed和ObjectLink数据管理

当开发者需要在子组件中针对父组件的一个变量(parent_a)设置双向同步时,开发者可以在父组件中使用@State装饰变量(parent_a),并在子组件中使用@Link装饰对应的变量(child_a)。这样不仅可以实现父组件与单个子组件之间的数据同步,也可以实现父组件与多个子组件之间的数据同步。如下图所示,可以看到,父子组件针对ClassA类型的变量设置了双向同步,那么当子组件1中变量对应的属性c的值变化时,会通知父组件同步变化,而当父组件中属性c的值变化时,会通知所有子组件同步变化。而当需要传递数组其中一个实例时,使用@Link就不能满足要求。如果这些部分信息是一个类对象,就可以使用@ObjectLink配合@Observed来实现

设置要求:

  • @Observed用于类,@ObjectLink用于变量。

  • @ObjectLink装饰的变量类型必须为类(class type)。

    • 类要被@Observed装饰器所装饰。
    • 不支持简单类型参数,可以使用@Prop进行单向同步。
  • @ObjectLink装饰的变量是不可变的。

    • 属性的改动是被允许的,当改动发生时,如果同一个对象被多个@ObjectLink变量所引用,那么所有拥有这些变量的自定义组件都会被通知进行重新渲染。
  • @ObjectLink装饰的变量不可设置默认值。

    • 必须让父组件中有一个由@State、@Link、@StorageLink、@Provide或@Consume装饰的变量所参与的TS表达式进行初始化。
  • @ObjectLink装饰的变量是私有变量,只能在组件内访问。

示例与下面的修饰符一起演示

@Provide和@Consume

@Provide作为数据的提供方,可以更新其子孙节点的数据,并触发页面渲染。@Consume在感知到@Provide数据的更新后,会触发当前自定义组件的重新渲染。

@Provide

  • 装饰器参数是一个string类型的常量,用于给装饰的变量起别名。如果规定别名,则提供对应别名的数据更新。如果没有,则使用变量名作为别名。推荐使用@Provide('alias')这种形式。
  • 同步机制:@Provide的变量类似@State,可以修改对应变量进行页面重新渲染。也可以修改@Consume装饰的变量,反向修改@State变量。
  • 必须设置初始值。
  • 触发页面渲染的修改:基础类型(boolean,string,number)变量的改变;@Observed class类型变量及其属性的修改;添加,删除,更新数组中的元素。

@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用于监听状态变量的变化,语法结构为:

  1. @State @Watch("onChanged") count : number = 0

如上所示,给状态变量增加一个@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<number> = [7, 12, 47, 3]
  @State totalPurchase: number = 0
  @State addPurchase: number = 0

  aboutToAppear() {
    this.updateTotal()
  }

  updateTotal(): void {
    let sum = 0;
    this.shopBasket.forEach((i) => {
      sum += i
    })
    // 计算新的购物篮总价值,如果超过100,则适用折扣
    this.totalPurchase = (sum < 100) ? sum : 0.9 * sum
  }

  // shopBasket更改时触发该方法
  onBasketUpdated(propName: string): void {
    this.updateTotal()
  }

  build() {
    Column() {
      Button('add to basket ' + this.addPurchase)
        .margin(15)
        .onClick(() => {
          this.addPurchase = Math.round(100 * Math.random())
          this.shopBasket.push(this.addPurchase)
        })
      Text(`${this.totalPurchase}`)
        .fontSize(30)
      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()
      })
    }
  }
}

3.应用级变量的状态管理

AppStorage

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<number>('varA', AppStorage.Get<number>('varA') + 1)
          })
        Button(`language: ${this.languageCode}`)
          .onClick(() => {
            if (AppStorage.Get<string>('languageCode') === 'zh') {
              AppStorage.Set<string>('languageCode', 'en')
            } else {
              AppStorage.Set<string>('languageCode', 'zh')
            }
            this.label = (this.languageCode === 'zh') ? '数量' : 'Count'
          })
      }
      .margin({ bottom: 50 })

      Row() {
        Button(`更改@StorageLink修饰的变量:${this.varA}`).height(40).fontSize(14)
          .onClick(() => {
            this.varA++
          })
      }.margin({ bottom: 50 })

      Row() {
        Button(`更改@StorageProp修饰的变量:${this.languageCode}`).height(40).fontSize(14)
          .onClick(() => {
            if (this.languageCode === 'zh') {
              this.languageCode = 'en'
            } else {
              this.languageCode = 'zh'
            }
          })
      }.margin({bottom:100})
      Row() {
        Button(`跳转到AppStorageB`).height(40).fontSize(14)
          .onClick(() => {
            router.pushUrl({url:'pages/AppStorageB'})
          })
      }
    }
  }
}

@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

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

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

Environment是框架在应用程序启动时创建的单例对象,它为AppStorage提供了一系列应用程序需要的环境状态数据,这些数据描述了应用程序运行的设备环境,包括系统语言、深浅色模式等等。Environment及其属性是不可变的,所有数据类型均为简单类型。如下示例展示了从Environment获取系统是否开启无障碍屏幕朗读:

Environment.EnvProp('accessibilityEnabled', 'default')
var enable = AppStorage.Get('accessibilityEnabled')

accessibilityEnabled是Environment提供的系统默认变量识别符。首先需要将对应系统属性绑定到AppStorage上,再通过AppStorage中的方法或者装饰器访问对应的系统属性数据。

四.动态构建UI元素

基本UI描述介绍的是如何创建一个内部UI结构固定的自定义组件,为了满足开发者自定义组件内部UI结构的需求,ArkTS同时提供了动态构建UI元素的能力。

@Builder

可通过@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)
  }
}

@BuilderParam8+

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

@Styles

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

@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

@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条件语句中使用的状态变量发生变化时,条件渲染都会更新并重新进行条件评估,如果条件分支发生了变化,这意味着需要构建另一个条件分支,此时框架将:

  1. 删除所有以前渲染的(早期分支的)组件。
  2. 执行分支的UI描述,将生成的子组件添加到其父组件中。

在以下示例中,如果count从0增加到1,那么条件渲染会进行更新并计算各个条件分支值:

  • 条件(count > 0)计算结果为false,未发生变化,不执行额外操作;
  • 条件(this.count % 2 === 0)从true更改为false,该分支内的Text组件将从Column组件中移除并销毁;
  • else分支为true,执行该条件下的UI描述,创建一个Text组件,并将它放在Column组件内。
@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中的所有节点都将重建。

IDataSource类型说明

名称

描述

totalCount(): number

获取数据总数。

getData(index: number): any

获取索引值index对应的数据。

registerDataChangeListener(listener:DataChangeListener): void

注册数据改变的监听器。

unregisterDataChangeListener(listener:DataChangeListener): void

注销数据改变的监听器。

DataChangeListener类型说明

名称

描述

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

Guess you like

Origin blog.csdn.net/gongjdde/article/details/130242541