在鸿蒙应用开发中,你是不是经常遇到这样的问题:不同页面之间要共享数据,来回传递参数太麻烦;全局状态不好管理,改个值到处都要同步…… 别担心,今天咱们就来聊聊 API 12 开始支持的 AppStorageV2,它可是解决这些问题的好帮手。

一、AppStorageV2 到底是个啥?

简单说,AppStorageV2 就是应用级别的全局 UI 状态存储器。它有几个很关键的特点,咱们得先搞清楚:

  1. 单例模式:应用一启动就会创建,整个应用里就这么一个实例,保证数据一致性。
  2. 全局可访问:不管你在哪个页面、哪个 UIAbility 里,只要用对方法,都能访问到里面的数据。
  3. 跨 UIAbility 共享:同一个应用里的多个 UIAbility 实例,也能通过它共享状态,这可比以前方便多了。
  4. 独立存储:注意哦,它和老的 AppStorage 数据是不互通的,别搞混了。
  5. 数据持久:只要应用在运行,里面的数据就一直存在,不会随便丢。

咱们用个简单的代码片段感受下它的基本用法(后面会详细说每个方法):

// 先定义一个要存储的数据类型
@ObservedV2
class UserInfo {
  name: string = "默认名称"
  age: number = 18
}

// 在页面中获取或创建数据
let user = AppStorageV2.connect(UserInfo, () => new UserInfo())

这几行代码就完成了基本的数据存储和获取,是不是很简单?接下来咱们逐个拆解它的核心功能。

二、核心方法大揭秘

AppStorageV2 主要有三个方法:connect、remove 和 keys。这三个方法可是它的灵魂,咱们一个一个说清楚。

1. connect 方法:创建或获取数据的万能钥匙

connect 方法是最常用的,既能创建新数据,也能获取已有的数据。它的定义长这样:

static connect<T extends object>(
  type: TypeConstructorWithArgs<T>,
  keyOrDefaultCreator?: string | StorageDefaultCreator<T>,
  defaultCreator?: StorageDefaultCreator<T>
): T | undefined;

看着参数有点多,别怕,咱们慢慢捋。

参数说明

  • type:指定数据的类型,比如上面的 UserInfo。如果没指定 key,就用这个类型的 name 当 key。
  • keyOrDefaultCreator:可以是指定的 key,也可以是默认数据的构造器(就是一个能创建默认数据的函数)。
  • defaultCreator:默认数据的构造器,当第二个参数是 key 时,就用这个来创建默认数据。

返回值:成功的话返回对应的数据,失败就返回 undefined。

这里有几个特别重要的注意点,咱们得敲黑板:

  1. key 和构造器的搭配规则

    • 没指定 key 的时候,第二个参数就当构造器用;
    • 指定了 key,就用第三个参数当构造器;
    • 要是第二个参数不合法,也会用第三个参数。
  2. 必须有默认构造器

    • 如果要获取的数据还没存过,那就必须指定默认构造器,不然应用会出问题;
    • 要是数据已经存在了,那就可以不用构造器,直接获取。
  3. 类型必须一致

    • 同一个 key,前后 connect 的类型必须一样,不然应用会异常。
  4. key 的命名规矩

    • 可以用字母、数字、下划线,长度不能超过 255;
    • 别用非法字符或者空字符,不然行为是未定义的(简单说就是可能出各种奇怪的问题)。
  5. @Observed 对象的特殊处理

    • 因为 @Observed 修饰的类 name 属性可能没定义,所以要么指定 key,要么给它自定义一个 name 属性。

咱们来几个例子,看看不同情况下怎么用:

例子 1:不指定 key,用类型名当 key

@ObservedV2
class Message {
  content: string = "初始消息"
  read: boolean = false
}

// 因为没指定key,会用Message作为key
// 第二个参数是构造器,当数据不存在时会调用new Message()创建
let msg = AppStorageV2.connect(Message, () => new Message())

if (msg) {
  console.log("获取到的消息:" + msg.content) // 输出"初始消息"
}

例子 2:指定 key,用第三个参数当构造器

@ObservedV2
class Setting {
  theme: string = "light"
  fontSize: number = 16
}

// 第一个参数是类型,第二个是key,第三个是构造器
let appSetting = AppStorageV2.connect(Setting, "app_main_setting", () => new Setting())

if (appSetting) {
  appSetting.theme = "dark" // 修改主题为深色
}

例子 3:获取已存在的数据(不用构造器)

// 假设之前已经存储了key为"app_main_setting"的Setting数据
let existingSetting = AppStorageV2.connect(Setting, "app_main_setting")

