Skip to content
当前页导航

ArkTS 简介

声明式 UI 描述

自定义组件

基本结构

ts
// @Component({ freezeWhenInactive: true })  开启组件冻结功能
@Component
struct MyComponentName {
  build() {

  }
}

自定义组件的参数

ts
@Component
struct MyComponent {
  private countDownFrom: number = 0; // 组件参数
  private color: Color = Color.Blue; // 组件参数

  build() {
  }
}

@Entry
@Component
struct ParentComponent {
  private someColor: Color = Color.Pink;

  build() {
    Column() {
      MyComponent({ countDownFrom: 10, color: this.someColor })
    }
  }
}

build()函数

  • @Entry 装饰的自定义页面入口组件,其 build()函数下的根节点唯一且必要,且必须为容器组件,其中 ForEach 禁止作为根节点。
  • @Component 装饰的自定义组件,其 build()函数下的根节点唯一且必要,可以为非容器组件,其中 ForEach 禁止作为根节点。
ts
@Entry
@Component
struct MyComponent {
  build() {
    // 根节点唯一且必要,必须为容器组件
    Row() {
      ChildComponent()
    }
  }
}

@Component
struct ChildComponent {
  build() {
    // 根节点唯一且必要,可为非容器组件
    Image('test.jpg')
  }
}

不允许声明本地变量,反例如下。

ts
build() {
  // 反例:不允许声明本地变量
  let a: number = 1;
}

不允许在 UI 描述里直接使用 console.info,但允许在方法或者函数里使用,反例如下。

ts
build() {
  // 反例:不允许console.info
  console.info('print debug log');
}

不允许创建本地的作用域,反例如下。

ts
build() {
  // 反例:不允许本地作用域
  {
    ...
  }
}

不允许调用没有用@Builder 装饰的方法,允许系统组件的参数是 TS 方法的返回值。

ts
@Component
struct ParentComponent {
  doSomeCalculations() {
  }

  calcTextValue(): string {
    return 'Hello World';
  }

  @Builder doSomeRender() {
    Text(`Hello World`)
  }

  build() {
    Column() {
      // 反例:不能调用没有用@Builder装饰的方法
      this.doSomeCalculations();
      // 正例:可以调用
      this.doSomeRender();
      // 正例:参数可以为调用TS方法的返回值
      Text(this.calcTextValue())
    }
  }
}

不允许使用 switch 语法,如果需要使用条件判断,请使用 if。示例如下。

ts
build() {
  Column() {
    // 反例:不允许使用switch语法
    switch (expression) {
      case 1:
        Text('...')
        break;
      case 2:
        Image('...')
        break;
      default:
        Text('...')
        break;
    }
    // 正例:使用if
    if(expression == 1) {
      Text('...')
    } else if(expression == 2) {
      Image('...')
    } else {
      Text('...')
    }
  }
}

不允许使用表达式,反例如下。

ts
build() {
  Column() {
    // 反例:不允许使用表达式
    (this.aVar > 10) ? Text('...') : Image('...')
  }
}

不允许直接改变状态变量,反例如下。

ts
@Component
struct CompA {
  @State col1: Color = Color.Yellow;
  @State col2: Color = Color.Green;
  @State count: number = 1;
  build() {
    Column() {
      // 应避免直接在Text组件内改变count的值
      Text(`${this.count++}`)
        .width(50)
        .height(50)
        .fontColor(this.col1)
        .onClick(() => {
          this.col2 = Color.Red;
        })
      Button("change col1").onClick(() =>{
        this.col1 = Color.Pink;
      })
    }
    .backgroundColor(this.col2)
  }
}

自定义组件冻结功能

自定义组件冻结功能专为优化复杂 UI 页面的性能而设计,尤其适用于包含多个页面栈、长列表或宫格布局的场景。在这些情况下,当状态变量绑定了多个 UI 组件,其变化可能触发大量 UI 组件的刷新,进而导致界面卡顿和响应延迟。为了提升这类负载 UI 界面的刷新性能,开发者可以选择尝试使用自定义组件冻结功能。

组件冻结的工作原理是

  1. 开发者通过设置 freezeWhenInactive 属性,即可激活组件冻结机制。
  2. 启用后,系统将仅对处于激活状态的自定义组件进行更新,这使得 UI 框架可以尽量缩小更新范围,仅限于用户可见范围内(激活状态)的自定义组件,从而提高复杂 UI 场景下的刷新效率。
  3. 当之前处于 inactive 状态的自定义组件重新变为 active 状态时,状态管理框架会对其执行必要的刷新操作,确保 UI 的正确展示。

