【HarmonyOS应用开发入门】第六期:状态管理V2入门 - 2

9、@Computed装饰器:计算属性

接上一篇。 小鱼认为 用智能计算器 比喻来解释 @Computed 装饰器再合适不过了,接下来就和小鱼一起学习吧。


比喻:智能计算器

想象 @Computed 就像你家有个智能计算器

  • 原材料 = 依赖的状态变量(@Local@Param 等)
  • 计算器 = @Computed 装饰的 getter 方法
  • 计算结果 = 计算器显示的数字
  • 自动重算 = 原材料变了,计算器自动重新计算

核心机制图示

复制代码
原材料仓库
├── 📦 苹果数量 @Local apples = 5
├── 📦 橙子数量 @Local oranges = 3
└── 📦 单价 @Local price = 10

智能计算器 @Computed
    ↓
总价 = (苹果 + 橙子子) × 单价
    ↓
显示屏:80元

规则解释

i. 只计算,不生产(装饰getter方法)

"计算器只负责计算显示,不能往仓库里放东西(不能修改状态)。"

typescript 复制代码
@Entry
@ComponentV2
struct FruitStore {
  @Local apples: number = 5;
  @Local oranges: number = 3;
  @Local price: number = 10;

  // ✅ 正确:只读计算器
  @Computed
  get totalPrice(): number {
    return (this.apples + this.oranges) * this.price;
  }

  // ❌ 错误:计算器不能生产水果
  @Computed
  get wrongComputed(): number {
    this.apples = 10; // 危险!可能破坏数据追踪
    return 100;
  }
}

ii. 自动重算(依赖变化时)

"只要苹果、橘子、单价任何一个变了,计算器自动重新算总价。"

typescript 复制代码
@Entry
@ComponentV2
struct FruitStore {
  @Local apples: number = 5;
  @Local oranges: number = 3;
  @Local price: number = 10;

  // ✅ 正确:只读计算器
  @Computed
  get totalPrice(): number {
    return (this.apples + this.oranges) * this.price;
  }
  
  build() {
    Column({ space: 20 }) {

      Text(`苹果:${this.apples} 个,橙子: ${this.oranges} 个`)
      Text(`总价格:${this.totalPrice}`)

      Button('加一个苹果').onClick(() => {
        this.apples += 1; // ✅ 重新触发新计算
      })

      Button('加一橙子').onClick(() => {
        this.oranges += 1; // ✅ 重新触发新计算
      })
    }.width('100%')

  }
}

iii. 只算一次(优化性能)

"就算原材料在一瞬间变了好几次,计算器也只最后算一次。"

typescript 复制代码
@Entry
@ComponentV2
struct FruitStore {
  @Local apples: number = 5;
  @Local oranges: number = 3;
  @Local price: number = 10;

  // ✅ 正确:只读计算器
  @Computed
  get totalPrice(): number {
    console.log('苹果:' + this.apples)
    return (this.apples + this.oranges) * this.price;
  }

  build() {
    Column({ space: 20 }) {

      Text(`苹果:${this.apples} 个,橙子: ${this.oranges} 个`)
      Text(`总价格:${this.totalPrice}`)

      Button('加一个苹果').onClick(() => {
        // this.apples += 1;
        // 在一次点击事件中:
        this.apples = 6; // 第一次变化
        this.apples = 7; // 第二次变化
        this.apples = 8; // 第三次变化
      })

      Button('加一个橙子').onClick(() => {
        this.oranges += 1;
      })
    }.width('100%')

  }
}

// 计算器只会:
// 1. 记住最初值:5
// 2. 记住最终值:8  
// 3. 用最终值算一次:(8 + 3) × 10 = 110
// 不会算三次!
// 日志只会打印一次
// 苹果:8

iv. 简单计算不用计算器(开销考虑)

"如果只是算 1+1,心算就行,没必要用智能计算器。"

typescript 复制代码
// ❌ 过度使用:太简单
@Computed
get doubleCount(): number {
  return this.count * 2; // 就一个乘法,直接写 UI 里就行
}

