本篇深入学习 AppStorage 和 Preferences,实现收藏功能与错题本

图:古今职鉴开源教程封面。本篇围绕「全局状态与持久化存储」展开。

学习目标

完成本篇后,你将能够:

  • ✅ 深入理解 AppStorage 的使用
  • ✅ 使用 Preferences 实现本地持久化
  • ✅ 封装 StorageManager 工具类
  • ✅ 实现收藏功能

预计学习时间

约 90 分钟

---

实战一:AppStorage 全局状态

第一步:理解 AppStorage

AppStorage 是应用级别的状态存储:

  • 所有页面共享
  • 应用退出后数据丢失
  • 不会自动持久化

第二步:AppStorage 基本操作

// 设置或创建(推荐用于初始化)
AppStorage.setOrCreate<number>('count', 0);

// 设置值(必须已存在)
AppStorage.set<number>('count', 10);

// 获取值
const count = AppStorage.get<number>('count');

// 检查是否存在
if (AppStorage.has('count')) {
  // 存在
}

// 删除
AppStorage.delete('count');

第三步:在组件中使用

@Entry
@Component
struct Lesson10Page {
  // 从 AppStorage 读取(单向)
  @StorageProp('globalCount') count: number = 0;

  aboutToAppear() {
    // 初始化全局状态
    AppStorage.setOrCreate('globalCount', 0);
  }