简而言之,组件冻结旨在优化复杂界面下的 UI 刷新性能。在存在多个不可见自定义组件的情况下,如多页面栈、长列表或宫格,通过组件冻结可以实现按需刷新,即仅刷新当前可见的自定义组件,而将不可见自定义组件的刷新延迟至它们变为可见时。

需要注意,组件 active/inactive 并不等同于其可见性。组件冻结目前仅适用于以下场景

  1. 页面路由:当前栈顶页面为 active,非栈顶不可见页面为 inactive。
  • 当页面 A 调用 router.pushUrl 接口跳转到页面 B 时,页面 A 为隐藏不可见状态,此时如果更新页面 A 中的状态变量,不会触发页面 A 刷新。
  1. TabContent:只有当前显示的 TabContent 中的自定义组件处于 active 状态,其余则为 inactive。
  • 对 Tabs 中当前不可见的 TabContent 进行冻结,不会触发组件的更新。
  • 需要注意的是:在首次渲染的时候,Tab 只会创建当前正在显示的 TabContent,当切换全部的 TabContent 后,TabContent 才会被全部创建。
  1. LazyForEach:仅当前显示的 LazyForEach 中的自定义组件为 active,而缓存节点的组件则为 inactive。
  • 对 LazyForEach 中缓存的自定义组件进行冻结,不会触发组件的更新。
  1. Navigation:当前显示的 NavDestination 中的自定义组件为 active,而其他未显示的 NavDestination 组件则为 inactive。
  • 当 NavDestination 不可见时,会对其子自定义组件设置成非激活态,不会触发组件的刷新。当返回该页面时,其子自定义组件重新恢复成激活态,触发@Watch 回调进行刷新。
  • 在下面例子中,NavigationContentMsgStack 会被设置成非激活态,将不再响应状态变量的变化,也不会触发组件刷新。

其他场景,如堆叠布局(Stack)下的被遮罩的组件,这些组件尽管不可见,但并不被视为 inactive 状态,因此不在组件冻结的适用范围内。

循环渲染

键值生成规则

在 ForEach 循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI 框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。

ForEach 提供了一个名为 keyGenerator 的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义 keyGenerator 函数,则 ArkUI 框架会使用默认的键值生成函数,即(item: Object, index: number) => { return index + '__' + JSON.stringify(item); }。

ArkUI 框架对于 ForEach 的键值生成有一套特定的判断规则,这主要与 itemGenerator 函数的第二个参数 index 以及 keyGenerator 函数的第二个参数 index 有关,具体的键值生成规则判断逻辑如下图所示。

1

ts
build() {
  Column() {
    ForEach(this.taskList, (item: Object, index: number) => {
      ToDoItem({content: item.name})
    }, (item: Object, index: number) => item.id.toString())
  }
}

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

自定义组件和页面的关系:

  • 自定义组件:@Component 装饰的 UI 单元,可以组合多个系统组件实现 UI 的复用,可以调用组件的生命周期。
  • 页面:即应用的 UI 页面。可以由一个或者多个自定义组件组成,@Entry 装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry 装饰的组件才可以调用页面的生命周期。

页面生命周期

  • onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景。
  • onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景。
  • onBackPress:当用户点击返回按钮时触发。

组件生命周期

  • aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其 build()函数之前执行。
  • onDidBuild:组件 build()函数执行完成之后回调该接口,不建议在 onDidBuild 函数中更改状态变量、使用 animateTo 等功能,这可能会导致不稳定的 UI 表现。
  • aboutToDisappear:aboutToDisappear 函数在自定义组件析构销毁之前执行。不允许在 aboutToDisappear 函数中改变状态变量,特别是@Link 变量的修改可能会导致应用程序行为不稳定。

生命周期流程如下图所示,下图展示的是被@Entry 装饰的组件(页面)生命周期。

1

