在 HarmonyOS 应用开发中,数据持久化是个很常见的需求。你有没有遇到过这种情况:辛辛苦苦存起来的对象,取出来的时候子属性全没了?或者控制台一堆反序列化失败的报错?别慌,今天咱们就来聊聊解决这个问题的关键角色 ——@Type 装饰器。

从 API version 12 开始,HarmonyOS 的 ArkTS 里新增了 @Type 装饰器,它就像给对象的属性贴了个 “身份证”,告诉系统 “这个属性是啥类型,序列化的时候可别搞错了”。尤其是配合 PersistenceV2 使用时,能帮咱们完美解决复杂对象持久化时的类型丢失问题。接下来,咱们就一点点扒开 @Type 的神秘面纱,从基础用法到避坑指南,再到实战案例,保证让你看完就会用。

重点 1:@Type 装饰器到底是个啥?为啥需要它?

咱们先搞明白最基本的:@Type 装饰器到底能干啥?

简单说,@Type 的核心作用就是 “标记类属性的类型”。当咱们把对象序列化(比如存到本地)的时候,系统会把对象转成一种可存储的格式;而反序列化时,又要把这种格式转回原来的对象。但如果对象里有复杂的子对象(比如自定义类的实例),系统可能就 “认不出” 它们的类型了,反序列化之后就会丢数据或者变成 undefined。这时候,@Type 就派上用场了 —— 它明确告诉系统 “这个属性是 XX 类型的”,确保反序列化能成功。

举个例子:如果咱们有个 Sample 类,里面有个 SampleChild 类型的属性,要是不给这个属性加 @Type (SampleChild),存的时候可能没问题,但取出来的时候,这个 SampleChild 属性很可能就恢复不了,变成 null 或者 undefined。

不过要注意,@Type 可不是凭空出现的,它从 API version 12 才开始支持,所以如果你的项目还在用低于 12 的 API 版本,那暂时用不了这个功能哦。

// 简单感受下@Type的作用:标记属性类型
import { Type } from '@kit.ArkUI';
@ObservedV2 // 注意:@Type必须用在@ObservedV2装饰的类里,后面会详细说
class Child {
  value: number = 0;
}

@ObservedV2
class Parent {
  // 告诉系统:这个child属性是Child类型的
  @Type(Child)
  child: Child = new Child(); 
}

重点 2:@Type 装饰器的用法细节:参数和可装饰类型

咱们再聊聊 @Type 具体怎么用。首先看它的参数,特别简单,就一个 ——type,也就是你要标记的属性的类型。比如 @Type (SampleChild),这里的 SampleChild 就是参数,告诉系统这个属性是 SampleChild 类的实例。

那哪些类型能被 @Type 装饰呢?文档里说的很清楚,主要是这几类:

  • 自定义的 Object class(也就是咱们自己定义的类);
  • 内嵌类型,比如 Array、Date、Map、Set 这些。

举个例子,如果你有个属性是 Date 类型,虽然 Date 是内置类型,但它也算复杂类型,这时候就可以用 @Type (Date) 来标记;如果是自定义的 User 类,那就是 @Type (User)。

// @Type参数和可装饰类型示例
import { Type } from '@kit.ArkUI';

@ObservedV2
class MyClass {
  // 自定义类类型
  @Type(Child)
  child: Child = new Child();

  // 内嵌类型Date
  @Type(Date)
  createTime: Date = new Date();

  // 内嵌类型Array(注意:数组元素如果是复杂类型,可能还需要额外处理)
  @Type(Child) // 这里标记数组元素的类型是Child
  childList: Child[] = [new Child()];
}

@ObservedV2
class Child {
  name: string = "test";
}

不过这里要提醒一句,虽然 Array 是支持的,但如果数组里的元素是复杂类型,那也得确保这些元素的类型是被正确标记的,否则数组反序列化的时候,里面的元素可能还是会出问题。

重点 3:这些坑千万别踩!@Type 的使用限制