// ✅ 适合场景:复杂计算
@Computed  
get discountPrice(): number {
  const base = this.price * this.quantity;
  const discount = this.vip ? 0.8 : 0.9;
  const tax = this.region === "US" ? 1.08 : 1.06;
  return base * discount * tax;
}

实际使用示例:购物车

typescript 复制代码
@Entry
@ComponentV2
struct ShoppingCart {
  // 原材料
  @Local items: CartItem[] = [];
  @Local taxRate: number = 0.08;
  @Local isVip: boolean = false;

  // 计算器1:商品总数
  @Computed
  get itemCount(): number {
    return this.items.reduce((sum, item) => sum + item.quantity, 0);
  }

  // 计算器2:小计(不含税)
  @Computed
  get subtotal(): number {
    return this.items.reduce((sum, item) =>
    sum + item.price * item.quantity, 0);
  }

  // 计算器3:折扣后价格
  @Computed
  get discountedPrice(): number {
    const discount = this.isVip ? 0.85 : 0.95;
    return this.subtotal * discount;
  }

  // 计算器4:最终含税价
  @Computed
  get finalPrice(): number {
    return this.discountedPrice * (1 + this.taxRate);
  }

  build() {
    Column({ space: 20 }) {
      // 直接使用计算结果,它们会自动更新
      Text(`商品数:${this.itemCount}`)
      Text(`小计:${this.subtotal.toFixed(2)}元`)
      Text(`折扣价:${this.discountedPrice.toFixed(2)}元`)
      Text(`实付:${this.finalPrice.toFixed(2)}元(含税)`)

      Button("添加商品")
        .onClick(() => {
          this.items.push(new CartItem(8.88, 1)); // 所有计算器自动重算
        })
    }.padding(20)
  }
}

@ObservedV2
class CartItem {
  price: number;
  quantity: number;

  constructor(price: number, quantity: number) {
    this.price = price;
    this.quantity = quantity;
  }
}

使用场景判断

应该用 @Computed 的场景:
  1. 复杂公式计算

    typescript 复制代码
    @Computed
    get bmi(): number {
      return this.weight / (this.height * this.height);
    }
  2. 数据过滤/筛选

    typescript 复制代码
    @Computed
    get filteredList(): Item[] {
      return this.items.filter(item => 
        item.price > this.minPrice && item.category === this.selectedCategory
      );
    }
  3. 格式化显示

    typescript 复制代码
    @Computed
    get formattedDate(): string {
      return `${this.year}年${this.month}月${this.day}日`;
    }
  4. 依赖多个状态的计算

    typescript 复制代码
    @Computed
    get canSubmit(): boolean {
      return this.name.length > 0 && 
             this.email.includes('@') && 
             this.agreedToTerms;
    }
不应该用 @Computed 的场景:
  1. 简单计算

    typescript 复制代码
    // 直接写:
    Text(`双倍:${count * 2}`)
    
    // 不用:
    // @Computed get double() { return count * 2; }
  2. 无依赖的计算

    typescript 复制代码
    @Computed
    get constantValue(): number {
      return 100; // 永远不变,没必要用@Computed
    }
  3. 副作用操作

    typescript 复制代码
    @Computed
    get dangerous(): number {
      this.saveToDatabase(); // ❌ 绝对不能有副作用!
      return 0;
    }

性能优化原理

计算器缓存机制:

typescript 复制代码
// 伪代码逻辑
let cachedValue: T = null;
let dependenciesChanged = false;

function computedGetter() {
  if (dependenciesChanged) {
    cachedValue = 重新计算(); // 只算一次
    dependenciesChanged = false;
  }
  return cachedValue; // 直接返回缓存
}

一句话总结

@Computed 就是数据的"智能计算器":

  • 自动计算:依赖的状态变了就自动重算
  • 只算一次:同事件多次变化只最后算一次
  • 只读不写:只能获取结果,不能修改数据
  • 复杂才用:简单计算直接写,复杂计算用计算器
  • 性能优化:缓存结果,避免重复计算

10、PersistenceV2

用用一个"智能保险柜 "的比喻来解释 PersistenceV2


比喻:智能保险柜系统

