〇、前言

在这里插入图片描述
如上所示,截止到本文发布的时间为止,鸿蒙 API 正式版已经更新到 18 了,其中涉及到首选项相关的,新特性有如下:
在这里插入图片描述
新增了 4 个 API,但中心却只有一个:使用户首选项支持选择存储类型,下面就让我基于这个新特性,分享如何封装一个 PreferenceUtil

一、新旧差异

作为开发者,兼容是我们的应有职责,所以,断断不可以有了新欢,就不顾旧爱,因此,势必了解一下鸿蒙 API 中用户首选项相关API,在新旧版本之间的差异。

自从鸿蒙平台开始采用全新的开发语言:ArkTS 以来,我就一直在关注着变化。在 API 18 之前的 SDK 中,为开发者提供的用户首选项,都是存储在 XML 文档中的,除此之外,并无其他存储类型可选,同时,所有涉及数据变更的首选项操作,必须紧跟着使用 flush 操作,才能让新的数据真正写入用户首选项——新手非常容易忘了这一步。

而 API 18 新提供的了一种 GSKV 存储类型,去存储用户首选项,这种存储类型有着 XML 类型所没有的实时落盘特性,即开发者在更改用户首选项数据时,无需调用 flush 接口去刷新存储文件,就非常的方便了。

二、认识 StorageType 与 isStorageTypeSupported

针对用户首选项的存储,由于提供了两种不同的存储类型,ArkData Kit下的 preferences 模块,新增了一个名为 StorageType 的枚举类:
在这里插入图片描述
由于是正式版API的最新特性,所以并非所有的鸿蒙设备都支持,那些系统版本未升级到最新版本的鸿蒙设备,就必然不支持使用 GSKV 去存储用户首选项,所以,为了查询鸿蒙设备是否支持 GSKV 类型的用户首选项,preferences 模块同时新增了一个 isStorageTypeSupported 接口:
在这里插入图片描述
供开发者去查询鸿蒙设备适合使用哪种存储类型的用户首选项。

三、实践封装 PreferencesUtil

1、设计思路

之所以要进行二次封装,而不选择直接使用 preferences 模块下的 API 接口,是因为考虑到数据类型的解耦。官方接口读取数据时,返回的是一个联合多种数据类型的复合数据类型 ValueType:
在这里插入图片描述
在这里插入图片描述
如果不进行二次封装,那么从用户首选项读取数据就非常不方便,每次读取时都需要将 ValueType 转换成单一的具体类型,就会出现很多相似度极高的代码,倒不如直接进行二次封装。

另者,现在新特性支持下,用户首选项以及有了不同存储类型的选择,所以,可以利用二次封装进行类型选择。再者就是方便管理不同的用户首选项,当应用提供很多偏好设置给用户时,为了避免一份用户首选项太过庞大,势必会使用多份用户首选项,特别是使用多窗口的应用。

2、整体结构

在这里插入图片描述
这里,我遵循持久化接口的典型开发方式,围绕初始化、增删改查和资源释放提供接口。

3、具体实现

3.1、初始化

考虑到兼容不同的存储类型,和支持操作多份用户首选项,我弃用 static 模式采用 new 模式,所以,PreferencesUtil 就需要自定义的 constructor 方法:

constructor(ctx: common.UIAbilityContext, pfName: string) {
    Logger.info(`初始化${pfName}首选项`, TAG)
    let xmlType = preferences.StorageType.XML;
    let gskvType = preferences.StorageType.GSKV;
    let options: preferences.Options = {name: pfName, storageType: xmlType}
    let observer = (key: string) => {
      Logger.info(`${key} changed`, TAG)
    }
    let isGskvSupported = preferences.isStorageTypeSupported(gskvType);
    if(isGskvSupported) {
      options = {name: pfName, storageType: gskvType}
      this.PFS_TYPE = gskvType;
    } else {
      this.PFS_TYPE = xmlType;
    }
    this.PFS = preferences.getPreferencesSync(ctx, options);
    this.PFS.on("change",  observer)
  }

