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 的场景:
-
复杂公式计算
typescript@Computed get bmi(): number { return this.weight / (this.height * this.height); } -
数据过滤/筛选
typescript@Computed get filteredList(): Item[] { return this.items.filter(item => item.price > this.minPrice && item.category === this.selectedCategory ); } -
格式化显示
typescript@Computed get formattedDate(): string { return `${this.year}年${this.month}月${this.day}日`; } -
依赖多个状态的计算
typescript@Computed get canSubmit(): boolean { return this.name.length > 0 && this.email.includes('@') && this.agreedToTerms; }
❌ 不应该用 @Computed 的场景:
-
简单计算
typescript// 直接写: Text(`双倍:${count * 2}`) // 不用: // @Computed get double() { return count * 2; } -
无依赖的计算
typescript@Computed get constantValue(): number { return 100; // 永远不变,没必要用@Computed } -
副作用操作
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 的场景:
-
用户偏好设置
- 主题颜色、字体大小、语言设置
-
应用状态恢复
- 文章阅读进度、视频播放位置、表单草稿
-
用户配置信息
- 通知开关、隐私设置、个性化选项
❌ 不应该用 PersistenceV2 的场景:
- 大量数据存储 → 用数据库
- 非UI相关数据 → 用 Preferences
- 频繁读写的大数据 → 性能会差
- 基本类型直接存储 → 不支持,要包成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 装饰器学完了,咱们下次见。