002 紅蒙アプリケーション開発のArkTs言語を学ぶ

目次

1.ArkTs言語の紹介

1.ArkTs言語の概要

2.ArkTsの言語機能

2.基本的なUIの説明

1. 基本的な考え方

2. UI記述仕様

パラメーターを使用せずに構成を構築する

パラメータを使用して構成を構築する

属性の設定

イベント構成

サブコンポーネントの構成

3. 状態管理

1. 基本的な考え方

2. ページレベルの状態管理

@州

@プロップ

@Observed および ObjectLink データ管理

@提供和@消費

@時計

3. アプリケーションレベル変数の状態管理

アプリストレージ

ローカルストレージ

永続ストレージ

環境

4. UI要素を動的に構築する

@ビルダー

@BuilderParam8+

モチベーションを紹介する

パラメータ初期化コンポーネント

@スタイル

@拡張する

@カスタムダイアログ

5. レンダリング制御

条件付きレンダリング

ループレンダリング

データの遅延読み込み

IDataSource タイプの説明

DataChangeListener タイプの説明


1.ArkTs言語の紹介

1.ArkTs言語の概要

現在Hongmengが開発しているメイン言語はArkTsですが、現在Hongmengの​​メインモデルであるStageの新バージョン(3.1.0(API 9))の開発はJavaとJavaScriptをサポートしていないため、Hongmengを開発する必要があります。アプリケーションを作成し、ArkT を学習します。ArkTs は Ts (TypeScript) の拡張機能です。次の紹介は ArkTs の特徴を紹介するものです。Ts の部分については、次を参照してください: Get Started with TypeScript in 5 minutes TypeScript Chinese Network TypeScript—a superset of JavaScript

2.ArkTsの言語機能

現在の ArkTs 言語は、元の Ts に基づいて宣言型 UI 機能を拡張しています。その目的は、UI の構築と更新を簡素化することであり、主に次の機能があります。

  • 基本的な UI 記述: ArkTs は、メインの UI アーキテクチャを構築するために、さまざまなデコレーター、カスタム コンポーネント、UI 記述、UI フレームワークの組み込みコンポーネントのイベント メソッドとプロパティ メソッドを定義します。
  • 状態管理: ArkTs は多次元の状態管理メカニズムを提供し、UI 開発フレームワークでは、インターフェイスにバインドされたデータを同じコンポーネントだけでなく、異なるコンポーネントでも使用できます。たとえば、親子コンポーネント、祖父母コンポーネントと孫コンポーネントもアプリケーション内でグローバルに転送でき、データもデバイス間で転送できます。データ フローの観点から見ると、読み取り可能な単一項目データ転送と双方向変更データ転送は区別されます。開発者は既存の機能を柔軟に使用してデータと UI をリンクできます。
  • 動的 UI 構築: ArkTs は、コンポーネントの内部構造をカスタマイズするだけでなく、コンポーネント スタイルを再利用し、ネイティブ コンポーネントを拡張できる動的 UI 構築機能を提供します。
  • レンダリング制御: ArkTs は主に条件付きレンダリング制御とループ レンダリング制御を提供します。条件付きレンダリングは主に、条件に従って異なる UI コンテンツをレンダリングします。ループ レンダリングは、データ ソースから反復的にデータを取得し、反復プロセスで UI コンポーネントを作成できます。
  • 使用制限と拡張: ArkTS には使用プロセスに制限と制約があり、また双方向バインディングやその他の機能も拡張されます。

以下では、具体的な例を使用して、ArkTS の基本構成を説明します。以下の図に示すコード例では、UI インターフェイスにテキストとボタンが含まれており、開発者がボタンをクリックすると、テキストの内容が「Hello World」から「Hello ArkUI」に変わります。

