HarmonyOS鸿蒙应用开发(三、轻量级配置存储dataPreferences)

在应用开发中存储一些配置是很常见的需求。在android中有SharedPreferences,一个轻量级的存储类,用来保存应用的一些常用配置。在HarmonyOS鸿蒙应用开发中,实现类似功能的也叫首选项,dataPreferences。

相关概念

ohos.data.preferences (用户首选项)

dataPreferences(首选项),为应用提供Key-Value键值型的数据处理能力,支持应用持久化轻量级数据,并对其修改和查询。数据存储形式为键值对,键的类型为字符串型,值的存储数据类型包括数字型、字符型、布尔型以及这3种类型的数组类型。

如何使用

导入模块

import dataPreferences from '@ohos.data.preferences';

常量

系统能力: SystemCapability.DistributedDataManager.Preferences.Core

名称

参数类型

可读

可写

说明

MAX_KEY_LENGTH

number

Key的最大长度限制为80个字节。

MAX_VALUE_LENGTH

number

Value的最大长度限制为8192个字节。

dataPreferences.getPreferences

getPreferences(context: Context, name: string, callback: AsyncCallback<Preferences>): void

获取Preferences实例,使用callback异步回调。

系统能力: SystemCapability.DistributedDataManager.Preferences.Core

Stage模型使用示例

import UIAbility from '@ohos.app.ability.UIAbility';

let preferences = null;

class EntryAbility extends UIAbility {
    onWindowStageCreate(windowStage) {
        try {
            dataPreferences.getPreferences(this.context, 'mystore', function (err, val) {
                if (err) {
                    console.error("Failed to get preferences. code =" + err.code + ", message =" + err.message);
                    return;
                }
                preferences = val;
                console.info("Succeeded in getting preferences.");
            })
        } catch (err) {
            console.error("Failed to get preferences. code =" + err.code + ", message =" + err.message);
        }
    }
}

首选项使用demo

软件要求

  • DevEco Studio版本:DevEco Studio 3.1 Release。
  • HarmonyOS SDK版本:API version 9。

代码结构

├──entry/src/main/ets            // 代码区
│  ├──common
│  │  ├──constants
│  │  │  ├──CommonConstants.ets    // 公共常量类
│  │  │  └──StyleConstants.ets    // 样式常量类 
│  │  └──utils
│  │     └──Logger.ets        // 日志打印类
│  ├──entryability
│  │  └──EntryAbility.ts    // 程序入口类
│  ├──model
│  │  └──PreferenceModel.ets    // 首选项相关方法类
│  ├──pages
│  │  └──Index.ets        // 主界面    
│  ├──view
│  │  ├──ButtonComponent.ets    // 自定义Button组件类
│  │  └──TextItemComponent.ets  // 自定义Text组件类
│  └──viewmodel
│     ├──ButtonItemData.ets     // 按钮数据类
│     └──Fruit.ets            // 水果数据类
└──entry/src/main/resources    // 资源文件目录

构建主界面

在这个任务中,我们将完成示例主界面的开发,效果如图所示:

从上面效果图可以看出,主界面主要由2个相同样式的文本和文本输入框,以及3个相同样式的按钮组成。我们可以将文本和文本输入框抽取成一个TextItemComponent子组件。再将按钮抽取成一个ButtonComponent子组件。

在ets目录下,点击鼠标右键 > New > Directory,新建名为view的自定义子组件目录。然后在view目录下,点击鼠标右键 > New > ArkTS File,新建两个ArkTS文件,分别为TextItemComponent子组件、ButtonComponent子组件。可以看到文件目录结构,效果如图所示: 

// TextItemComponent.ets
@Component
export default struct TextItemComponent {
  private textResource: Resource = $r('app.string.empty'); // 按钮文本资源
  private placeholderResource: Resource = $r('app.string.empty'); // placeholder文本资源
  private marginBottom: string = '';
  private marginTop: string = '';
  private textInputType: InputType = InputType.Normal; // 输入框输入数据类型
  private textFlag: number = 0; // 文本框标记
  @Link fruit: Fruit; // 水果数据
  private textInputCallBack = (value: string) => {}; // TextInput的回调