想象 PersistenceV2 就像你家的智能保险柜系统

  • 保险柜 = PersistenceV2 单例对象
  • 保险箱格子 = 通过 key 存储的数据
  • 钥匙 = connect / globalConnect 方法
  • 自动备份 = 数据变化时自动保存到磁盘
  • 搬家公司 = 应用重启

核心机制图示

复制代码
你家(应用)
├── 智能保险柜(PersistenceV2)
│   ├── 1号格子(key: "user") → 存用户信息 📦
│   ├── 2号格子(key: "settings") → 存设置 📦
│   └── ...(最多约8k大小/格子)
│
├── 客厅(UI组件)
│   └── 📺 电视设置(连接保险柜,自动同步)
└── 卧室(另一个UI组件)
    └── 🛏️ 闹钟设置(连接同一个保险柜)

📡 自动备份到云端(磁盘存储)

规则解释

i. 全家共用一个保险柜(单例对象)

"整个家(应用)只有一个智能保险柜,所有房间(组件)都能访问。"

typescript 复制代码
// 客厅要存用户信息
PersistenceV2.connect<User>(User, () => new User());

// 卧室也能取到同样的用户信息
const sameUser = PersistenceV2.connect<User>(User, () => new User());

ii. 搬家后东西还在(持久化)

"就算全家搬到新房子(应用重启),保险柜里的东西还在。"

typescript 复制代码
// 今天存了主题设置
PersistenceV2.connect<Settings>(Settings, () => new Settings('dark'));

// 明天应用重启后
const theme = PersistenceV2.connect<Settings>(Settings, () => new Settings());
// theme 还是 'dark',不是默认值!

iii. 两种钥匙(connect vs globalConnect)

普通钥匙(connect) = 只能开你房间区域对应的柜子
万能钥匙(globalConnect) = 能开全家的柜子

typescript 复制代码
// ❌ 危险:不要混用同样key
PersistenceV2.connect('data', () => new Data());     // 普通钥匙
PersistenceV2.globalConnect('data', () => new Data()); // 万能钥匙,会冲突!

// ✅ 安全:用不同的key
PersistenceV2.connect('moduleData', () => new Data());
PersistenceV2.globalConnect('globalData', () => new Data());

iv. 智能物品才自动备份(@Trace属性)

"只有贴了'重要'标签(@Trace)的物品,保险柜才会自动拍照备份。"

typescript 复制代码
@ObservedV2
class UserSettings {
  @Trace theme: string = 'light'; // ✅ 自动备份
  fontSize: number = 14;           // ❌ 不自动备份
}

// 当修改:
settings.theme = 'dark';    // ✅ 自动保存到磁盘
settings.fontSize = 16;     // ❌ 需要手动调用 save()

v. 只收贵重物品(必须是class对象)

"保险柜只收包装好的贵重物品(class对象),不收散装货。"

typescript 复制代码
// ✅ 可以存:class对象
class User { name: string = ''; }

// ❌ 不能存:
const num: number = 100;          // 基本类型
const arr: string[] = [];         // 数组
const date: Date = new Date();    // Date对象
const map: Map<string, string>;   // Map对象

vi. 格子大小有限(约8k)

"每个保险箱格子大约能放8k大小的物品,太大的放不下。"

typescript 复制代码
class HugeData {
  // 如果这个对象序列化后超过8k,会保存失败
  data: string = 'x'.repeat(10000); // 危险!可能超限
}

实际使用示例

typescript 复制代码
// 1. 定义可持久化的类
@ObservedV2
class AppSettings {
  @Trace theme: 'light' | 'dark' = 'light';
  @Trace fontSize: number = 14;
  @Trace language: string = 'zh-CN';
  // 这个不会被自动持久化
  temporaryFlag: boolean = false;
}


// 2. 在应用启动时连接
@Entry
@ComponentV2
struct MainPage {
  // 连接保险柜,获取或创建设置
  private settings: AppSettings | undefined = PersistenceV2.connect<AppSettings>(
    AppSettings, () => new AppSettings() // 默认值(第一次运行时用)
  );

