从零开始开发纯血鸿蒙应用之用户首选项
本文介绍了鸿蒙API 18中用户首选项的新特性——支持GSKV存储类型,相比传统的XML存储具有实时落盘优势。作者详细对比了新旧版本差异,并基于StorageType枚举类和isStorageTypeSupported接口,分享了如何封装一个兼容两种存储类型的PreferenceUtil工具类。该工具类通过二次封装解决了数据类型解耦问题,提供了初始化、增删改查等完整接口,并自动处理XML类型所需的
从零开始开发纯血鸿蒙应用
〇、前言

如上所示,截止到本文发布的时间为止,鸿蒙 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 的代码语句。
具体的源代码在这里
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)