自定义组件的创建和渲染流程

  1. 自定义组件的创建:自定义组件的实例由 ArkUI 框架创建。
  2. 初始化自定义组件的成员变量:通过本地默认值或者构造方法传递参数来初始化自定义组件的成员变量,初始化顺序为成员变量的定义顺序。
  3. 如果开发者定义了 aboutToAppear,则执行 aboutToAppear 方法。
  4. 在首次渲染的时候,执行 build 方法渲染系统组件,如果子组件为自定义组件,则创建自定义组件的实例。在首次渲染的过程中,框架会记录状态变量和组件的映射关系,当状态变量改变时,驱动其相关的组件刷新。
  5. 如果开发者定义了 onDidBuild,则执行 onDidBuild 方法。

自定义组件重新渲染

当事件句柄被触发(比如设置了点击事件,即触发点击事件)改变了状态变量时,或者 LocalStorage / AppStorage 中的属性更改,并导致绑定的状态变量更改其值时:

  1. 框架观察到了变化,将启动重新渲染。
  2. 根据框架持有的两个 map(自定义组件的创建和渲染流程中第 4 步),框架可以知道该状态变量管理了哪些 UI 组件,以及这些 UI 组件对应的更新函数。执行这些 UI 组件的更新函数,实现最小化更新。

自定义组件的删除

如果 if 组件的分支改变,或者 ForEach 循环渲染中数组的个数改变,组件将被删除:

  1. 在删除组件之前,将调用其 aboutToDisappear 生命周期函数,标记着该节点将要被销毁。ArkUI 的节点删除机制是:后端节点直接从组件树上摘下,后端节点被销毁,对前端节点解引用,前端节点已经没有引用时,将被 JS 虚拟机垃圾回收。
  2. 自定义组件和它的变量将被删除,如果其有同步的变量,比如@Link、@Prop、@StorageLink,将从同步源上取消注册。

不建议在生命周期 aboutToDisappear 内使用 async await,如果在生命周期的 aboutToDisappear 使用异步操作(Promise 或者回调方法),自定义组件将被保留在 Promise 的闭包中,直到回调方法被执行完,这个行为阻止了自定义组件的垃圾回收。

ts
// Index.ets
import { router } from '@kit.ArkUI';

@Entry
@Component
struct MyComponent {
  @State showChild: boolean = true;
  @State btnColor:string = "#FF007DFF";

  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onPageShow() {
    console.info('Index onPageShow');
  }
  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onPageHide() {
    console.info('Index onPageHide');
  }

  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onBackPress() {
    console.info('Index onBackPress');
    this.btnColor ="#FFEE0606";
    return true // 返回true表示页面自己处理返回逻辑,不进行页面路由;返回false表示使用默认的路由返回逻辑,不设置返回值按照false处理
  }

  // 组件生命周期
  aboutToAppear() {
    console.info('MyComponent aboutToAppear');
  }

  // 组件生命周期
  onDidBuild() {
    console.info('MyComponent onDidBuild');
  }

  // 组件生命周期
  aboutToDisappear() {
    console.info('MyComponent aboutToDisappear');
  }

  build() {
    Column() {
      // this.showChild为true,创建Child子组件,执行Child aboutToAppear
      if (this.showChild) {
        Child()
      }
      // this.showChild为false,删除Child子组件,执行Child aboutToDisappear
      Button('delete Child')
        .margin(20)
        .backgroundColor(this.btnColor)
        .onClick(() => {
        this.showChild = false;
      })
      // push到page页面,执行onPageHide
      Button('push to next page')
        .onClick(() => {
          router.pushUrl({ url: 'pages/page' });
        })
    }
  }
}

@Component
struct Child {
  @State title: string = 'Hello World';
  // 组件生命周期
  aboutToDisappear() {
    console.info('[lifeCycle] Child aboutToDisappear');
  }

  // 组件生命周期
  onDidBuild() {
    console.info('[lifeCycle] Child onDidBuild');
  }

  // 组件生命周期
  aboutToAppear() {
    console.info('[lifeCycle] Child aboutToAppear');
  }

  build() {
    Text(this.title)
      .fontSize(50)
      .margin(20)
      .onClick(() => {
        this.title = 'Hello ArkUI';
      })
  }
}
ts
// page.ets
@Entry
@Component
struct page {
  @State textColor: Color = Color.Black;
  @State num: number = 0;

  onPageShow() {
    this.num = 5;
  }

  onPageHide() {
    console.log("page onPageHide");
  }

  onBackPress() { // 不设置返回值按照false处理
    this.textColor = Color.Grey;
    this.num = 0;
  }