  build() {
    Column({ space: 20 }) {
      // 3. 使用数据(会自动同步)
      Text('当前主题')
        .fontSize(this.settings?.fontSize)
        .fontColor(this.settings?.theme === 'dark' ? Color.White : Color.Black)

      Button('切换主题')
        .onClick(() => {
          // 修改数据,会自动持久化到磁盘(因为theme有@Trace)
          if (this.settings) {
            this.settings.theme = this.settings?.theme === 'light' ? 'dark' : 'light';

            // 如果要保存非@Trace的属性,需要手动保存
            this.settings.temporaryFlag = true;
            PersistenceV2.save('app_settings'); // 手动触发保存
          }

        })

      Button('清除设置')
        .onClick(() => {
          // 从保险柜移除这个格子
          PersistenceV2.remove('app_settings');
          // 下次访问会使用默认值
        })
    }.width('100%').height('100%')
    .backgroundColor(this.settings?.theme === 'light' ? Color.White : Color.Black)
  }
}

适用场景

应该用 PersistenceV2 的场景:
  1. 用户偏好设置

    • 主题颜色、字体大小、语言设置
  2. 应用状态恢复

    • 文章阅读进度、视频播放位置、表单草稿
  3. 用户配置信息

    • 通知开关、隐私设置、个性化选项
不应该用 PersistenceV2 的场景:
  1. 大量数据存储 → 用数据库
  2. 非UI相关数据 → 用 Preferences
  3. 频繁读写的大数据 → 性能会差
  4. 基本类型直接存储 → 不支持,要包成class

重要限制总结

限制 解释 解决方案
只能存class对象 不能直接存string/number等 包成class
单个key约8k 太大存不下 拆分数据
只自动存@Trace属性 其他属性不自动存 手动调用save()
不支持循环引用 A引用B,B又引用A 避免循环引用
UI线程专用 不能在Worker/TaskPool用 UI线程操作

与 Preferences 的对比

特性 PersistenceV2(智能保险柜) Preferences(文件柜)
数据类型 必须是class对象 支持基本类型
自动同步 ✅ 和UI自动同步 ❌ 需要手动读写
使用场景 UI状态持久化 通用数据存储
便捷性 高(声明式) 中(命令式)

一句话总结

PersistenceV2 就是UI状态的"智能保险柜":

  • 全家共用:应用级单例,所有组件都能访问
  • 永不丢失:数据自动保存到磁盘,重启还在
  • 智能备份 :只有@Trace属性变化时自动保存
  • 贵重物品:只收class对象,不收散装数据
  • 大小限制:每个格子约8k,太大放不下
  • UI专用:只能在UI线程使用

11、@Type装饰器:标记类属性的类型

小鱼使用一个"快递打包标签 "的比喻来解释 @Type 装饰器。


比喻:快递打包标签

想象你要把家里的物品(对象)打包寄给朋友(序列化/持久化),然后朋友再拆包(反序列化):

  • 物品 = 类属性
  • 快递打包 = 序列化(存到磁盘)
  • 朋友拆包 = 反序列化(从磁盘读取)
  • @Type标签 = 贴在物品上的"这是什么"的标签
  • 朋友不认识物品 = 没有标签时,朋友不知道这是什么类型

核心问题与解决方案

问题:朋友不认识你的特殊物品

"你寄了一个'无人机'(自定义类对象),朋友拆开后不知道这是无人机,以为是'玩具飞机'(普通对象),无法正确使用。"

解决方案:贴上类型标签

"在无人机上贴个标签'这是大疆无人机型号X',朋友就知道怎么用了。"

typescript 复制代码
// 你的特殊物品
class Drone {
  model: string = "DJI Mavic";
  battery: number = 100;
}

@ObservedV2
class MyGear {
  @Type(Drone)           // 📌 贴标签:这是Drone类型
  @Trace drone: Drone = new Drone(); // ✅ 朋友能正确识别

  // ❌ 没贴标签:
  @Trace unknownItem: object = new Drone(); // 朋友以为是普通玩具
}

规则解释

i. 只能贴在展览品上(@ObservedV2类中)