  build() {
    Column() {
      Text(this.textResource)
        .fontSize(StyleConstants.TEXT_FONT_SIZE)
        .height(StyleConstants.TEXT_HEIGHT)
        .width(StyleConstants.FULL_PERCENT)
        .fontColor($r('app.color.text_font_color'))
        .letterSpacing(StyleConstants.LETTER_SPACING)
        .fontWeight(StyleConstants.FONT_WEIGHT)
        .margin({
          bottom: StyleConstants.TEXT_MARGIN_BOTTOM,
          left: StyleConstants.TEXT_MARGIN_LEFT,
          top: this.marginTop
        })

      TextInput({
        placeholder: this.placeholderResource,
        text: this.textFlag === 0 ? (this.fruit.fruitName) : (this.fruit.fruitNum)
      })
        .placeholderFont({ size: StyleConstants.FONT_SIZE, weight: StyleConstants.FONT_WEIGHT })
        .placeholderColor($r('app.color.placeholder_color'))
        .caretColor(Color.Blue)
        .type(this.textInputType)
        .height(StyleConstants.TEXT_INPUT_HEIGHT)
        .width(StyleConstants.TEXT_INPUT_WIDTH)
        .margin({ bottom: this.marginBottom })
        .fontSize(StyleConstants.FONT_SIZE)
        .fontColor($r('app.color.text_font_color'))
        .fontWeight(StyleConstants.FONT_WEIGHT)
        .backgroundColor($r('app.color.white'))
        .onChange((value: string) => {
          this.textInputCallBack(value);
        })
    }
  }
}
// ButtonComponent.ets
@Component
export default struct ButtonComponent {
  private buttonItemValues: Array<ButtonItemData> = this.getButtonItemValues();
  @Link fruit: Fruit; // 水果数据 

  build() {
    Column() {
      ForEach(this.buttonItemValues, (item: ButtonItemData) => {
        Button(item.resource, { type: ButtonType.Capsule, stateEffect: true })
          .backgroundColor($r('app.color.button_background_color'))
          .width(StyleConstants.BUTTON_WIDTH)
          .height(StyleConstants.BUTTON_HEIGHT)
          .fontWeight(StyleConstants.FONT_WEIGHT)
          .fontSize(StyleConstants.FONT_SIZE)
          .margin({ bottom: StyleConstants.BUTTON_MARGIN_BOTTOM })
          .onClick(() => {
            item.clickMethod();
          })
      }, (item: ButtonItemData) => JSON.stringify(item))
    }
  }
}

在Index.ets主界面中引用TextItemComponent和ButtonComponent子组件。 

// Index.ets
Column() {
  // 水果名称输入框
  TextItemComponent({
    textResource: $r('app.string.fruit_text'),
    placeholderResource: $r('app.string.fruit_placeholder'),
    textFlag: CommonConstants.FRUIT_FLAG,
    fruit: $fruit,
    textInputCallBack: (value: string) => {
      this.fruit.fruitName = value;
    }
  })

  // 水果数量输入框
  TextItemComponent({
    textResource: $r('app.string.number_text'),
    placeholderResource: $r('app.string.number_placeholder'),
    textFlag: CommonConstants.NUMBER_FLAG,
    fruit: $fruit,
    textInputCallBack: (value: string) => {
      this.fruit.fruitNum = value;
    }
  })

  // 按钮组件
  ButtonComponent({ fruit: $fruit })
}
.width(StyleConstants.FULL_PERCENT)
.height(StyleConstants.FULL_PERCENT)
.backgroundColor($r('app.color.main_background_color'))

创建数据文件

创建数据文件需要如下两个步骤:

  • 导入dataPreferences模块。

  • 通过dataPreferences模块的getPreferences(context, name)方法获取到对应文件名的Preferences实例。