この例に含まれる ArkTS 宣言型開発パラダイムの基本コンポーネントは次のように説明されます。

  • デコレータ: クラス、構造体、メソッド、変数を装飾して特別な意味を与えるために使用されます。上記の例の @Entry、@Component、@State などはすべてデコレータです。具体的には、@Component はこれがカスタム コンポーネントであることを示し、@Entry はこれがエントリ コンポーネントであることを示し、@State はこれがコンポーネント内の状態変数であることを示し、この変数の変更により UI の更新がトリガーされます。
  • カスタム コンポーネント: @Component で装飾された前述の構造体 Hello など、他のコンポーネントと組み合わせることができる再利用可能な UI ユニット。
  • UI の説明: build() メソッドのコード ブロックなど、UI の構造を記述するための宣言的アプローチ。
  • 組み込みコンポーネント: デフォルトの組み込み基本コンポーネント、コンテナ コンポーネント、メディア コンポーネント、描画コンポーネント、キャンバス コンポーネント、および ArkTS のその他のコンポーネント (列、テキスト、ディバイダー、ボタンなど) は、開発者が直接呼び出すことができます。例。
  • プロパティ メソッド: fontSize()、width()、height()、color() などのコンポーネント プロパティを設定するために使用されます。チェーン呼び出しを通じて複数のプロパティを設定できます。
  • イベント メソッド: Button に続く onClick() など、イベントにコンポーネント応答ロジックを追加するために使用されます。チェーン呼び出しを通じて複数のイベント応答ロジックを設定することもできます。
  • 状態の変更: UI にバインドされたデータを変更するために使用されます。@state 修飾子が変更された後、状態が変化すると、対応する UI インターフェイスも変更されます。

2.基本的なUIの説明

ArkTs は、デコレーター @Component および @Entry を通じて struct キーワード ライフのデータ構造を装飾し、カスタム コンポーネントを構築します。ビルド関数はカスタム コンポーネントで提供されており、開発者はこの関数内の連鎖呼び出しで基本的な UI の記述を実行する必要があります。

1. 基本的な考え方

  • 構造体:カスタム コンポーネントは構造体に基づいて実装できますが、継承には使用できません。
  • デコレータ: デコレータは、現在装飾されているオブジェクトに特定の機能を与えます。構造体やクラスを装飾するだけでなく、クラスの属性も装飾できます。複数のデコレータは、より標準化するために複数行の記述を推奨しています
  • ビルド関数: カスタム コンポーネントはビルド関数を定義する必要があり、カスタム コンストラクターは禁止されていますbuild 関数は、Builder コンストラクター インターフェイスの定義を満たし、コンポーネントの宣言的な UI 記述を定義するために使用されます。
  • @Component: Decorate struct。この構造には、 decoration の後にコンポーネントベースの機能があり、UI を作成するために build メソッドを実装する必要があります。

  • @Entry: 構造体を装飾します。コンポーネントはページのエントリとして装飾され、ページが読み込まれるときにレンダリングされて表示されます。

  • チェーン呼び出し:UIコンポーネントのプロパティメソッドやイベントメソッドなどを「.」チェーン呼び出しで設定します。

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

パラメータを使用して構成を構築する

コンポーネントのインターフェース定義に構築パラメータが含まれている場合、コンポーネントの後ろの「()」内に対応するパラメータを設定し、パラメータに定数を割り当てることができます。

例えば:

  • 画像コンポーネントの必須パラメータ src:

画像($r('app.media.icon'))
  • Text コンポーネントのパラメーターの内容。このパラメーターはオプションです。つまり、構成することも構成しないこともできます。
テキスト('テスト')

変数または式はパラメーターの割り当てにも使用でき、式によって返される結果の型はパラメーターの型の要件を満たしている必要があります。変数の定義については、「ページ レベルの変数の状態管理」および「アプリケーション レベルの変数の状態管理」を参照してください。たとえば、変数または式を設定して画像コンポーネントとテキスト コンポーネントのパラメータを構築します。

画像(this.imagePath)
画像('https://' + this.imageUrl)
テキスト(`カウント: ${this.count}`)

属性の設定

コンポーネントのプロパティを設定するには、プロパティ メソッドを使用します。プロパティ メソッドの後にコンポーネントが続き、「.」演算子で接続されます。

  • Text コンポーネントのフォント サイズ プロパティを設定します。

テキスト('テスト') 
  .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 コンポーネントの色とフォントのプロパティは次のように構成できます。
Text('hello') 
  .fontSize(20) 
  .fontColor(Color.Red) 
  .fontWeight(FontWeight.Bold)

イベント構成

コンポーネントがサポートするイベントは、イベント メソッドを通じて構成できます。イベント メソッドはコンポーネントの後に続き、「.」演算子で接続されます。

  • ラムダ式を使用してコンポーネントのイベント メソッドを構成します。

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

おすすめ

転載: blog.csdn.net/gongjdde/article/details/130242541