"只能在博物馆(@ObservedV2装饰的类)里给展品贴标签,其他地方不能贴。"

typescript 复制代码
@ObservedV2
class Museum {
  @Type(Painting)        // ✅ 正确:博物馆里的画
  @Trace painting: Painting = new Painting();
}

@ComponentV2
struct MyRoom {
  @Type(Toy)             // ❌ 错误:家里不是博物馆
  toy: Toy = new Toy();  // 编译报错!
}

ii. 只贴复杂物品(不支持简单类型)

可以贴标签的物品:

  • 🎁 包装好的礼物(class对象)

  • 📦 箱子(Array、Map、Set、Date等)
    不用贴标签的物品:

  • 📄 一张纸(string)

  • 🔢 一个数字(number)

  • ✅ 一个开关(boolean)

typescript 复制代码
@ObservedV2
class MyCollection {
  // ✅ 需要贴标签的:
  @Type(CustomClass) custom: CustomClass = new CustomClass();
  @Type(Date) dateObj: Date = new Date();
  @Type(Array) stringList: string[] = [];
  
  // ❌ 不用贴标签的(也不支持):
  @Trace name: string = "小明";      // 系统知道这是string
  @Trace count: number = 10;         // 系统知道这是number
  @Trace isActive: boolean = true;   // 系统知道这是boolean
}

iii. 必须贴有说明书的物品(不支持Native类型)

"不能给外国进口的'神秘黑科技'(Native类型)贴标签,因为说明书看不懂。"

typescript 复制代码
@ObservedV2
class MyGadgets {
  // ❌ 这些都不能贴@Type标签:
  // PixelMap(图片对象)
  // NativePointer(原生指针)
  // ArrayList(Java数组)
}

iv. 只能贴标准组装物品(构造函数不能含参)

"只能贴'开箱即用'的物品(无参构造函数),不能贴'需要组装的IKEA家具'(需要参数的构造函数)。"

typescript 复制代码
class Furniture {
  constructor(name: string) { } // ❌ 需要组装说明
}

class Toy {
  constructor() { } // ✅ 开箱即用
}

@ObservedV2
class MyHouse {
  @Type(Furniture) chair: Furniture; // ❌ 不能贴标签
  @Type(Toy) toy: Toy = new Toy();   // ✅ 可以贴标签
}

实际使用场景:持久化

typescript 复制代码
// 📦 定义你的特殊物品
@ObservedV2
class UserSettings {
  @Trace theme: string = "light";
  @Trace fontSize: number = 14;
}

// 🏛️ 放在博物馆里(@ObservedV2)
@ObservedV2
class AppData {
  // 📌 关键:贴上类型标签!
  @Type(UserSettings)
  @Trace settings: UserSettings = new UserSettings();
  @Trace counter: number = 0; // 基本类型,不用贴标签
}

// 🚚 打包寄出(持久化)
@Entry
@ComponentV2
struct MainApp {
  // 连接保险柜(PersistenceV2)
  @Local appData: AppData = PersistenceV2.connect<AppData>(
    AppData,
    () => new AppData()
  )!;

  build() {
    Column({ space: 20 }) {
      Text(`主题:${this.appData.settings.theme}`)
        .fontSize(this.appData.settings.fontSize)

      Button("切换主题并保存")
        .onClick(() => {
          // 修改数据
          this.appData.settings.theme = this.appData.settings.theme === "dark" ? 'light' : 'dark';
          this.appData.counter++;

          // 因为有@Type标签,系统知道如何正确保存UserSettings
          // 下次启动应用时,也能正确读取
          PersistenceV2.save('my_app_data');
        })
    }
    .width('100%')
    .height('100%')
  }
}

为什么需要 @Type?

没有 @Type 的情况:
javascript 复制代码
// 存到磁盘的数据:
{
  "settings": {
    "theme": "dark",
    "fontSize": 14
  }
}
// 读回来时:这是个普通对象,不是UserSettings实例!
// 你无法调用 UserSettings 特有的方法
有 @Type 的情况:
javascript 复制代码
// 存到磁盘的数据:
{
  "settings": {
    "@type": "UserSettings",  // 📌 关键:记录了类型信息
    "theme": "dark",
    "fontSize": 14
  }
}
// 读回来时:系统知道这是UserSettings,会创建正确的实例