Preferences的数据存储在文件中,因此需要指定存储的文件名PREFERENCES_NAME。再通过Preferences提供的方法进行数据库的相关操作。

// PreferenceModel.ets
// 导入dataPreferences模块
import dataPreferences from '@ohos.data.preferences';

let context = getContext(this);
let preference: dataPreferences.Preferences;
let preferenceTemp: dataPreferences.Preferences;

// 调用getPreferences方法读取指定首选项持久化文件,将数据加载到Preferences实例,用于数据操作
async getPreferencesFromStorage() {
  try {
    preference = await dataPreferences.getPreferences(context, CommonConstants.PREFERENCES_NAME);
  } catch (err) {
    Logger.error(CommonConstants.TAG, `Failed to get preferences, Cause: ${err}`);
  }
}

写入数据

获取Preferences实例后,使用Preferences的put方法,将用户输入的水果名称和水果数量数据,保存到缓存的实例中。再通过Preferences的flush方法将Preferences实例异步存储到首选项持久化文件中。

// PreferenceModel.ets
async putPreference(fruit: Fruit) {
  ...
  try {
    // 将用户输入的水果名称和水果数量数据,保存到缓存的Preference实例中
    await preference.put(CommonConstants.KEY_NAME, JSON.stringify(fruit));
  } catch (err) {
    Logger.error(CommonConstants.TAG, `Failed to put value, Cause: ${err}`);
  }
  // 将Preference实例存储到首选项持久化文件中
  await preference.flush();
}

读取数据

使用Preferences的get方法读取数据。如果键不存在,则返回默认值。例如获取下面代码中fruit的值,如果fruit的键KEY_NAME不存在,则会返回空字符串。通过默认值的设置,来避免程序出现异常。

// PreferenceModel.ets
async getPreference() {
  let fruit = '';
  ...
  try {
    fruit = (await preference.get(CommonConstants.KEY_NAME, '')).toString();
  } catch (err) {
    Logger.error(CommonConstants.TAG, `Failed to get value, Cause: ${err}`);
  }
}

删除数据文件

通过dataPreferences模块的deletePreferences(context, name)方法从内存中移除指定文件对应的Preferences单实例。移除Preferences单实例时,应用不允许再使用该实例进行数据操作,否则会出现数据一致性问题。

// PreferenceModel.ets
async deletePreferences() {
  try {
    await dataPreferences.deletePreferences(context, CommonConstants.PREFERENCES_NAME);
  } catch(err) {
    Logger.error(CommonConstants.TAG, `Failed to delete preferences, Cause: ${err}`);
  }
  ...
}

附上demo源码地址:demo源码

注意和AppStorage区别

AppStorage是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。

和AppStorage不同的是,LocalStorage是页面级的,通常应用于页面内的数据共享。而AppStorage是应用级的全局状态共享,还相当于整个应用的“中枢”,持久化数据PersistentStorage和环境变量Environment都是通过和AppStorage中转,才可以和UI交互。

AppStorage

AppStorage是在应用启动的时候会被创建的单例。它的目的是为了提供应用状态数据的中心存储,这些状态数据在应用级别都是可访问的。AppStorage将在应用运行过程保留其属性。属性通过唯一的键字符串值访问。

AppStorage可以和UI组件同步,且可以在应用业务逻辑中被访问。

AppStorage中的属性可以被双向同步,数据可以是存在于本地或远程设备上,并具有不同的功能,比如数据持久化(详见PersistentStorage)。这些数据是通过业务逻辑中实现,与UI解耦,如果希望这些数据在UI中使用,需要用到@StorageProp和@StorageLink。

由于AppStorage是内存内数据,该行为会导致数据丧失持久化能力。

@StorageProp

在上文中已经提到,如果要建立AppStorage和自定义组件的联系,需要使用@StorageProp和@StorageLink装饰器。使用@StorageProp(key)/@StorageLink(key)装饰组件内的变量,key标识了AppStorage的属性。