if (existingSetting) {
  console.log("当前主题:" + existingSetting.theme) // 会输出之前设置的"dark"
}

例子 4:处理 @Observed 对象(指定 key)

@ObservedV2
class CartItem {
  id: string = ""
  name: string = ""
  price: number = 0
}

// 因为CartItem是@Observed的,这里明确指定key为"user_cart"
let cart = AppStorageV2.connect(CartItem, "user_cart", () => {
  let item = new CartItem()
  item.id = "default_001"
  item.name = "默认商品"
  item.price = 99
  return item
})

通过这些例子,是不是对 connect 方法清晰多了?它就像一把万能钥匙,能帮你搞定数据的创建和获取。

2. remove 方法:删除数据很简单

当你不需要某个数据的时候,就可以用 remove 方法把它从 AppStorageV2 里删掉。方法定义是这样的:

static remove<T>(keyOrType: string | TypeConstructorWithArgs<T>): void;

参数说明

  • 可以是要删除的 key,也可以是类型(这时会用类型的 name 作为 key 来删除)。

注意:如果删除一个不存在的 key,会报警告,但不会让应用崩溃。

来几个例子看看:

例子 1:通过 key 删除

// 删除key为"app_main_setting"的数据
AppStorageV2.remove("app_main_setting")

例子 2:通过类型删除

@ObservedV2
class TempData {
  value: string = ""
}

// 先创建数据
AppStorageV2.connect(TempData, () => new TempData())

// 通过类型删除(会用TempData作为key)
AppStorageV2.remove(TempData)

删除操作很简单,但要注意一旦删除,再用 connect 获取时,如果没有默认构造器就会出问题,所以删除后记得处理后续的获取逻辑。

3. keys 方法:看看里面都有啥数据

keys 方法能返回当前 AppStorageV2 里所有的 key,方便你查看存储了哪些数据。定义很简单:

static keys(): Array<string>;

返回值:一个包含所有 key 的数组。

用起来也超简单:

// 获取所有key
let allKeys = AppStorageV2.keys()

// 打印出来看看
console.log("当前存储的key有:" + allKeys.join(","))

// 也可以在UI上显示
Text(`存储的key列表:${allKeys.join(",")}`)
  .fontSize(16)

这个方法在调试的时候特别有用,能帮你确认数据是不是真的存进去了,或者有没有多余的垃圾数据需要清理。

三、使用限制要注意,不然容易掉坑里

AppStorageV2 虽然好用,但也有一些限制,咱们必须知道,不然写代码的时候容易出各种奇怪的问题。

  1. 只能在 UI 线程使用
    它是给 UI 状态服务的,所以只能在主线程(UI 线程)里用,不能在其他线程(比如后台任务线程)使用,也不支持 @Sendable 装饰器。

    // 错误示例:在后台线程使用
    taskPool.execute(() => {
      // 这样用会出问题!
      AppStorageV2.connect(UserInfo, () => new UserInfo())
    })
    
  2. 不支持这些类型

    • collections.Set、collections.Map 这些集合类型;
    • 非内置类型,比如 PixelMap、NativePointer、ArrayList 等 Native 类型;
    • 基本类型(string、number、boolean 等),注意哦,必须是对象类型才行。
    // 错误示例:存储基本类型
    // 下面这行代码会导致异常,因为number是基本类型
    AppStorageV2.connect(Number, "count", () => 100)
    
    // 正确示例:用对象包装基本类型
    @ObservedV2
    class CountWrapper {
      value: number = 0
    }
    // 这样就没问题了
    AppStorageV2.connect(CountWrapper, "count", () => new CountWrapper())
    
  3. 数据必须是对象类型
    前面也提到了,必须是 object 的子类,所以存储简单数据时记得用对象包装一下。

这些限制看起来有点多,但都是为了保证状态管理的稳定性和性能,只要平时用的时候多注意就行。

四、实战场景:两个页面之间共享数据

光说理论太枯燥,咱们来个实际的例子:两个页面(Page1 和 Page2)通过 AppStorageV2 共享数据。这个场景在实际开发中太常见了,比如用户登录状态、购物车信息等都需要跨页面共享。

1. 先定义共享的数据类型

首先咱们定义一个要共享的数据类,用 @ObservedV2 装饰,这样数据变化时 UI 才能同步更新。

// Sample.ets
@ObservedV2
export class Sample {
  // 用@Trace装饰的属性,修改时会触发UI刷新
  @Trace p1: number = 0;
  // 没有@Trace,修改时不会自动刷新UI
  p2: number = 10;
}