  aboutToAppear() {
    this.textColor = Color.Blue;
  }

  build() {
    Column() {
      Text(`num 的值为:${this.num}`)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.textColor)
        .margin(20)
        .onClick(() => {
          this.num += 5;
        })
    }
    .width('100%')
  }
}

以上示例中,Index 页面包含两个自定义组件,一个是被@Entry 装饰的 MyComponent,也是页面的入口组件,即页面的根节点;一个是 Child,是 MyComponent 的子组件。只有@Entry 装饰的节点才可以使页面级别的生命周期方法生效,因此在 MyComponent 中声明当前 Index 页面的页面生命周期函数(onPageShow / onPageHide / onBackPress)。MyComponent 和其子组件 Child 分别声明了各自的组件级别生命周期函数(aboutToAppear / onDidBuild/aboutToDisappear)。

  • 应用冷启动的初始化流程为:MyComponent aboutToAppear --> MyComponent build --> MyComponent onDidBuild--> Child aboutToAppear --> Child build --> Child onDidBuild --> Index onPageShow。
  • 点击“delete Child”,if 绑定的 this.showChild 变成 false,删除 Child 组件,会执行 Child aboutToDisappear 方法。
  • 点击“push to next page”,调用 router.pushUrl 接口,跳转到另外一个页面,当前 Index 页面隐藏,执行页面生命周期 Index onPageHide。此处调用的是 router.pushUrl 接口,Index 页面被隐藏,并没有销毁,所以只调用 onPageHide。跳转到新页面后,执行初始化新页面的生命周期的流程。
  • 如果调用的是 router.replaceUrl,则当前 Index 页面被销毁,执行的生命周期流程将变为:Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。上文已经提到,组件的销毁是从组件树上直接摘下子树,所以先调用父组件的 aboutToDisappear,再调用子组件的 aboutToDisappear,然后执行初始化新页面的生命周期流程。
  • 点击返回按钮,触发页面生命周期 Index onBackPress,且触发返回一个页面后会导致当前 Index 页面被销毁。
  • 最小化应用或者应用进入后台,触发 Index onPageHide。当前 Index 页面没有被销毁,所以并不会执行组件的 aboutToDisappear。应用回到前台,执行 Index onPageShow。
  • 退出应用,执行 Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。

自定义组件监听页面生命周期

使用无感监听页面路由的能力,能够实现在自定义组件中监听页面的生命周期。

ts
// Index.ets
import { uiObserver, router, UIObserver } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  listener: (info: uiObserver.RouterPageInfo) => void = (info: uiObserver.RouterPageInfo) => {
    let routerInfo: uiObserver.RouterPageInfo | undefined = this.queryRouterPageInfo();
    if (info.pageId == routerInfo?.pageId) {
      if (info.state == uiObserver.RouterPageState.ON_PAGE_SHOW) {
        console.log(`Index onPageShow`);
      } else if (info.state == uiObserver.RouterPageState.ON_PAGE_HIDE) {
        console.log(`Index onPageHide`);
      }
    }
  }
  aboutToAppear(): void {
    let uiObserver: UIObserver = this.getUIContext().getUIObserver();
    uiObserver.on('routerPageUpdate', this.listener);
  }
  aboutToDisappear(): void {
    let uiObserver: UIObserver = this.getUIContext().getUIObserver();
    uiObserver.off('routerPageUpdate', this.listener);
  }
  build() {
    Column() {
      Text(`this page is ${this.queryRouterPageInfo()?.pageId}`)
        .fontSize(25)
      Button("push self")
        .onClick(() => {
          router.pushUrl({
            url: 'pages/Index'
          })
        })
      Column() {
        SubComponent()
      }
    }
  }
}
@Component
struct SubComponent {
  listener: (info: uiObserver.RouterPageInfo) => void = (info: uiObserver.RouterPageInfo) => {
    let routerInfo: uiObserver.RouterPageInfo | undefined = this.queryRouterPageInfo();
    if (info.pageId == routerInfo?.pageId) {
      if (info.state == uiObserver.RouterPageState.ON_PAGE_SHOW) {
        console.log(`SubComponent onPageShow`);
      } else if (info.state == uiObserver.RouterPageState.ON_PAGE_HIDE) {
        console.log(`SubComponent onPageHide`);
      }
    }
  }
  aboutToAppear(): void {
    let uiObserver: UIObserver = this.getUIContext().getUIObserver();
    uiObserver.on('routerPageUpdate', this.listener);
  }
  aboutToDisappear(): void {
    let uiObserver: UIObserver = this.getUIContext().getUIObserver();
    uiObserver.off('routerPageUpdate', this.listener);
  }
  build() {
    Column() {
      Text(`SubComponent`)
    }
  }
}