其中,会对两个私有类成员字段进行初始化,this.PFS 自不必待说,是用于持有首选项实例的字段,而 this.PFS_TYPE 是为了兼容 GSKV 和 XML 类型的用户首选项,当使用的是 XML 类型的用户首选项,那么所有的数据变更操作,就必须紧随着调用 flush 接口。

3.2、查询接口

结合 ValueType 所联合的数据类型,我封装了如下查询接口:

/**
   * 获取字符串
   * @param key 键
   * @returns 值
   */
  getStr(key: string): string {
    try {
      if (this.PFS.hasSync(key)) {
        const value: string = this.PFS.getSync(key, "") as string;
        return value;
      } else {
        Logger.warn(`key: ${key} not exist`, TAG)
        return "";
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
      return "";
    }
  }

  /**
   * 获取数字
   * @param key 键
   * @returns 值
   */
  getNum(key: string): number {
    try {
      if (this.PFS.hasSync(key)) {
        const value: number = this.PFS.getSync(key, 0) as number;
        return value;
      } else {
        Logger.warn(`key: ${key} not exist`, TAG)
        return 0;
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
      return 0;
    }
  }

  /**
   * 获取布尔值
   * @param key 键
   * @returns 值
   */
  getBool(key: string): boolean {
    try {
      if (this.PFS.hasSync(key)) {
        const value: boolean = this.PFS.getSync(key, false) as boolean;
        return value;
      } else {
        Logger.warn(`key: ${key} not exist`, TAG)
        return false;
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
      return false;
    }
  }

  getStrArr(key: string): Array<string> {
    try {
      if (this.PFS.hasSync(key)) {
        const value: string[] = this.PFS.getSync(key, []) as string[];
        return value;
      } else {
        Logger.warn(`key: ${key} not exist`, TAG)
        return [];
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
      return [];
    }
  }

  getNumArr(key: string): Array<number> {
    try {
      if (this.PFS.hasSync(key)) {
        const value: number[] = this.PFS.getSync(key, []) as number[];
        return value;
      } else {
        Logger.warn(`key: ${key} not exist`, TAG)
        return [];
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
      return [];
    }
  }

查询操作,体现不了 preferences.StorageType 所带来的影响,下面的写入操作才能体现。

3.3、写入接口

写入接口本来无需针对单一数据类型,去封装多个接口,使用一个接口便足够了,但为了追求对称美,我还是封装成如下:

setStr(key: string, value: string): void {
    try {
      this.PFS.putSync(key, value);
      if (this.PFS_TYPE == preferences.StorageType.XML) {
        this.PFS.flushSync();
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
    }
  }

  setNum(key: string, value: number): void {
    try {
      this.PFS.putSync(key, value);
      if (this.PFS_TYPE == preferences.StorageType.XML) {
        this.PFS.flushSync();
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
    }
  }

  setBool(key: string, value: boolean): void {
    try {
      this.PFS.putSync(key, value);
      if (this.PFS_TYPE == preferences.StorageType.XML) {
        this.PFS.flushSync();
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
    }
  }

  setStrArr(key: string, value: Array<string>): void {
    try {
      this.PFS.putSync(key, value);
      if (this.PFS_TYPE == preferences.StorageType.XML) {
        this.PFS.flushSync();
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
    }
  }

  setNumArr(key: string, value: Array<number>): void {
    try {
      this.PFS.putSync(key, value);
      if (this.PFS_TYPE == preferences.StorageType.XML) {
        this.PFS.flushSync();
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
    }
  }

可以看到,每个写入接口里面,都会利用私有成员字段 PFS_TYPE 去判断当前使用的存储类型,如果就是 XML 类型,就会执行 flush 操作以保证新数据真正写入。

3.4、清除接口

首选项,既然有创建记录的操作,也必然有删除记录的操作,所以,清除接口也必然需要提供:

deleteStr(key: string): string {
    try {
      if (this.PFS.hasSync(key)) {
        const value: string = this.PFS.getSync(key, "") as string;
        this.PFS.deleteSync(key);
        if (this.PFS_TYPE == preferences.StorageType.XML) {
          this.PFS.flushSync();
        }
        return value;
      } else {
        Logger.warn(`key: ${key} not exist`, TAG)
        return ""
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
      return ""
    }
  }

  deleteNum(key: string): number {
    try {
      if (this.PFS.hasSync(key)) {
        const value: number = this.PFS.getSync(key, 0) as number;
        this.PFS.deleteSync(key);
        if (this.PFS_TYPE == preferences.StorageType.XML) {
          this.PFS.flushSync();
        }
        return value;
      } else {
        Logger.warn(`key: ${key} not exist`, TAG)
        return 0
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
      return 0
    }
  }

  deleteBool(key: string): boolean {
    try {
      if (this.PFS.hasSync(key)) {
        const value: boolean = this.PFS.getSync(key, false) as boolean;
        this.PFS.deleteSync(key);
        if (this.PFS_TYPE == preferences.StorageType.XML) {
          this.PFS.flushSync();
        }
        return value;
      } else {
        Logger.warn(`key: ${key} not exist`, TAG)
        return false
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
      return false
    }
  }

  deleteStrArr(key: string): Array<string> {
    try {
      if (this.PFS.hasSync(key)) {
        const value: string[] = this.PFS.getSync(key, []) as string[];
        this.PFS.deleteSync(key);
        if (this.PFS_TYPE == preferences.StorageType.XML) {
          this.PFS.flushSync();
        }
        return value;
      } else {
        Logger.warn(`key: ${key} not exist`, TAG)
        return []
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
      return []
    }
  }

  deleteNumArr(key: string): Array<number> {
    try {
      if (this.PFS.hasSync(key)) {
        const value: number[] = this.PFS.getSync(key, []) as number[];
        this.PFS.deleteSync(key);
        if (this.PFS_TYPE == preferences.StorageType.XML) {
          this.PFS.flushSync();
        }
        return value;
      } else {
        Logger.warn(`key: ${key} not exist`, TAG)
        return []
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
      return []
    }
  }

  deleteAll(): void {
    try {
      this.PFS.clearSync();
      if (this.PFS_TYPE == preferences.StorageType.XML) {
        this.PFS.flushSync();
      }
    } catch (e) {
      Logger.error(`happen error: ${e}`, TAG)
    }
  }

可以看到,对单一记录进行清除时,我会将对应的数据值返回,因为考虑到存在记录转移,即将一条记录从首选项A转移到首选项B的场景。

3.5、资源释放

由于初始化的时候,注册了一个监听器,去监听首选项记录的变更,而监听器都必须手动关闭,才不会继续占用资源,所以就需要如下的 release 接口:

release() {
    if (this.PFS) {
      let observer = (key: string) => {
        Logger.info(`${key} changed`, TAG)
      }
      this.PFS.off("change", observer);
      preferences.removePreferencesFromCacheSync(this.ctx, this.PFS_NAME);
      this.PFS = null;
      this.PFS_TYPE = null;
      this.ctx = null;
      Logger.info(`${this.PFS_NAME} release success`, TAG)
      this.PFS_NAME = ""
    }
  }

如果想在释放监听器的同时,将首选项实例从缓存中移除,那么就需要将类成员字段更新成如下:

private PFS: preferences.Preferences|null;
private PFS_TYPE: preferences.StorageType|null;
private PFS_NAME: string;
private ctx: common.UIAbilityContext|null;

如此一来,上面那些增删改查接口,都需要增加判断 PFS 是否未 null 的代码语句。

具体的源代码在这里

Logo

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

更多推荐