特殊注意事项

1. 可以留空,但要小心
typescript 复制代码
@ObservedV2
class MyData {
  @Type(UserSettings)
  @Trace settings?: UserSettings = undefined; // ✅ 可以留空
  
  // 但使用时要注意:
  // this.settings?.theme  // 安全访问
}
2. 必须配合@Trace使用
typescript 复制代码
@ObservedV2
class Example {
  @Type(CustomClass)          // ✅ 正确:有@Trace
  @Trace item: CustomClass = new CustomClass();
  
  @Type(CustomClass)          // ❌ 错误:没有@Trace
  normalItem: CustomClass = new CustomClass(); // @Type无效
}

一句话总结

@Type 就是给复杂对象的"类型身份证":

  • 用途:告诉系统"这个属性是什么具体类型"
  • 场合 :只在@ObservedV2类中使用
  • 对象:只给class、Array、Date等复杂类型贴标签
  • 目的 :让持久化数据能正确还原为原来的类型
  • 必须配合@Trace + PersistenceV2 使用

12、@AppStorageV2装饰器:应用全局UI状态存储

小鱼用一个"公司云盘 "的比喻来解释 AppStorageV2


比喻:公司云盘系统

想象 AppStorageV2 就像你们公司的共享云盘

  • 云盘 = AppStorageV2 单例
  • 共享文件夹 = 通过 key 存储的 class 对象
  • 下载链接 = connect 方法
  • 公司所有电脑 = 各个 UIAbility 和页面
  • 自动同步 = 数据修改自动更新所有地方

核心机制图示

复制代码
公司云盘(AppStorageV2)
├── 📁 用户配置 (key: "UserConfig") → 存员工设置
├── 📁 主题管理 (key: "Theme") → 存公司主题
└── 📁 购物车 (key: "Cart") → 存购物数据

公司各个办公室:
├── 💻 总经理办公室(PageOne)
│   └── 连接云盘"用户配置",修改后全公司同步
├── 💻 会议室(PageTwo)
│   └── 连接同一个云盘"用户配置",看到同样数据
└── 💻 休息区(另一个Ability)
    └── 也连接云盘,跨Ability共享数据

规则解释

i. 全公司一个云盘(应用级单例)

"整个公司(应用)只有一个云盘,所有办公室(Ability/页面)都能访问相同数据。"

typescript 复制代码
// 两个config是同一个对象!改一处,另一处自动更新
@ObservedV2
class UserConfig {
  public userID: number;
  @Trace public userName: string;

  constructor(userID?: number, userName?: string) {
    this.userID = userID ?? 1;
    this.userName = userName ?? 'Jack';
  }
}
typescript 复制代码
@Entry
@ComponentV2
export struct Index {
  build() {
    Column({ space: 20 }) {
      PageOne();
      PageTwo();

    }.width('100%')
  }
}
typescript 复制代码
@ComponentV2
export struct PageOne {
  // 总经理办公室(PageOne)
  @Local config: UserConfig = AppStorageV2.connect<UserConfig>(
    UserConfig,
    () => new UserConfig()
  )!;

  build() {
    Column({ space: 20 }) {
      Text(`PageOne name:${this.config.userName},id: ${this.config.userID}`)
      Button('PageOne 改名字').onClick(() => {
        this.config.userName = 'PageOne';
      })
      Button('PageOne 改ID').onClick(() => {
        this.config.userID = 10000;
      })
    }
    .width('80%').backgroundColor('#80000000')
    .borderRadius(10).padding(20)
  }
}
typescript 复制代码
@ComponentV2
export struct PageTwo {
  // 会议室(PageTwo,甚至不同Ability)
  @Local sameConfig: UserConfig = AppStorageV2.connect<UserConfig>(
    UserConfig,
    () => new UserConfig()
  )!;