当自定义组件初始化的时候,@StorageProp(key)/@StorageLink(key)装饰的变量会通过给定的key,绑定在AppStorage对应的属性,完成初始化。本地初始化是必要的,因为无法保证AppStorage一定存在给定的key,这取决于应用逻辑,是否在组件初始化之前在AppStorage实例中存入对应的属性。

@StorageProp(key)是和AppStorage中key对应的属性建立单向数据同步,我们允许本地改变的发生,但是对于@StorageProp,本地的修改永远不会同步回AppStorage中,相反,如果AppStorage给定key的属性发生改变,改变会被同步给@StorageProp,并覆盖掉本地的修改。

PersistentStorage

最后需要注意的是,LocalStorage和AppStorage都是运行时的内存,但是在应用退出再次启动后,依然能保存选定的结果,需要用到PersistentStorage。

PersistentStorage是应用程序中的可选单例对象。此对象的作用是持久化存储选定的AppStorage属性,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。虽然借助PersistentStorage也可以实现数据的持久话,但它是用来持久化UI状态的,注意和用户首选项的区别(如果是一些配置存储,使用用户首选项)。

综上所述,什么时候用PersistentStorage?在应用退出再次启动后,依然能保存选定的结果,是应用开发中十分常见的现象,这就需要用到PersistentStorage。

限制条件

PersistentStorage允许的类型和值有:

  • number, string, boolean, enum 等简单类型。
  • 可以被JSON.stringify()和JSON.parse()重构的对象。例如Date, Map, Set等内置类型则不支持,以及对象的属性方法不支持持久化。

PersistentStorage不允许的类型和值有:

  • 不支持嵌套对象(对象数组,对象的属性是对象等)。因为目前框架无法检测AppStorage中嵌套对象(包括数组)值的变化,所以无法写回到PersistentStorage中。
  • 不支持undefined 和 null 。

持久化数据是一个相对缓慢的操作,应用程序应避免以下情况:

  • 持久化大型数据集。
  • 持久化经常变化的变量。

PersistentStorage的持久化变量最好是小于2kb的数据,不要大量的数据持久化,因为PersistentStorage写入磁盘的操作是同步的,大量的数据本地化读写会同步在UI线程中执行,影响UI渲染性能。如果开发者需要存储大量的数据,建议使用数据库api。

PersistentStorage只能在UI页面内使用,否则将无法持久化数据。

PersistentStorage将选定的AppStorage属性保留在设备磁盘上。应用程序通过API,以决定哪些AppStorage属性应借助PersistentStorage持久化。UI和业务逻辑不直接访问PersistentStorage中的属性,所有属性访问都是对AppStorage的访问,AppStorage中的更改会自动同步到PersistentStorage。

PersistentStorage和AppStorage中的属性建立双向同步。应用开发通常通过AppStorage访问PersistentStorage,另外还有一些接口可以用于管理持久化属性,但是业务逻辑始终是通过AppStorage获取和设置属性的。

更多介绍,参见官方文档:PersistentStorage:持久化存储UI状态

其他资源

@ohos.data.preferences (用户首选项)

文档中心--Codelabs

AppStorage:应用全局的UI状态存储

健康生活应用(ArkTS)_华为开发者HarmonyOS专区小助手-华为开发者联盟HarmonyOS专区

harmonyOS鸿蒙-数据管理-用户首选项(@ohos.data.preferences)_harmonyos datapreferences-CSDN博客

HarmonyOS-AppStorage:应用全局的UI状态存储-CSDN博客  

HarmonyOS 网络请求以及数据持久化-CSDN博客

https://developer.huawei.com/consumer/cn/codelabsPortal/carddetails/tutorials_HarmonyOS-HealthyLife

https://developer.huawei.com/consumer/cn/codelabsPortal/carddetails/tutorials_Preferences

HarmonyOS应用开发-首选项与后台通知管理[email protected]博客