静态资源存放目录

1

  • 应用级别的静态资源: AppScope/resources 目录
    • base/element:存放一些变量,通过 $r('app.xxxxxx') 访问。
    • base/media:存放一些图片,视频等媒体文件,通过 $r('app.media.xxxxxx') 访问。如果跟模块下面的资源目录存在重名文件,编译打包后只会保留 AppScope 目录下的资源文件。
  • 模块级别的静态资源: 模块目录/src/main/resources 目录
    • base/element:存放一些变量,通过 $r('app.xxxxxx') 访问。
    • base/media:存放一些图片,视频等媒体文件,通过 $r('app.media.xxxxxx') 访问。
    • rawfile:存放一些图片,视频等媒体文件,通过 $rawfile('xxxxx') 访问。找个目录下的文件不会经过编译加上 hash,而是直接复制 copy 一份。

组件级状态(响应式变量)管理

官网文档

为了方便开发者管理组件状态,ArkTS 提供了一系列状态相关的装饰器,例如 @State,@Prop,@Link,@Provide 和@Consume 等等。

@State

@State 用于装饰当前组件的状态变量,@State 装饰的变量发生变化时会驱动当前组件的视图刷新。@State 装饰的变量必须进行初始化。

ts
@State count:number = 1;
  • @State 允许装饰的变量类型有 string、number、boolean、object、class 和 enum 类型,以及这些类型的数组。
  • 当@State 装饰的变量类型为 boolean、string、number 类型时,可以观察到赋值的变化
  • 当@State 装饰的变量类型为 class 或者 object 时,可以观察到变量自身赋值的变化,和其属性赋值的变化。需要注意的是,若某个属性也为 class 或者 object,则嵌套属性的变化是观察不到的。
ts
//类型定义
class Employee {
  name: string;
  age: number;
  job: Job;

  constructor(name: string, age: number, job: Job) {
    this.name = name;
    this.age = age;
    this.job = job;
  }
}

class Job {
  name: string;
  salary: number;

  constructor(name: string, salary: number) {
    this.name = name;
    this.salary = salary;
  }
}

//状态定义
@State employee: Employee = new Employee('张三', 28, new Job('销售', 8000))

//状态操作
employee = new Employee('李四', 26, new Job('行政', 6000))//状态变量重新赋值,可以观察到
employee.age++;//修改状态变量的属性,可以观察到
employee.job.salary++;//修改状态变量的属性的属性,不可以观察到
  • 当@State 装饰的变量类型为数组时,可以观察到数组本身赋值的变化,和其元素的添加、删除及更新的变化,若元素类型为 class 或者 object 时,元素属性的变化,是观察不到的。
ts
//类型定义
class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

//状态定义
@State persons: Person[] = [new Person('张三', 19), new Person('李四', 20)];

//状态操作
persons = [];//状态变量重新赋值,可以观察到
persons.push(new Person('王五',21));//新增数组元素,可以观察到
persons[0]=new Person('张三',22);//对数组元素重新赋值,可以观察到
persons[1].age++;//修改数组元素的属性,不可以观察到

@Prop

@Prop 也可用于装饰状态变量,@Prop 装饰的变量发生变化时也会驱动当前组件的视图刷新,除此之外,@Prop 装饰的变量还可以同步父组件中的状态变量,但只能单向同步,也就是父组件的状态变化会自动同步到子组件,而子组件的变化不会同步到父组件。

@Prop 允许装饰的变量类型有 string、number、boolean、enum,注意不支持 class、object 和数组。

注意:@Prop 装饰的变量不允许初始化,只能通过父组件向子组件传参。

ts
// 父组件
@Entry
@Component
struct Parent{
  @State count:number = 1;
  build(){
    Column(){
      Child({count:this.count});
    }
  }
}
ts
// 子组件
@Component
struct Child{
  @Prop count:number;
  build(){
    Text(this.count.toString());
  }
}