  build() {
    Column({ space: 20 }) {
      Text(`PageTwo name:${this.sameConfig.userName},id: ${this.sameConfig.userID}`)
      Button('PageTwo 改名字').onClick(() => {
        this.sameConfig.userName = 'PageTwo';
      })

      Button('PageTwo 改ID').onClick(() => {
        this.sameConfig.userID = 20000;
      })
    }.width('80%').backgroundColor('#99000000')
    .borderRadius(10)
    .padding(20)
  }
}

ii. 只存正式文件(必须是class对象)

"云盘只存正式文件(class对象),不收散件(基本类型)。"

typescript 复制代码
// ✅ 可以存:class对象
class Settings {
  @Trace theme: string = 'light';
  fontSize: number = 14;
}

// ❌ 不能直接存:
// AppStorageV2.connect('name', () => '张三');      // string不行
// AppStorageV2.connect('count', () => 100);        // number不行

iii. 贴上标签才能实时同步(@Trace属性)

"只有文件里贴了'重要'标签(@Trace)的部分,修改后全公司电脑才自动刷新显示。"

typescript 复制代码
@ObservedV2
class CompanySettings {
  @Trace theme: string = 'light'; // ✅ 修改后UI自动刷新
  fontSize: number = 14;          // ❌ 修改后UI不刷新(但值变了)
}

// 总经理修改:
settings.theme = 'dark';    // ✅ 所有办公室界面立即变暗
settings.fontSize = 16;     // ❌ 值变成16了,但界面字体不变

iv. 下载链接(connect)有三种用法

"可以从云盘下载文件,如果还没有就新建一个。"

typescript 复制代码
// 方法1 将key为UserConfig、value为new UserConfig()对象的键值对存储到内存中,并赋值给config1

 @Local config1: UserConfig = AppStorageV2.connect<UserConfig>(
    UserConfig,
    () => new UserConfig()
  )!;

// 方法2:将key为key_alias、value为new UserConfig()对象的键值对存储到内存中,并赋值给config2
  @Local config2: UserConfig = AppStorageV2.connect<UserConfig>(
    UserConfig,
    'key_alias',
    () => new UserConfig()
  )!;

// 方法3:key为AppStorageV2已经在AppStorageV2中,将key为AppStorageV2的值返回给config3
  @Local  config3: UserConfig = AppStorageV2.connect(UserConfig) as UserConfig;

5. 删除只是从云盘删(不影响已下载的)

"从云盘删除文件,但已经下载到各办公室电脑里的文件还在。"

typescript 复制代码
// 办公室A下载了文件
@Local file: Document = AppStorageV2.connect<Document>(Document,'doc', () => new Document())!;

// 从云盘删除
AppStorageV2.remove('doc');

// 但是:
this.file.title = "修改标题"; // ✅ officeA的文件还能用
// 只是云盘里没有了,新办公室下载不到

实际使用示例

typescript 复制代码
// 📁 定义云盘文件类型
@ObservedV2
export class AppTheme {
  @Trace mode: 'light' | 'dark' = 'light';
  @Trace primaryColor: string = '#007AFF';
}
typescript 复制代码
// 🏢 总经理办公室(首页)
@Entry
@ComponentV2
struct HomePage {
  // 📥 连接云盘获取主题配置
  @Local theme: AppTheme = AppStorageV2.connect<AppTheme>(
    AppTheme, // 用类名当key
    () => new AppTheme()
  )!;

  build() {
    Column({ space: 20 }) {
      Text('公司主题管理系统')
        .fontColor(this.theme.mode === 'dark' ? Color.White : Color.Black)

      Button(`当前主题:${this.theme.mode}`)
        .onClick(() => {
          // 修改云盘数据,全公司同步
          this.theme.mode = this.theme.mode === 'light' ? 'dark' : 'light';
        })

      Button('切换到产品页')
        .onClick(() => {
          router.pushUrl({ url: 'pages/ProductPage' });
        })
    }.padding(20)
  }
}
typescript 复制代码
import { AppStorageV2 } from '@kit.ArkUI';
import { AppTheme } from './Index';