讲完了用法,咱们得重点说说 @Type 的使用限制。这部分特别重要,稍微不注意就会踩坑,导致编译报错或者运行时出问题。咱们一条条来看:

限制 1:只能用在 @ObservedV2 装饰的类中,不能用在 @Observed 或自定义组件里

这是最容易出错的一点。@Type 有个 “死规定”:必须跟 @ObservedV2 搭配,只能用在被 @ObservedV2 装饰的类里。如果用在被 @Observed(注意是不带 V2 的旧版本)装饰的类里,或者用在自定义组件(@ComponentV2 装饰的 struct)里,直接就会编译报错。

// 正确用法:@Type用在@ObservedV2装饰的类中
import { Type, ObservedV2, Observed } from '@kit.ArkUI';

class Sample {
  data: number = 0;
}

@ObservedV2
class Info {
  @Type(Sample)
  sample: Sample = new Sample(); // 没问题,编译通过
}

// 错误用法1:用在@Observed装饰的类中
@Observed
class Info2 {
  @Type(Sample)
  sample: Sample = new Sample(); // 报错!@Observed不行,必须是@ObservedV2
}

// 错误用法2:用在自定义组件里
@ComponentV2
struct MyComponent {
  @Type(Sample)
  sample: Sample = new Sample(); // 报错!自定义组件里不能用@Type
  build() {}
}

为啥会有这限制呢?因为 @ObservedV2 和 @Observed 是不同版本的状态管理装饰器,@Type 是为新版本的 @ObservedV2 设计的,两者的内部机制不兼容。而自定义组件有自己的属性管理方式,不支持 @Type 这种类型标记。

限制 2:不支持 collections.Set、collections.Map 等类型

这里要注意,虽然文档里说支持 Set、Map 等内嵌类型,但特指的是内置的 Set、Map,而不是 collections 模块下的 Set 和 Map。如果你用的是 import {Set} from '@ohos.collections' 这种,那 @Type 是不支持的。

// 错误用法:使用collections.Set
import { Type } from '@kit.ArkUI';
import { Set } from '@ohos.collections';

@ObservedV2
class MyClass {
  @Type(Set) // 报错!不支持collections.Set
  mySet: Set<number> = new Set();
}

// 正确用法:使用内置Set
@ObservedV2
class MyClass2 {
  @Type(Set) // 可以,内置Set是支持的
  mySet: Set<number> = new Set();
}

限制 3:不支持非 buildin 类型(比如 Native 类型)

像 PixelMap(图片像素数据)、NativePointer(原生指针)、ArrayList 这些 Native 类型,还有一些系统提供的非内置类型,@Type 都不支持。因为这些类型的内部结构比较特殊,序列化和反序列化机制跟普通对象不一样,@Type 处理不了。

// 错误用法:使用非buildin类型
import { Type } from '@kit.ArkUI';
import { PixelMap } from '@ohos.multimedia.image';

@ObservedV2
class MyClass {
  @Type(PixelMap) // 报错!PixelMap是Native类型,不支持
  image: PixelMap | null = null;
}

限制 4:不支持简单类型(string、number、boolean 等)

简单类型根本不需要 @Type,因为系统本身就能识别它们的类型,序列化和反序列化不会出问题。如果你给 string、number 这些加 @Type,反而会报错。

// 错误用法:给简单类型加@Type
import { Type } from '@kit.ArkUI';

@ObservedV2
class MyClass {
  @Type(String) // 报错!string是简单类型,不需要@Type
  name: string = "test";

  @Type(Number) // 报错!number也不行
  age: number = 18;
}

限制 5:不支持构造函数含参的类

如果你的自定义类的构造函数是带参数的,比如 class Person {constructor (name: string) {} },那 @Type 是不支持这种类的。因为反序列化的时候,系统需要用无参构造函数来创建实例,如果构造函数有参数,就没办法正确初始化了。

// 错误用法:构造函数含参的类
import { Type } from '@kit.ArkUI';