  build() {
    Column({ space: 20 }) {
      Text(`全局计数: ${this.count}`)
        .fontSize(24)

      Button('增加')
        .onClick(() => {
          // 修改 AppStorage
          const current = AppStorage.get<number>('globalCount') || 0;
          AppStorage.set('globalCount', current + 1);
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#f8f6f5')
  }
}

第四步:@StorageProp vs @StorageLink

// 单向绑定:只读取,不写回
@StorageProp('key') value: Type = defaultValue;

// 双向绑定:修改会同步到 AppStorage
@StorageLink('key') value: Type = defaultValue;
特性 @StorageProp @StorageLink
数据流向 单向(只读) 双向
组件修改 不同步 同步到 AppStorage
适用场景 只需显示 需要修改

第五步:运行验证

hvigorw assembleHap --no-daemon

---

实战二:Preferences 本地持久化

第一步:理解 Preferences

Preferences 用于存储轻量级数据到本地文件:

  • 数据持久化,重启后保留
  • 适合存储配置、设置
  • 不适合大量数据(建议 < 1MB)

第二步:获取 Preferences 实例

import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct Lesson10Page {
  private dataPreferences: preferences.Preferences | null = null;

  async aboutToAppear() {
    const context = getContext(this) as common.UIAbilityContext;
    // 获取 Preferences 实例
    this.dataPreferences = await preferences.getPreferences(context, 'myStore');
  }
}

第三步:存储和读取数据

// 存储数据
async saveData() {
  if (this.dataPreferences) {
    await this.dataPreferences.put('username', '张三');
    await this.dataPreferences.put('age', 25);
    // 重要:flush 持久化到磁盘
    await this.dataPreferences.flush();
  }
}

// 读取数据
async loadData() {
  if (this.dataPreferences) {
    // 第二个参数是默认值
    const username = await this.dataPreferences.get('username', '');
    const age = await this.dataPreferences.get('age', 0);
  }
}

// 删除数据
async deleteData() {
  if (this.dataPreferences) {
    await this.dataPreferences.delete('username');
    await this.dataPreferences.flush();
  }
}

第四步:存储复杂数据

Preferences 只支持基本类型,复杂数据需要 JSON 序列化:

interface FavoriteItem {
  id: number;
  name: string;
}

// 存储数组
async saveFavorites(favorites: FavoriteItem[]) {
  if (this.dataPreferences) {
    const jsonStr = JSON.stringify(favorites);
    await this.dataPreferences.put('favorites', jsonStr);
    await this.dataPreferences.flush();
  }
}

// 读取数组
async loadFavorites(): Promise<FavoriteItem[]> {
  if (this.dataPreferences) {
    const jsonStr = await this.dataPreferences.get('favorites', '[]') as string;
    return JSON.parse(jsonStr) as FavoriteItem[];
  }
  return [];
}

第五步:运行验证

hvigorw assembleHap --no-daemon

---

实战三:实现收藏功能

第一步:定义数据接口

interface PositionItem {
  id: number;
  name: string;
  dynasty: string;
  description: string;
}

interface FavoritePosition {
  id: number;
  name: string;
  dynasty: string;
  addTime: number;
}

第二步:创建收藏页面

import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct Lesson10Page {
  private dataPreferences: preferences.Preferences | null = null;
  @State favorites: FavoritePosition[] = [];
  @State currentTab: number = 0;

  private positions: PositionItem[] = [
    { id: 1, name: '丞相', dynasty: '秦', description: '百官之长' },
    { id: 2, name: '太尉', dynasty: '秦', description: '掌管军事' },
    { id: 3, name: '御史大夫', dynasty: '秦', description: '监察百官' },
    { id: 4, name: '大司马', dynasty: '汉', description: '最高军事长官' },
    { id: 5, name: '尚书令', dynasty: '唐', description: '尚书省长官' }
  ];

  async aboutToAppear() {
    const context = getContext(this) as common.UIAbilityContext;
    this.dataPreferences = await preferences.getPreferences(context, 'lesson10');
    await this.loadFavorites();
  }

  async loadFavorites() {
    if (this.dataPreferences) {
      const jsonStr = await this.dataPreferences.get('favorites', '[]') as string;
      this.favorites = JSON.parse(jsonStr) as FavoritePosition[];
    }
  }

  async saveFavorites() {
    if (this.dataPreferences) {
      await this.dataPreferences.put('favorites', JSON.stringify(this.favorites));
      await this.dataPreferences.flush();
    }
  }

  isFavorite(id: number): boolean {
    return this.favorites.some(f => f.id === id);
  }

  async toggleFavorite(item: PositionItem) {
    const index = this.favorites.findIndex(f => f.id === item.id);
    if (index >= 0) {
      this.favorites.splice(index, 1);
    } else {
      this.favorites.push({
        id: item.id,
        name: item.name,
        dynasty: item.dynasty,
        addTime: Date.now()
      });
    }
    await this.saveFavorites();
  }

  build() {
    Column() {
      // 头部
      Row() {
        Text('收藏功能演示')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1e293b')
      }
      .width('100%')
      .height(56)
      .padding({ left: 16, right: 16 })
      .backgroundColor(Color.White)

      // Tab 切换
      Row() {
        this.TabButton('官职列表', 0)
        this.TabButton(`我的收藏 (${this.favorites.length})`, 1)
      }
      .width('100%')
      .padding(16)

      // 内容
      if (this.currentTab === 0) {
        this.PositionList()
      } else {
        this.FavoriteList()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f8f6f5')
  }

  @Builder
  TabButton(title: string, index: number) {
    Text(title)
      .fontSize(14)
      .fontColor(this.currentTab === index ? Color.White : '#1e293b')
      .backgroundColor(this.currentTab === index ? '#c41e3a' : '#f0f0f0')
      .padding({ left: 20, right: 20, top: 10, bottom: 10 })
      .borderRadius(20)
      .margin({ right: 12 })
      .onClick(() => {
        this.currentTab = index;
      })
  }

  @Builder
  PositionList() {
    List() {
      ForEach(this.positions, (item: PositionItem) => {
        ListItem() {
          Row() {
            Column() {
              Text(item.name)
                .fontSize(16)
                .fontWeight(FontWeight.Medium)
                .fontColor('#1e293b')

              Text(`${item.dynasty} · ${item.description}`)
                .fontSize(13)
                .fontColor('#64748b')
                .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Image(this.isFavorite(item.id) ?
              $r('app.media.ic_favorite_filled') :
              $r('app.media.ic_favorite'))
              .width(24)
              .height(24)
              .fillColor(this.isFavorite(item.id) ? '#c41e3a' : '#cccccc')
              .onClick(() => {
                this.toggleFavorite(item);
              })
          }
          .width('100%')
          .padding(16)
          .backgroundColor(Color.White)
          .borderRadius(12)
          .margin({ bottom: 12 })
        }
      }, (item: PositionItem) => item.id.toString())
    }
    .width('100%')
    .layoutWeight(1)
    .padding({ left: 16, right: 16 })
  }

  @Builder
  FavoriteList() {
    if (this.favorites.length === 0) {
      Column() {
        Text('暂无收藏')
          .fontSize(16)
          .fontColor('#64748b')
      }
      .width('100%')
      .layoutWeight(1)
      .justifyContent(FlexAlign.Center)
    } else {
      List() {
        ForEach(this.favorites, (item: FavoritePosition) => {
          ListItem() {
            Row() {
              Column() {
                Text(item.name)
                  .fontSize(16)
                  .fontWeight(FontWeight.Medium)
                  .fontColor('#1e293b')

                Text(`${item.dynasty}`)
                  .fontSize(13)
                  .fontColor('#64748b')
                  .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)

              Image($r('app.media.ic_close'))
                .width(20)
                .height(20)
                .fillColor('#cccccc')
                .onClick(() => {
                  this.removeFavorite(item.id);
                })
            }
            .width('100%')
            .padding(16)
            .backgroundColor(Color.White)
            .borderRadius(12)
            .margin({ bottom: 12 })
          }
        }, (item: FavoritePosition) => item.id.toString())
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ left: 16, right: 16 })
    }
  }

  async removeFavorite(id: number) {
    const index = this.favorites.findIndex(f => f.id === id);
    if (index >= 0) {
      this.favorites.splice(index, 1);
      await this.saveFavorites();
    }
  }
}

第三步:运行验证

hvigorw assembleHap --no-daemon

预期效果

  • 点击心形图标可以收藏/取消收藏
  • 切换到"我的收藏"Tab 查看收藏列表
  • 重启应用后收藏数据保留

---

实战四:封装 StorageManager 工具类

第一步:创建工具类

// StorageManager.ets
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';

export class StorageManager {
  private static instance: StorageManager | null = null;
  private dataPreferences: preferences.Preferences | null = null;

  private constructor() {}

  static getInstance(): StorageManager {
    if (!StorageManager.instance) {
      StorageManager.instance = new StorageManager();
    }
    return StorageManager.instance;
  }

  async init(context: common.UIAbilityContext): Promise<void> {
    this.dataPreferences = await preferences.getPreferences(context, 'app_data');
  }

  async setString(key: string, value: string): Promise<void> {
    if (!this.dataPreferences) return;
    await this.dataPreferences.put(key, value);
    await this.dataPreferences.flush();
  }

  async getString(key: string, defaultValue: string = ''): Promise<string> {
    if (!this.dataPreferences) return defaultValue;
    return await this.dataPreferences.get(key, defaultValue) as string;
  }

  async setObject<T>(key: string, value: T): Promise<void> {
    if (!this.dataPreferences) return;
    await this.dataPreferences.put(key, JSON.stringify(value));
    await this.dataPreferences.flush();
  }

  async getObject<T>(key: string, defaultValue: T): Promise<T> {
    if (!this.dataPreferences) return defaultValue;
    const jsonStr = await this.dataPreferences.get(key, '') as string;
    if (!jsonStr) return defaultValue;
    try {
      return JSON.parse(jsonStr) as T;
    } catch {
      return defaultValue;
    }
  }

  async remove(key: string): Promise<void> {
    if (!this.dataPreferences) return;
    await this.dataPreferences.delete(key);
    await this.dataPreferences.flush();
  }
}

第二步:使用工具类

// 在 EntryAbility 中初始化
async onCreate() {
  await StorageManager.getInstance().init(this.context);
}

// 在组件中使用
async aboutToAppear() {
  this.favorites = await StorageManager.getInstance()
    .getObject<FavoritePosition[]>('favorites', []);
}

async saveFavorites() {
  await StorageManager.getInstance()
    .setObject('favorites', this.favorites);
}

第三步:工具类的优势

  • 单例模式,全局共享
  • 类型安全的存取方法
  • 简化异步操作
  • 统一管理存储逻辑

---

完整代码

// 文件路径:products/jiaocheng/src/main/ets/lesson10/Lesson10Page.ets

import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';

interface PositionItem {
  id: number;
  name: string;
  dynasty: string;
  description: string;
}

interface FavoritePosition {
  id: number;
  name: string;
  dynasty: string;
  addTime: number;
}

@Entry
@Component
struct Lesson10Page {
  private dataPreferences: preferences.Preferences | null = null;
  @State favorites: FavoritePosition[] = [];
  @State currentTab: number = 0;

  private positions: PositionItem[] = [
    { id: 1, name: '丞相', dynasty: '秦', description: '百官之长' },
    { id: 2, name: '太尉', dynasty: '秦', description: '掌管军事' },
    { id: 3, name: '御史大夫', dynasty: '秦', description: '监察百官' },
    { id: 4, name: '大司马', dynasty: '汉', description: '最高军事长官' },
    { id: 5, name: '尚书令', dynasty: '唐', description: '尚书省长官' }
  ];

  async aboutToAppear() {
    const context = getContext(this) as common.UIAbilityContext;
    this.dataPreferences = await preferences.getPreferences(context, 'lesson10');
    await this.loadFavorites();
  }

  async loadFavorites() {
    if (this.dataPreferences) {
      const jsonStr = await this.dataPreferences.get('favorites', '[]') as string;
      this.favorites = JSON.parse(jsonStr) as FavoritePosition[];
    }
  }

  async saveFavorites() {
    if (this.dataPreferences) {
      await this.dataPreferences.put('favorites', JSON.stringify(this.favorites));
      await this.dataPreferences.flush();
    }
  }

  isFavorite(id: number): boolean {
    return this.favorites.some(f => f.id === id);
  }

  async toggleFavorite(item: PositionItem) {
    const index = this.favorites.findIndex(f => f.id === item.id);
    if (index >= 0) {
      this.favorites.splice(index, 1);
    } else {
      this.favorites.push({
        id: item.id,
        name: item.name,
        dynasty: item.dynasty,
        addTime: Date.now()
      });
    }
    await this.saveFavorites();
  }

  async removeFavorite(id: number) {
    const index = this.favorites.findIndex(f => f.id === id);
    if (index >= 0) {
      this.favorites.splice(index, 1);
      await this.saveFavorites();
    }
  }

  build() {
    Column() {
      Row() {
        Text('收藏功能演示')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1e293b')
      }
      .width('100%')
      .height(56)
      .padding({ left: 16, right: 16 })
      .backgroundColor(Color.White)

      Row() {
        this.TabButton('官职列表', 0)
        this.TabButton(`我的收藏 (${this.favorites.length})`, 1)
      }
      .width('100%')
      .padding(16)

      if (this.currentTab === 0) {
        this.PositionList()
      } else {
        this.FavoriteList()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f8f6f5')
  }

  @Builder
  TabButton(title: string, index: number) {
    Text(title)
      .fontSize(14)
      .fontColor(this.currentTab === index ? Color.White : '#1e293b')
      .backgroundColor(this.currentTab === index ? '#c41e3a' : '#f0f0f0')
      .padding({ left: 20, right: 20, top: 10, bottom: 10 })
      .borderRadius(20)
      .margin({ right: 12 })
      .onClick(() => { this.currentTab = index; })
  }

  @Builder
  PositionList() {
    List() {
      ForEach(this.positions, (item: PositionItem) => {
        ListItem() {
          Row() {
            Column() {
              Text(item.name)
                .fontSize(16)
                .fontWeight(FontWeight.Medium)
                .fontColor('#1e293b')
              Text(`${item.dynasty} · ${item.description}`)
                .fontSize(13)
                .fontColor('#64748b')
                .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Image(this.isFavorite(item.id) ?
              $r('app.media.ic_favorite_filled') :
              $r('app.media.ic_favorite'))
              .width(24)
              .height(24)
              .fillColor(this.isFavorite(item.id) ? '#c41e3a' : '#cccccc')
              .onClick(() => { this.toggleFavorite(item); })
          }
          .width('100%')
          .padding(16)
          .backgroundColor(Color.White)
          .borderRadius(12)
          .margin({ bottom: 12 })
        }
      }, (item: PositionItem) => item.id.toString())
    }
    .width('100%')
    .layoutWeight(1)
    .padding({ left: 16, right: 16 })
  }

  @Builder
  FavoriteList() {
    if (this.favorites.length === 0) {
      Column() {
        Text('暂无收藏')
          .fontSize(16)
          .fontColor('#64748b')
      }
      .width('100%')
      .layoutWeight(1)
      .justifyContent(FlexAlign.Center)
    } else {
      List() {
        ForEach(this.favorites, (item: FavoritePosition) => {
          ListItem() {
            Row() {
              Column() {
                Text(item.name)
                  .fontSize(16)
                  .fontWeight(FontWeight.Medium)
                  .fontColor('#1e293b')
                Text(item.dynasty)
                  .fontSize(13)
                  .fontColor('#64748b')
                  .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)

              Image($r('app.media.ic_close'))
                .width(20)
                .height(20)
                .fillColor('#cccccc')
                .onClick(() => { this.removeFavorite(item.id); })
            }
            .width('100%')
            .padding(16)
            .backgroundColor(Color.White)
            .borderRadius(12)
            .margin({ bottom: 12 })
          }
        }, (item: FavoritePosition) => item.id.toString())
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ left: 16, right: 16 })
    }
  }
}

@Builder
export function Lesson10PageBuilder() {
  Lesson10Page()
}

---

本课小结

核心知识点

知识点 说明
AppStorage 应用级全局状态,不持久化
@StorageProp 单向绑定 AppStorage
@StorageLink 双向绑定 AppStorage
Preferences 本地持久化存储
flush() 持久化到磁盘(必须调用)
JSON 序列化 存储复杂数据类型

存储方案选择

场景 推荐方案
跨页面共享状态 AppStorage
需要持久化的配置 Preferences
大量结构化数据 关系型数据库

---

课后练习

练习1:实现错题本功能

记录用户答错的题目,支持查看和删除。

练习2:添加收藏排序

支持按收藏时间或名称排序。

---

下一课预告

第11课我们将学习响应式布局,包括:

  • 屏幕信息获取
  • 断点设计与响应式策略
  • 媒体查询
  • 横竖屏适配

项目开源地址

https://gitcode.com/daleishen/gujinzhijian

Logo

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

更多推荐