卡片数据不丢失: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)。

相关推荐
大师兄666818 小时前
HarmonyOS 卡片 UI 三种玩法:普通卡片、动效卡片、Canvas 卡片
harmonyos·arkts·formkit·动效卡片·canvas卡片
全栈若城10 天前
HarmonyOS Pen Kit 实战:手写笔轻捏、双击与取色器完整集成
华为·harmonyos·手写笔·harmonyos6
是稻香啊10 天前
HarmonyOS6 ArkTS TimePicker 组件使用文档
harmonyos6
全栈若城14 天前
自定义 TabBar 实战:浮动标签栏与舵式标签栏
harmonyos·harmonyos6·三方库开发
是稻香啊14 天前
HarmonyOS6 ArkTS Tabs页签超出TabBar区域显示
harmonyos6
全栈若城22 天前
HarmonyOS6 半年磨一剑 - RcInput 组件核心架构与类型系统设计
架构·harmonyos6·三方库开发实战·rchoui·三方库开发
全栈若城1 个月前
HarmonyOS6 半年磨一剑 - RcInput 组件清空、密码切换与图标交互机制
架构·交互·harmonyos6·三方库开发实战·rchoui·三方库开发
全栈若城2 个月前
HarmonyOS 6 实战:Component3D 与 SURFACE 渲染模式深度解析
3d·架构·harmonyos6
全栈若城2 个月前
HarmonyOS 6 实战:使用 ArkGraphics3D 加载 GLB 模型与 Scene 初始化全流程
3d·华为·架构·harmonyos·harmonyos6