// 带参数的构造函数
class Person {
  name: string;
  constructor(name: string) { // 有参数
    this.name = name;
  }
}

@ObservedV2
class MyClass {
  @Type(Person) // 报错!Person的构造函数有参数,不支持
  person: Person = new Person("张三");
}

// 正确用法:构造函数无参
class Person2 {
  name: string = "张三"; // 无参构造函数,默认的
}

@ObservedV2
class MyClass2 {
  @Type(Person2) // 可以,构造函数无参
  person: Person2 = new Person2();
}

这一点特别容易被忽略,很多时候咱们定义类的时候会习惯性地加带参构造函数,结果用 @Type 的时候就出问题了。所以如果某个类需要被 @Type 标记,一定要确保它的构造函数是无参的。

重点 4:实战!用 @Type 搞定数据持久化

了解了这么多理论,咱们来看看 @Type 最常用的场景 —— 数据持久化。在 HarmonyOS 开发中,咱们经常需要把应用的数据存起来,比如用户设置、缓存信息等,这时候就会用到 PersistenceV2 这个工具。而当要持久化的对象包含复杂子对象时,@Type 就成了必不可少的 “好帮手”。

咱们通过一个完整的例子,看看 @Type 在持久化中是怎么工作的。

步骤 1:定义需要持久化的类

首先,咱们需要定义几个类,用来存储数据。假设我们有个 User 类,里面包含一个 Address 类型的子对象,还有一个 Date 类型的注册时间。

import { ObservedV2, Type } from '@kit.ArkUI';

// 地址类:包含街道和门牌号
@ObservedV2
class Address {
  street: string = "Main Street";
  number: number = 100;
}

// 用户类:包含姓名、地址(复杂对象)、注册时间(Date类型)
@ObservedV2
class User {
  name: string = "小明"; // 简单类型,不需要@Type

  // 复杂对象Address,必须用@Type标记,否则反序列化会失败
  @Type(Address)
  address: Address = new Address();

  // Date类型是内嵌复杂类型,需要@Type标记
  @Type(Date)
  registerTime: Date = new Date();
}

这里要注意:Address 和 User 都必须用 @ObservedV2 装饰,因为 @Type 只能用在这样的类里。Address 是 User 的子对象,属于复杂类型,所以 User 里的 address 属性必须加 @Type (Address);registerTime 是 Date 类型,也需要 @Type (Date)。

步骤 2:在组件中使用 PersistenceV2 进行持久化

接下来,咱们在自定义组件里用 PersistenceV2 来保存和读取 User 对象。PersistenceV2.connect 用来连接持久化存储,获取已保存的对象;PersistenceV2.save 用来保存对象的最新状态。

import { PersistenceV2 } from '@kit.ArkUI';
import { ComponentV2, Entry, Column, Text, Button } from '@kit.ArkUI';

@Entry
@ComponentV2
struct PersistenceDemo {
  // 连接持久化存储,获取User对象(如果没有保存过,就用new User()初始化)
  @Local user: User = PersistenceV2.connect(User, () => new User())!;

  build() {
    Column({ space: 20 }) {
      // 展示当前用户信息
      Text(`用户名:${this.user.name}`)
        .fontSize(18)
      Text(`地址:${this.user.address.street} ${this.user.address.number}`)
        .fontSize(18)
      Text(`注册时间:${this.user.registerTime.toLocaleString()}`)
        .fontSize(18)

      // 按钮1:修改地址信息并保存
      Button("修改地址")
        .onClick(() => {
          this.user.address.street = "Second Street";
          this.user.address.number = 200;
          PersistenceV2.save(User); // 保存修改
        })

      // 按钮2:修改注册时间并保存
      Button("更新注册时间")
        .onClick(() => {
          this.user.registerTime = new Date();
          PersistenceV2.save(User); // 保存修改
        })
    }
    .padding(30)
  }
}

步骤 3:验证 @Type 的作用