@Link 也可用于装饰状态变量,@Link 装饰的变量发生变化时也会驱动当前组件的视图刷新,除此之外,@Link 变量同样会同步父组件状态,并且能够双向同步。也就是父组件的变化会同步到子组件,子组件的变化也会同步到父组件。

  • 允许装饰的变量类型同 @State
  • 能够响应式的变量类型跟规则同 @State

注意:@Link 装饰的变量不允许初始化,只能由父组件通过传参进行初始化,并且父组件必须使用 $ 变量名的方式传参,以表示传递的是变量的引用。

ts
// 父组件
@Entry
@Component
struct Parent{
  @State count:number = 1;
  build(){
    Column(){
      Child({count: $count});
    }
  }
}
ts
// 子组件
@Component
struct Child{
  @Link count:number;
  build(){
    Text(this.count.toString()).onClick(() => {
      this.count = 3
    })
  }
}

@Provide 和@Consume

@Provide 和@Consume 用于跨组件层级传递状态信息,其中@Provide 用于装饰祖先组件的状态变量,@Consume 用于装饰后代组件的状态变量。可以理解为祖先组件提供(Provide)状态信息供后代组件消费(Consume),并且祖先和后代的状态信息可以实现双向同步。

  • 允许装饰的变量类型同 @State
  • 能够响应式的变量类型跟规则同 @State

注意:@Provide 装饰变量必须进行初始化,而@Consume 装饰的变量不允许进行初始化。另外,@Provide 和@Consume 装饰的变量不是通过父组件向子组件传参的方式进行绑定的,而是通过相同的变量名进行绑定的。

ts
// 祖先组件
@Entry
@Component
struct GrandParent {
  @Provide count: number = 1;
  build() {
    Column() {
      Parent()
    }
  }
}
ts
// 父组件
@Component
struct Parent {
  build() {
    Column() {
      Child()
    }
  }
}
ts
// 子组件
@Component
struct Child {
  @Consume count:number;
  build() {
    Column() {
      Text(this.count.toString());
    }
  }
}

除了通过变量名进行绑定,还可通过变量的别名进行绑定,具体语法如下

ts
// 祖先组件
@Entry
@Component
struct GrandParent {
  @Provide('count') parentCount: number = 1;
  build() {
    Column() {
      Parent()
    }
  }
}
ts
// 父组件
@Component
struct Parent {
  build() {
    Column() {
      Child()
    }
  }
}
ts
// 子组件
@Component
struct Child {
  @Consume('count') childCount:number;
  build() {
    Column() {
      Text(this.childCount.toString());
    }
  }
}

前面所述的装饰器都仅能观察到状态变量第一层的变化,而第二层的变化是观察不到的。如需观察到这些状态变量第二层的变化,则需要用到@ObjectLink 和@Observed 装饰器。

用法为:

  • 数组里的对象需要用 class 类生成, calss 需要用 @Observed 装饰器进行装饰。
  • 在子组件中接收的状态变量需要使用 @ObjectLink 装饰。
ts
@Observed
class Job {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

@Entry
@Component
struct Pages {
  @State list: Job[] = [
    new Job('名字1', 18),
    new Job('名字2', 19),
    new Job('名字3', 20),
    new Job('名字4', 21),
    new Job('名字5', 22),
  ]

  build() {
    Column() {
      ForEach(this.list, (item: Job) => {
        Child({ job: item })
      })
    }
    .height('100%')
    .width('100%')
  }
}


@Component
struct Child {
  @ObjectLink job: Job;

  build() {
    Column() {
      Text(`名字:${this.job.name};年龄:${this.job.age}`)
        .fontSize('16fp')
        .margin({bottom: 10})
        .onClick(() => {
          // 允许
          this.job.age = parseInt((Math.random() * 10).toString())
          // 不允许
          // this.job = {name: 'dd', age: 8}
        })
    }
  }
}

应用级状态管理

官网文档

LocalStorage

LocalStorage 用于存储页面级的状态数据,位于 LocalStorage 中的状态数据可以在一个页面内的所有组件中共享。

  • 创建 LocalStorage 实例,并初始化状态变量 new LocalStorage({})
  • 将 LocalStorage 实例绑定到页面的入口组件 @Entry(storage)
  • 在页面内的组件访问 LocalStorage。ArkTs 提供了两个装饰器用于访问 LocalStorage,分别是@LocalStorageProp 和@LocalStorageLink,前者可以和 LocalStorage 实现单向同步,后者可以和 LocalStorage 实现双向同步,具体用法如下

如何获取状态变量:

  • 组件外部获取: let message: string = storage.get('message')
  • 组件内部获取: @LocalStorageLink('message')@LocalStorageProp('message')
ts
interface StorageInterface {
  message: string
  age: number
}
let para: StorageInterface = { message: '你好啊', age: 18 };

let storage = new LocalStorage(para)

@Entry(storage)
@Component
struct Pages {

  build() {
    Column() {
      Child()
    }
    .height('100%')
    .width('100%')
  }
}


@Component
struct Child {
  @LocalStorageLink('message') message: string = '';
  @LocalStorageProp('age') num: number = 1

  build() {
    Column() {
      Text(this.num.toString())
        .fontSize('16fp')
        .margin({bottom: 10})
      Text(this.message)
        .fontSize('16fp')
        .margin({bottom: 10})
        .onClick(() => {
          this.message = '水电费第三方是'
        })
    }
  }
}

AppStorage

AppStorage 用于存储应用级的状态数据,位于 AppStorage 中的状态数据可以在整个应用的所有组件中共享。

  • 初始化状态变量。AppStorage.setOrCreate('message', 'iu')
  • ArkTs 提供了两个装饰器用于访问 AppStorage 实例,分别是@StorageProp 和@StorageLink,前者可以和 AppStorage 实现单向同步,后者可以和 AppStorage 实现双向同步,具体用法如下

如何获取状态变量:

  • 组件外部获取: let message: string = AppStorage.get('message')
  • 组件内部获取: @StorageLink('message')@StorageProp('message')
ts
AppStorage.setOrCreate('message', 'iu')
AppStorage.setOrCreate('age', 18)

@Entry()
@Component
struct Pages {

  build() {
    Column() {
      Child()
    }
    .height('100%')
    .width('100%')
  }
}


@Component
struct Child {
  @StorageLink('message') message: string = '';
  @StorageProp('age') num: number = 1

  build() {
    Column() {
      Text(this.num.toString())
        .fontSize('16fp')
        .margin({bottom: 10})
      Text(this.message)
        .fontSize('16fp')
        .margin({bottom: 10})
        .onClick(() => {
          this.message = '水电费第三方是'
        })
    }
  }
}

PersistentStorage

LocalStorage 和 AppStorage 都是将状态数据保存在内存中,应用一旦退出,数据就会被清理

如果需要对数据进行持久化存储,就需要用到 PersistentStorage,PersistentStorage 可以将指定的 AppStorage 中的属性保存到磁盘中,并且 PersistentStorage 和 AppStorage 的该属性会自动建立双向同步,开发者不能直接访问 PersistentStorage 中的属性,而只能通过 AppStorage 进行访问

PersistentStorage不允许的类型和值有:

  • 不支持嵌套对象(对象数组,对象的属性是对象等)。因为目前框架无法检测AppStorage中嵌套对象(包括数组)值的变化,所以无法写回到PersistentStorage中。

PersistentStorage的持久化变量最好是小于2kb的数据,不要大量的数据持久化,因为PersistentStorage写入磁盘的操作是同步的,大量的数据本地化读写会同步在UI线程中执行,影响UI渲染性能。如果开发者需要存储大量的数据,建议使用数据库api。

PersistentStorage和UI实例相关联,持久化操作需要在UI实例初始化成功后(即loadContent传入的回调被调用时)才可以被调用,早于该时机调用会导致持久化失败。

如何获取状态变量:

  • 组件外部获取: let message: string = AppStorage.get('message')
  • 组件内部获取: @StorageLink('message')@StorageProp('message')
ts
PersistentStorage.persistProp('message', 'iu')
PersistentStorage.persistProp('age', 18)

@Entry()
@Component
struct Pages {

  build() {
    Column() {
      Child()
    }
    .height('100%')
    .width('100%')
  }
}


@Component
struct Child {
  @StorageLink('message') message: string = '';
  @StorageProp('age') num: number = 1

  build() {
    Column() {
      Text(this.num.toString())
        .fontSize('16fp')
        .margin({bottom: 10})
      Text(this.message)
        .fontSize('16fp')
        .margin({bottom: 10})
        .onClick(() => {
          this.message = '水电费第三方是'
        })
    }
  }
}