卡片数据不丢失:Preferences 在 FormKit 里的正确用法

文章目录

      • [Preferences 是什么](#Preferences 是什么)
      • 单例封装:PreferencesUtil
      • [管理多张卡片的 formId](#管理多张卡片的 formId)
      • [在生命周期里正确使用 Preferences](#在生命周期里正确使用 Preferences)
      • 完整的数据流转图
      • [Preferences 的几个注意事项](#Preferences 的几个注意事项)

做卡片开发的时候,有个问题早晚会遇到:App 重启之后,卡片的数据为什么全没了?

问题根源很简单:FormExtensionAbility 是无状态的,每次调用都是全新实例,内存里的数据下次就没了。要保住数据,必须用持久化存储。HarmonyOS 提供了几种选择,其中 Preferences(轻量级键值存储)是 FormKit 场景用得最多的。

本文把 Preferences 在卡片开发里的所有典型用法全部覆盖,包括封装技巧、多卡片管理、数据恢复等。

Preferences 是什么

类比 Android 的 SharedPreferences,或者 Web 里的 localStorage

用大白话说:一个持久化的键值对字典,写入后重启也不会丢,App 更新也不会丢(除非手动清除数据或卸载 App)。

适合存储:

  • 卡片的 formId 列表
  • 用户在编辑页面做的选择
  • 卡片的状态(比如"展示步数"还是"展示天气")

不适合存储:

  • 大量数据(Preferences 不是数据库,存太多会有性能问题)
  • 频繁写入的数据(每次 flush 都有 I/O 开销)

单例封装:PreferencesUtil

FormEditDemoFormEditUIAbility 都用到了一个 PreferencesUtil 工具类,这是一个很好的封装模式,直接拿来用:

typescript 复制代码
// common/PreferencesUtil.ets
import { preferences } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';

const MY_STORE = 'formKitStore'; // Preferences 文件名,同一应用内唯一

export class PreferencesUtil {
  private static instance: PreferencesUtil;

  // 单例模式
  static getInstance(): PreferencesUtil {
    if (!PreferencesUtil.instance) {
      PreferencesUtil.instance = new PreferencesUtil();
    }
    return PreferencesUtil.instance;
  }

  /**
   * 获取 Preferences 实例
   * 注意:每次获取前先清缓存,保证读到磁盘上最新的数据
   */
  getPreferences(context: Context): preferences.Preferences | undefined {
    try {
      // 清除内存缓存,防止跨进程写入后读不到最新值
      preferences.removePreferencesFromCacheSync(context, MY_STORE);
      return preferences.getPreferencesSync(context, { name: MY_STORE });
    } catch (error) {
      console.error(`getPreferences 失败: ${(error as BusinessError).message}`);
      return undefined;
    }
  }

  /**
   * 写入一个键值对
   */
  put(context: Context, key: string, value: string | number | boolean): void {
    const pref = this.getPreferences(context);
    if (!pref) return;
    try {
      pref.putSync(key, value);
      // flush 将内存数据持久化到磁盘
      pref.flush((err) => {
        if (err) console.error(`flush 失败: ${err.message}`);
      });
    } catch (error) {
      console.error(`put 失败: ${(error as BusinessError).message}`);
    }
  }

  /**
   * 读取一个值
   */
  get<T>(context: Context, key: string, defaultValue: T): T {
    const pref = this.getPreferences(context);
    if (!pref) return defaultValue;
    try {
      return pref.getSync(key, defaultValue as preferences.ValueType) as T;
    } catch (error) {
      console.error(`get 失败: ${(error as BusinessError).message}`);
      return defaultValue;
    }
  }

  /**
   * 删除一个键
   */
  remove(context: Context, key: string): void {
    const pref = this.getPreferences(context);
    if (!pref) return;
    try {
      if (pref.hasSync(key)) {
        pref.deleteSync(key);
        pref.flush((err) => {
          if (err) console.error(`flush 失败: ${err.message}`);
        });
      }
    } catch (error) {
      console.error(`remove 失败: ${(error as BusinessError).message}`);
    }
  }

  /**
   * 检查键是否存在
   */
  has(context: Context, key: string): boolean {
    const pref = this.getPreferences(context);
    if (!pref) return false;
    return pref.hasSync(key);
  }
}

管理多张卡片的 formId

用户可以把同一张卡片添加多次,每次生成不同的 formId。你需要管理这些 formId,方便批量更新卡片。

typescript 复制代码
// 存储多个 formId 的方案:JSON 序列化为字符串
const FORM_IDS_KEY = 'formIdList';

// 添加一个 formId
function addFormId(context: Context, formId: string): void {
  const util = PreferencesUtil.getInstance();
  const idsJson = util.get<string>(context, FORM_IDS_KEY, '[]');
  const ids: string[] = JSON.parse(idsJson) as string[];
  
  if (!ids.includes(formId)) {
    ids.push(formId);
    util.put(context, FORM_IDS_KEY, JSON.stringify(ids));
    console.info(`formId ${formId} 已存储,当前共 ${ids.length} 张卡片`);
  }
}

// 删除一个 formId
function removeFormId(context: Context, formId: string): void {
  const util = PreferencesUtil.getInstance();
  const idsJson = util.get<string>(context, FORM_IDS_KEY, '[]');
  const ids: string[] = JSON.parse(idsJson) as string[];
  
  const filtered = ids.filter(id => id !== formId);
  util.put(context, FORM_IDS_KEY, JSON.stringify(filtered));
  console.info(`formId ${formId} 已删除,剩余 ${filtered.length} 张卡片`);
}

// 获取所有 formId
function getAllFormIds(context: Context): string[] {
  const util = PreferencesUtil.getInstance();
  const idsJson = util.get<string>(context, FORM_IDS_KEY, '[]');
  return JSON.parse(idsJson) as string[];
}

在生命周期里正确使用 Preferences

把上面的工具方法整合到 FormExtensionAbility 里:

typescript 复制代码
import { formBindingData, FormExtensionAbility, formProvider } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';

// 卡片数据模型
interface CardData {
  formId: string;
  displayContent: string; // 用户选择展示的内容
  lastUpdateTime: string;
}

const CARD_DATA_PREFIX = 'card_'; // 每张卡片数据的 key 前缀

export default class EntryFormAbility extends FormExtensionAbility {

  onAddForm(want: Want) {
    const formId = want.parameters?.[
      'ohos.extra.param.key.form_identity'
    ] as string ?? '';

    // 记录 formId
    addFormId(this.context, formId);

    // 尝试读取该 formId 之前的数据(用户重新添加已删除的卡片时恢复)
    const util = PreferencesUtil.getInstance();
    const savedDataStr = util.get<string>(this.context, CARD_DATA_PREFIX + formId, '');
    
    let displayContent = '默认内容';
    if (savedDataStr) {
      const savedData = JSON.parse(savedDataStr) as CardData;
      displayContent = savedData.displayContent;
    }

    return formBindingData.createFormBindingData({
      'message': displayContent,
      'formId': formId
    });
  }

  onDestroyForm(formId: string) {
    // 从 formId 列表中移除
    removeFormId(this.context, formId);
    // 清理该卡片的数据(可选,保留的话重新添加时可以恢复)
    // PreferencesUtil.getInstance().remove(this.context, CARD_DATA_PREFIX + formId);
    console.info(`卡片 ${formId} 已删除`);
  }

  onFormEvent(formId: string, message: string) {
    const msg = JSON.parse(message) as Record<string, string>;
    
    if (msg['msgType'] === 'updateContent') {
      const newContent = msg['content'];
      
      // 持久化用户的选择
      const cardData: CardData = {
        formId: formId,
        displayContent: newContent,
        lastUpdateTime: new Date().toISOString()
      };
      PreferencesUtil.getInstance().put(
        this.context,
        CARD_DATA_PREFIX + formId,
        JSON.stringify(cardData)
      );

      // 更新卡片展示
      formProvider.updateForm(
        formId,
        formBindingData.createFormBindingData({
          'message': newContent,
          'lastUpdate': new Date().toLocaleTimeString()
        })
      );
    }
  }
}

完整的数据流转图

Preferences 的几个注意事项

跨进程读写一定要清缓存

FormExtensionAbilityUIAbility 运行在不同进程,两边都可能读写同一个 Preferences 文件。一边写入后,另一边如果不清缓存(removePreferencesFromCacheSync),读到的可能还是旧值。这是 FormKit 开发里一个经典坑。

flush 是异步的,别忘了

pref.putSync() 只是写到内存,必须调用 pref.flush() 才会真正写入磁盘 。如果 App 突然崩溃而没来得及 flush,数据就丢了。重要数据可以用同步版本 pref.flushSync(),但要注意主线程阻塞问题。

不适合存大对象

Preferences 底层是 XML 文件,存大量数据或者嵌套很深的 JSON 会有性能问题。如果需要存复杂的结构化数据,考虑用 relationalStore(SQLite)。

相关推荐
大师兄666813 天前
卡片状态驱动刷新——让不同卡片实例显示不同内容的正确方式
服务卡片·harmonyos6·formkit·状态驱动
大师兄666817 天前
从零开发一个 HarmonyOS 输入法——KikaInputMethod 完整拆解
harmonyos·服务卡片·harmonyos6·formkit
大师兄666819 天前
HarmonyOS 服务卡片开发之JS 卡片开发
javascript·华为·harmonyos·harmonyos6·formkit
爱编程的小新☆20 天前
Langchain4j对话记忆
数据库·缓存·持久化存储·langchain4j
大师兄666823 天前
HarmonyOS 卡片 UI 三种玩法:普通卡片、动效卡片、Canvas 卡片
harmonyos·arkts·formkit·动效卡片·canvas卡片
全栈若城1 个月前
HarmonyOS Pen Kit 实战:手写笔轻捏、双击与取色器完整集成
华为·harmonyos·手写笔·harmonyos6
是稻香啊1 个月前
HarmonyOS6 ArkTS TimePicker 组件使用文档
harmonyos6
全栈若城1 个月前
自定义 TabBar 实战:浮动标签栏与舵式标签栏
harmonyos·harmonyos6·三方库开发
是稻香啊1 个月前
HarmonyOS6 ArkTS Tabs页签超出TabBar区域显示
harmonyos6