现在,咱们来看看如果去掉 @Type 会发生什么。假设咱们把 User 类里 address 属性的 @Type (Address) 去掉:

// 错误示例:去掉@Type标记
@ObservedV2
class User {
  name: string = "小明";

  // 去掉了@Type(Address)
  address: Address = new Address(); // 没有@Type

  @Type(Date)
  registerTime: Date = new Date();
}

这时候,当咱们第一次运行应用,修改地址并保存,看起来没问题。但当咱们关闭应用再重新打开时,会发现 address 属性的值恢复成了初始值(Main Street 100),而不是咱们修改后的(Second Street 200)。这就是因为没有 @Type 标记,系统在反序列化时 “认不出” address 是 Address 类型,没办法正确恢复它的值。

而加上 @Type (Address) 之后,重新运行应用,修改地址并重启,会发现地址信息能正确恢复,这就是 @Type 的作用。

步骤 4:注意无初值属性的处理

还有个细节要注意:如果类的属性没有初始值,就算加了 @Type,也需要手动保存,否则持久化会失败。比如:

@ObservedV2
class User {
  @Type(Address)
  address?: Address; // 没有初值(注意是可选属性)

  // 正确做法:如果有这种无初值的属性,需要在赋值后手动调用save
  // 比如在组件里给address赋值后,必须调用PersistenceV2.save(User)
}

如果 address 没有初值,当咱们在代码里给它赋值后,一定要记得调用 PersistenceV2.save,否则这个属性的值不会被持久化。

重点 5:常见问题排查:为啥我的 @Type 不好使?

用 @Type 的时候,可能会遇到各种问题,比如反序列化失败、编译报错等。咱们来总结几个常见问题和排查方向:

问题 1:编译时报错 “@Type can only be used in classes decorated with @ObservedV2”

这是最常见的错误,原因就是 @Type 用在了不是 @ObservedV2 装饰的类里。排查步骤:

  • 检查包含 @Type 的类是不是被 @ObservedV2 装饰了;
  • 确保没有用 @Observed(不带 V2)装饰这个类;
  • 确保这个类不是自定义组件(struct + @ComponentV2)。

问题 2:反序列化后,复杂属性变成了 undefined

这种情况通常是因为没给复杂属性加 @Type。排查步骤:

  • 检查所有自定义类类型的属性,是否都加了 @Type (对应的类);
  • 确认这些自定义类是否都被 @ObservedV2 装饰了;
  • 检查属性的类型是不是 @Type 支持的(比如是不是构造函数无参)。

问题 3:持久化后,无初值的属性没有被保存

原因是无初值的属性需要手动保存。解决办法:

  • 在给无初值的属性赋值后,立即调用 PersistenceV2.save (类名);
  • 尽量给属性设置初始值,减少手动保存的麻烦。

问题 4:使用 Date 类型时,反序列化后时间不对

可能是因为没给 Date 类型加 @Type (Date)。虽然 Date 是内置类型,但也需要 @Type 标记才能正确反序列化。解决办法:给 Date 类型的属性加上 @Type (Date)。

总结:@Type 装饰器的核心要点

不知不觉说了这么多,最后咱们再来总结一下 @Type 装饰器的核心要点,帮你快速记住:

  1. 作用:标记类属性的类型,配合 PersistenceV2 使用,防止序列化 / 反序列化时丢失类型信息;
  2. 版本:从 API 12 开始支持;
  3. 用法:参数是属性的类型(如 @Type (Address)),可装饰自定义类、Array、Date、Map、Set 等;
  4. 限制:必须用在 @ObservedV2 类中,不支持简单类型、带参构造函数的类、Native 类型等;
  5. 关键场景:数据持久化时,复杂子对象必须用 @Type 标记,否则反序列化失败。

掌握了 @Type,你在 HarmonyOS 开发中处理复杂对象的持久化就会得心应手,再也不用担心存进去的数据取不出来了。下次开发的时候,记得把今天学的知识点用起来,避开那些容易踩的坑,让你的应用数据管理更靠谱!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