这里 p1 用 @Trace 装饰了,p2 没有,后面咱们会看到它们的区别。

2. 第一个页面(Page1)的实现

Page1 主要负责初始化数据、修改数据、跳转到 Page2,以及展示当前的数据状态。

// Page1.ets
import { AppStorageV2 } from '@kit.ArkUI';
import { Sample } from '../Sample';
@Entry
@ComponentV2
struct Page1 {
  // 连接到Sample类型的数据,没有就创建
  @Local prop: Sample = AppStorageV2.connect(Sample, () => new Sample())!;
  pageStack: NavPathStack = new NavPathStack();

  build() {
    Navigation(this.pageStack) {
      Column({ space: 15 }) {
        // 跳转到Page2的按钮
        Button('跳转到Page2')
          .fontSize(18)
          .padding(10)
          .onClick(() => {
            this.pageStack.pushPathByName('Page2', null);
          })

        // 连接到key为Sample的数据
        Button('Page1连接Sample数据')
          .fontSize(18)
          .padding(10)
          .onClick(() => {
            this.prop = AppStorageV2.connect(Sample, 'Sample', () => new Sample())!;
          })

        // 删除Sample数据
        Button('Page1删除Sample数据')
          .fontSize(18)
          .padding(10)
          .onClick(() => {
            AppStorageV2.remove(Sample);
          })

        // 显示p1并能点击增加
        Text(`Page1中p1的值:${this.prop.p1}(点击+1)`)
          .fontSize(18)
          .onClick(() => {
            this.prop.p1++; // p1有@Trace,修改后UI会刷新
          })

        // 显示p2并能点击增加
        Text(`Page1中p2的值:${this.prop.p2}(点击+1)`)
          .fontSize(18)
          .onClick(() => {
            this.prop.p2++; // p2没有@Trace,修改后UI不会自动刷新
          })

        // 显示所有的key
        Text(`当前存储的key:${AppStorageV2.keys().join(',')}`)
          .fontSize(18)
      }
      .padding(20)
    }
  }
}

这个页面里有几个关键点:

  • 用 AppStorageV2.connect 初始化了 Sample 数据;
  • 有按钮可以跳转到 Page2;
  • 可以手动连接和删除数据;
  • 两个文本分别展示 p1 和 p2,点击能增加数值,但 p1 修改后 UI 会刷新,p2 不会(因为没有 @Trace);
  • 显示当前所有的 key,方便查看数据存储情况。

3. 第二个页面(Page2)的实现

Page2 主要用来展示从 AppStorageV2 获取到的数据,并尝试修改它们,看看是否能和 Page1 同步。

// Page2.ets
import { AppStorageV2 } from '@kit.ArkUI';
import { Sample } from '../Sample';

@Builder
export function Page2Builder() {
  Page2()
}

@ComponentV2
struct Page2 {
  // 同样连接到Sample类型的数据
  @Local prop: Sample = AppStorageV2.connect(Sample, () => new Sample())!;
  pathStack: NavPathStack = new NavPathStack();

  build() {
    NavDestination() {
      Column({ space: 15 }) {
        // 连接到另一个key(Sample1)的按钮
        Button('连接到Sample1数据')
          .fontSize(18)
          .padding(10)
          .onClick(() => {
            this.prop = AppStorageV2.connect(Sample, 'Sample1', () => new Sample())!;
          })

        // 显示p1并能点击增加
        Text(`Page2中p1的值:${this.prop.p1}(点击+1)`)
          .fontSize(18)
          .onClick(() => {
            this.prop.p1++; // 同样,p1修改后UI会刷新
          })

        // 显示p2并能点击增加
        Text(`Page2中p2的值:${this.prop.p2}(点击+1)`)
          .fontSize(18)
          .onClick(() => {
            this.prop.p2++; // p2修改后UI不自动刷新
          })

        // 显示所有的key
        Text(`当前存储的key:${AppStorageV2.keys().join(',')}`)
          .fontSize(18)
      }
      .padding(20)
    }
    .onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
    })
  }
}

Page2 和 Page1 类似,但多了一个连接到 "Sample1" 这个 key 的按钮,用来演示不同 key 的数据是隔离的。

4. 路由配置不能忘

因为咱们用了 Navigation 和 NavDestination 进行页面跳转,所以需要配置路由表,不然跳转会失败。

首先创建 src/main/resources/base/profile/route_map.json 文件:

{
  "routerMap": [
    {
      "name": "Page2",
      "pageSourceFile": "src/main/ets/pages/Page2.ets",
      "buildFunction": "Page2Builder",
      "data": {
        "description": "AppStorageV2示例页面"
      }
    }
  ]
}

然后在 module.json5 中添加路由配置:

{
  // 其他配置...
  "routerMap": "$profile:route_map"
}

这样页面跳转才能正常工作。

5. 实际运行效果讲解

当咱们运行这个应用时,会看到这些效果:

  1. 打开 Page1,会自动创建 key 为 "Sample" 的数据,显示 p1=0,p2=10,key 列表里有 "Sample"。
  2. 点击 Page1 中 p1 的文本,p1 会增加,并且 UI 会实时更新,因为有 @Trace。
  3. 点击 Page1 中 p2 的文本,p2 会增加,但 UI 不会马上变,需要页面重新渲染(比如切换到 Page2 再切回来)才能看到变化。
  4. 点击 "Go to page2" 跳转到 Page2,会发现 Page2 中的 p1 和 p2 和 Page1 中最后显示的一样,说明数据共享成功了。
  5. 在 Page2 中修改 p1 和 p2,回到 Page1 后,会看到 Page1 的数据也跟着变了,证明双向同步有效。
  6. 在 Page2 中点击 "Page2 connect the key Sample1",会创建一个新的 key 为 "Sample1" 的数据,此时 p1 和 p2 会回到初始值(0 和 10),因为连接了新的 key,和之前的 "Sample" 数据隔离开了。
  7. 在 Page1 中点击 "Page1 remove the key Sample",会删除 "Sample" 数据,此时如果再操作 p1 和 p2,可能会出问题(因为数据被删了),这也提醒我们删除数据要谨慎。

通过这个例子,咱们能清楚地看到 AppStorageV2 在跨页面数据共享中的作用,尤其是配合 @ObservedV2 和 @Trace,能很方便地实现 UI 和数据的同步。

五、深入理解:为什么要这么设计?

可能有同学会问,为什么 AppStorageV2 要搞这么多规则,比如必须是对象类型,要用 @ObservedV2 和 @Trace 这些装饰器?

其实这都是为了性能和稳定性。全局状态管理很容易出问题,比如数据混乱、频繁刷新导致性能下降。AppStorageV2 的这些设计能帮我们避免很多坑:

  1. 强制对象类型:基本类型太简单,而实际应用中的状态往往是复杂的对象,统一用对象类型能让状态管理更规范。
  2. @ObservedV2 装饰类:告诉框架这个类的实例可能会被多个 UI 组件观察,需要做好变更跟踪。
  3. @Trace 装饰属性:精确控制哪些属性变化需要触发 UI 刷新,避免不必要的刷新,提高性能。如果所有属性变化都触发刷新,当对象很复杂时,性能会受影响。
  4. 单例模式:保证全局只有一个数据源,避免数据不一致的问题。

理解这些设计理念,能帮助咱们更好地使用 AppStorageV2,写出更高效、更稳定的代码。

六、总结和扩展思考

AppStorageV2 作为 API 12 新增的全局状态管理工具,给鸿蒙应用开发带来了很多便利。咱们来总结一下它的核心要点:

  1. 全局唯一的 UI 状态存储,从 API 12 开始支持。
  2. 三个核心方法:connect(创建 / 获取数据)、remove(删除数据)、keys(获取所有 key)。
  3. 必须存储对象类型,不支持基本类型和某些特殊类型。
  4. 配合 @ObservedV2 和 @Trace 能实现数据和 UI 的高效同步。
  5. 支持跨页面、跨 UIAbility 的数据共享。

在实际开发中,咱们可以用它来存储这些数据:

  • 用户登录状态信息(用户名、头像、权限等);
  • 应用全局设置(主题、字体大小、语言等);
  • 购物车数据(在电商应用中非常有用);
  • 多步骤流程中的临时数据(比如填写表单的中间结果)。

当然,使用的时候也要注意:

  • 不要存储太大的数据,全局状态太大会影响性能;
  • 及时清理不需要的临时数据,避免内存占用过高;
  • 多人开发时,要约定好 key 的命名规则,避免冲突;
  • 复杂状态可以拆分成多个小对象,方便管理和维护。

总的来说,AppStorageV2 是个非常实用的工具,掌握它能让我们的状态管理代码更简洁、更高效。希望这篇文章能帮你彻底搞懂它,在实际项目中用起来!如果还有什么疑问,欢迎一起交流探讨,共同进步!

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