// 🏢 产品展示页(另一个页面)
@Entry
@ComponentV2
struct ProductPage {
  // 📥 连接同一个云盘文件
  @Local theme: AppTheme = AppStorageV2.connect<AppTheme>(
    AppTheme, // 同样的key
    () => new AppTheme()
  )!;

  build() {
    Column({ space: 20 }) {
      Text('产品展示')
        .fontColor(Color.White)
        .backgroundColor(this.theme.primaryColor).padding(4).borderRadius(6)

      // 这里也能修改,会同步回首页
      Button('修改主题色')
        .onClick(() => {
          this.theme.primaryColor = '#FF3B30'; // 红色
        })
    }.padding(20)
  }
}

与 PersistenceV2 的对比

特性 AppStorageV2(公司云盘) PersistenceV2(智能保险柜)
持久化 ❌ 不保存到磁盘,应用关闭就没了 ✅ 保存到磁盘,重启还在
使用场景 应用运行时全局状态共享 需要持久化的UI状态
数据类型 必须是class对象 必须是class对象
跨Ability ✅ 支持 ✅ 支持
自动保存 ❌ 不自动保存到磁盘 ✅ @Trace属性自动保存

重要限制总结

限制 解释 正确做法
只支持class 不能存string/number等基本类型 包成class对象
UI线程专用 不能在Worker/TaskPool用 只在UI线程操作
不支持Native类型 PixelMap等不能用 用其他方式存储
key要合法 字母数字下划线,≤255字符 用有意义的名字
类型要匹配 同一个key不能存不同类型 确保类型一致

常见使用场景

1. 全局主题管理
typescript 复制代码
// 所有页面共享同一套主题
@Local theme = AppStorageV2.connect(Theme, () => new Theme());
2. 用户登录状态
typescript 复制代码
// 任何页面都能获取/修改登录状态
@Local user = AppStorageV2.connect(User, () => new User());
3. 购物车(跨页面)
typescript 复制代码
// 商品页加购,购物车页自动更新
@Local cart = AppStorageV2.connect(Cart, () => new Cart());
4. 多语言配置
typescript 复制代码
// 切换语言,所有页面自动刷新
@Local i18n = AppStorageV2.connect(I18n, () => new I18n());

一句话总结

AppStorageV2 就是应用的"公司云盘":

  • 一处修改,处处同步:修改数据,所有连接的地方自动更新
  • 跨页面/Ability共享:全应用都能访问相同数据
  • 只存正式文件:必须是class对象,不收散件
  • 实时同步:@Trace属性变化,UI立即刷新
  • 临时存储:应用关闭数据就清空(不持久化)
  • UI专用:只能在UI线程使用

给自己一点掌声👏,V2 装饰器学完了,咱们下次见。

相关推荐
小学生波波2 小时前
HarmonyOS6 - 图片保存到图库中的两种方式
华为·harmonyos·arkts·鸿蒙·harmonyos6
行者963 小时前
用Flutter打造适配OpenHarmony的打卡组件:实践与优化
flutter·harmonyos·鸿蒙
lili-felicity3 小时前
React Native 鸿蒙跨平台开发:useColorScheme 自定义鸿蒙端深色模式的主题配色
react native·react.js·harmonyos
小雨下雨的雨3 小时前
Flutter跨平台开发实战: 鸿蒙与循环交互艺术:虚拟列表与百万级数据性能巅峰
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨3 小时前
Flutter跨平台开发实战: 鸿蒙与循环交互艺术:Sliver 视差滚动与沉浸式布局
flutter·华为·交互·harmonyos·鸿蒙系统
前端世界4 小时前
鸿蒙系统中的分布式任务依赖是如何处理的?原理、方案与实践
分布式·华为·harmonyos
小雨下雨的雨4 小时前
Flutter跨平台开发实战: 鸿蒙与循环交互艺术:卡片堆叠与叠放切换动效
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨4 小时前
Flutter跨平台开发实战: 鸿蒙与循环交互艺术:分布式联动与多端状态同步
分布式·flutter·华为·交互·harmonyos·鸿蒙系统
哈__4 小时前
React Native 鸿蒙跨平台开发:ToastAndroid 提示消息
react native·react.js·harmonyos