HarmonyOS 鸿蒙Next中PersistenceV2使用求助

HarmonyOS 鸿蒙Next中PersistenceV2使用求助

引言

使用通用市场里的通用设置模板时,想将模板中的设置数据由AppStorageV2改造为PersistenceV2进行持久化存储,但是失败了。

预期

夜间模式,个性化推荐以及字体大小 三个设置项实现持久化

问题

字体大小设置已经实现持久化,但夜间模式和个性化推荐设置项未成功实现持久化, PersistenceV2异常回调日志内容为:

error key: SettingViewModel, reason: serialization, message: TypeError: Cannot set property when setter is undefined。

项目源码地址: https://gitcode.com/imashimaro/UserPerfenceSettingDemoV2

问了AI,试了各种方法还是不行,实在没招了,有大佬帮看下吗,感激不尽

-------------------分隔线------------------------------------

2026-06-05 18:34:58 再次编辑

首先非常感谢评论区各位大佬的热心回复,给了我很多启发。

今天突然开智了,之前陷入一个误区,一直在想法设法持久化通用模板组件里的SettingViewModel,反复在钻牛角尖😭,现在再看,思路确实跑偏了。

我整理了一下新的想法,分享出来供大家参考/指正:

以官方示例AppSettingSample1举例:

import {
  AboutModel,
  SettingItem,
  SettingGroup,
  SettingType,
  SettingView,
  SupportedSetting,
  WebModel,
  IFontModel
} from 'app_setting'

import { PersistenceV2 } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';

// 持久化异常回调
PersistenceV2.notifyOnError((key: string, reason: string, msg: string) => {
  hilog.error(0x0000, 'testTag', '%{public}s', `error key: ${key}, reason: ${reason}, message: ${msg}`);
});

// 封装要持久化的设置模型
@ObservedV2
 class AppSettingModel{
  @Trace fontSizeRatio: number = 2 // 自定义字体大小
  @Trace followSystem: boolean = false //是否跟随系统字体大小
  @Trace sysFontSizeScale: number = 1; // 系统字体大小
  @Trace themeModel: number = 1 // 主题模式 0随系统 1浅色 2暗黑
  @Trace personalRecommend: boolean = false //个性化推荐开关
}

@Entry
@ComponentV2
struct AppSettingSample1 {
  @Local navPathStack: NavPathStack = new NavPathStack();
  // 持久化设置模型
  @Local appSetting:AppSettingModel = PersistenceV2.globalConnect({
    type: AppSettingModel,
    defaultCreator: () => new AppSettingModel()
  })!

  @Local group1: SettingGroup = new SettingGroup()
    .setList([
      new SettingItem()
        .setType(SettingType.ROUTER)
        .setLabel('个人信息')
        .setRouterParam({ routerName: 'PersonalSample1' }),
    ])
  @Local group2: SettingGroup = new SettingGroup()
    .setList([
      new SettingItem()
        .setSupportedType(SupportedSetting.LIGHT_DARK_MODE)
        .setSelectIndex(this.appSetting.themeModel) //绑定设置模型的值
        .setOnChange((index) => {
          // 回调修改设置模型的值
          this.appSetting.themeModel = index as number
        })
      ,
      new SettingItem()
        .setSupportedType(SupportedSetting.FONT_SIZE)
        .setExtraParams({
          //绑定设置模型的值
          followSystem: this.appSetting.followSystem,
          fontSizeRatio: this.appSetting.fontSizeRatio,
          onChange: (followSystem: boolean, fontSizeRatio: number, sysFontSizeScale: number) => {
            // 回调修改设置模型的值
            this.appSetting.followSystem = followSystem
            this.appSetting.fontSizeRatio = fontSizeRatio
            this.appSetting.sysFontSizeScale = sysFontSizeScale
          }
        } as IFontModel)
    ])
  @Local group3: SettingGroup = new SettingGroup()
    .setList([
      new SettingItem()
        .setSupportedType(SupportedSetting.PUSH_SWITCH)
        .setOnChange((isOn) => {
          // 获取推送开关情况:isOn as boolean
        }),
      new SettingItem()
        .setSupportedType(SupportedSetting.CLEAR_CACHE)
        .setClickEvent(() => {
            PersistenceV2.remove(AppSettingModel)
        })
    ])
  @Local recommendItem: SettingItem = new SettingItem()
    .setType(SettingType.SWITCH)
    .setLabel('个性化推荐')
    .setSwitchValue(this.appSetting.personalRecommend)//绑定设置模型的值
    .setOnChange((isOn) => {
      // 回调修改设置模型的值
      this.appSetting.personalRecommend = isOn as boolean;
    })
  @Local group4: SettingGroup = new SettingGroup()
    .setTitle('数据和隐私')
    .setList([
      this.recommendItem,
    ])
  @Local group5: SettingGroup = new SettingGroup()
    .setTitle('了解我们如何使用您的数据')
    .setList([
      new SettingItem()
        .setSupportedType(SupportedSetting.WEB)
        .setLabel('业务与隐私的声明')
        .setExtraParams({ src: 'https://www.example.com' } as WebModel),
      new SettingItem()
        .setSupportedType(SupportedSetting.WEB)
        .setLabel('第三方共享信息清单')
        .setExtraParams({ src: 'https://www.example.com' } as WebModel),
      new SettingItem()
        .setSupportedType(SupportedSetting.WEB)
        .setLabel('第三方 SDK 列表')
        .setExtraParams({ src: 'https://www.example.com' } as WebModel),
      new SettingItem()
        .setSupportedType(SupportedSetting.WEB)
        .setLabel('已收集个人信息清单')
        .setExtraParams({ src: 'https://www.example.com' } as WebModel),
      new SettingItem()
        .setSupportedType(SupportedSetting.WEB)
        .setLabel('隐私声明摘要')
        .setExtraParams({ src: 'https://www.example.com' } as WebModel),
    ])
  @Local group6: SettingGroup = new SettingGroup()
    .setTitle('更多')
    .setList([
      new SettingItem()
        .setSupportedType(SupportedSetting.CHECK_VERSION),
      new SettingItem()
        .setSupportedType(SupportedSetting.ABOUT)
        .setExtraParams({
          icpText: '苏ICP备17040376号-135A',
          copyrightText: '华为xx 版权所有 © 2019-2025',
          hotline: '00000000',
        } as AboutModel),
      new SettingItem()
        .setSupportedType(SupportedSetting.WEB)
        .setLabel('服务协议与规则')
        .setExtraParams({ src: 'https://www.example.com' } as WebModel),
    ])

  build() {
    Navigation(this.navPathStack) {
      Column() {
        SettingView({
          pathStack: this.navPathStack,
          settingList: [this.group1, this.group2, this.group3, this.group4, this.group5, this.group6],
          // settingList: [this.group4],
        })

        Button('AppSettingModel')
          .onClick(() => {
            this.getUIContext().getPromptAction().openToast({
              message: `${JSON.stringify(this.appSetting)}`
            })
          })
          .margin({
            bottom: 20
          })
      }
      .width('100%')
      .height('100%')
    }
    .title('示例1(常规设置)')
    .backgroundColor($r('sys.color.background_secondary'))
    .mode(NavigationMode.Stack)
  }
}

仅仅以上还不够,需要稍稍修改下通用组件的逻辑

改动一:

SettingVM.handleDark中回调参数类型是ConfigurationConstant.ColorMode 而我们自定义的设置模型对应类型是number 所以这里修改下回调参数, 直接将对应Select的下标作为回调参数

 handleDark(item: SettingItem) {
    try {
      const options = item.selectOptions ?? lightDarkOptions;
      if (item.selectIndex < options.length && item.selectIndex >= 0) {
        const colorMode = options[item.selectIndex].value as ConfigurationConstant.ColorMode;
        this.uiContext?.getHostContext()?.getApplicationContext().setColorMode(colorMode);
      }

      item.setType(SettingType.SELECT)
        .setLabel(item.label ?? '夜间模式')
        .setSelectOptions(item.selectOptions ?? lightDarkOptions)
        .setSelectEvent((index: number) => {
          const colorMode = lightDarkOptions[index].value as ConfigurationConstant.ColorMode;
          this.uiContext?.getHostContext()?.getApplicationContext().setColorMode(colorMode);
          if (item.onChange) {
            // item.onChange(colorMode); ConfigurationConstant.ColorMode
            item.onChange(index);
          }
        })
    } catch (e) {
      Logger.error(TAG, 'handleDark fail, error: ' + JSON.stringify(e));
    }
  }

若是担心这样会破坏通用组件的完整性,我们也可以换一种修改方式:

// 1.在app_setting组件中的Index.ets新增
export {lightDarkOptions} from './src/main/ets/constants/LightDarkOptions'

// 2.AppSettingSample1.ets中使用
import {lightDarkOptions} form 'app_setting'

new SettingItem()
  .setSupportedType(SupportedSetting.LIGHT_DARK_MODE)
  .setSelectIndex(this.appSetting.themeModel)
  .setOnChange((colorMode) => {
  // 转换为我们需要的对应select的下标
  let index = lightDarkOptions.findIndex(mode => mode.value === colorMode as ConfigurationConstant.ColorMode)
  this.appSetting.themeModel = index
})

改动二

SetttingCard.toggleScene方法中绑定了一个onTouchIntercept事件,不知有什么用,不注释掉开关无法修改。 另外这个是回调的onClick事件,没有onChange事件,所以新增了绑定onClick事件

  // 修改前
  @Builder
  toggleScene(v: SettingItem) {
    Toggle({ type: ToggleType.Switch, isOn: v.switchV })
      .selectedColor(this.configInfo.themeColor)
      .onTouchIntercept(() => {
        if (v.onClick) {
          v.onClick(!v.switchV);
        }
        return HitTestMode.None;
      })
      }

  // 修改后
  @Builder
  toggleScene(v: SettingItem) {
    Toggle({ type: ToggleType.Switch, isOn: v.switchV })
      .selectedColor(this.configInfo.themeColor)
      .onChange(() => {
        v.onChange?.(!v.switchV)
      })
  }

OK,修改完后启动项目,这样设置的持久化就实现了。 欢迎继续讨论,感谢大家!


更多关于HarmonyOS 鸿蒙Next中PersistenceV2使用求助的实战教程也可以访问 https://www.itying.com/category-93-b0.html

12 回复

PersistenceV2 改造时要把它当成“V2 响应式对象的持久化”,不要直接按 AppStorageV2 的写法迁移。自动保存依赖 @ObservedV2 类和 @Trace 属性变化,非 @Trace 字段变化不会自动落盘,必要时要调用 PersistenceV2.save。

你这个模板改造建议重点查:1. 被 connect/globalConnect 的数据类是否是 class,并且需要持久化的字段都有初值;2. 夜间模式、个性化推荐是否是 @Trace 字段,而不是普通属性或 @Computed;3. 嵌套对象要用 @Type 指明类型,否则反序列化后可能变成普通对象,setter/响应式链路失效;4. 多 module 或多 Ability 场景优先用 globalConnect,并确保 key 唯一。

如果日志里有 Cannot set property when setter is undefined,通常说明反序列化回来的对象结构和状态类定义不匹配,先把持久化类简化成只含 boolean/number/string 的最小类验证,再逐步加回嵌套字段。

更多关于HarmonyOS 鸿蒙Next中PersistenceV2使用求助的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


  1. 明显错误的是,在参与序列化的类里用了@Computed。PersistenceV2是不支持在使用connect或globalConnect的类中使用@Computed。@Computed为只读属性,不支持赋值操作,因此会导致反序列化失败。参考《使用限制》
  2. 在ArkTS中,序列化与反序列化主要处理对象的数据(属性(变量)/状态)。你的代码里参与序列化的类里声明了很多类型,可以参考《支持的类型》检查下。参与序列化的属性,最好别声明定义成联合类型,会导致不确定的异常行为。

这个异常可以按“反序列化时写不回属性”来定位,不是 PersistenceV2 本身没有保存成功。日志里的 Cannot set property when setter is undefined,通常说明 SettingViewModel 里有被持久化的字段是 getter、@Computed 或只读计算属性,恢复数据时框架想赋值,但目标没有 setter。

建议把夜间模式、个性化推荐改成真正可写的 @Trace 字段,UI 文案用普通方法/派生 getter 来算,不要把 @Computed 放进 connect/globalConnect 的持久化类里;UIContext、AbilityContext 这类运行时对象也不要放进持久化模型。改完模型结构后,最好先 remove 旧 key 或清应用数据,否则旧序列化数据还会按老结构恢复。

import { PersistenceV2 } from '@kit.ArkUI';

@ObservedV2
class SettingViewModel {
  [@Trace](/user/Trace) darkMode: boolean = false;
  [@Trace](/user/Trace) personalized: boolean = true;
  [@Trace](/user/Trace) fontSize: number = 16;

  getDarkModeLabel(): string {
    return this.darkMode ? '已开启' : '已关闭';
  }
}

const vm = PersistenceV2.connect(
  SettingViewModel,
  'SettingViewModel',
  () => new SettingViewModel()
);

如果字段不是 @Trace,修改后要手动 PersistenceV2.save(key)。另外同一个 key 不要混用 connect/globalConnect,也不要连接过不同类型。

参考文档:

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-new-persistencev2

https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-statemanagement#persistencev2

滴滴,问题已解决,可以参考下

ersistenceV2 的自动持久化机制极其依赖 V2 的响应式装饰器:

  • 只有被 @Trace 装饰的属性发生改变时,才会自动触发磁盘持久化。
  • 如果模板中的设置数据类没有使用 @ObservedV2 标记类,且属性没有加 @Trace,那么你修改了设置,数据也不会同步到磁盘,导致重启后流失。

此外,在 V2 状态管理中,如果直接用一个新对象覆盖了通过 PersistenceV2.connect() 获取的对象,会导致该变量与底层的持久化代理失去绑定联系,从而导致保存失效。

有几个错误:

  1. 使用了@Computed装饰器
    不支持在使用了connect或者globalConnect的类中使用它
    @Computed是只读属性,会导致序列化失败
    去除改为普通方法

  2. SettingViewModel 类中使用了@Trace 修饰 UIContext和common.UIAbilityContext,也会导致序列化失败
    改为外部传值

  3. SettingViewModel中的一些 .setLabel方法需要改为三元运算符

这个错误更像是持久化反序列化时要回填对象属性,但目标属性只有 getter 或被做成了只读计算属性,没有可写 setter。

可以重点查夜间模式、个性化推荐这两个字段和字体大小字段的差异:

  1. 被 PersistenceV2 持久化的字段尽量使用普通可写属性,并加 @Trace

  2. 如果字段是 get xxx() 计算出来的,不要直接作为持久化字段;改成内部可写字段,UI 层再包装 getter。

  3. 嵌套对象需要用 @Type 标出真实类型,否则反序列化后可能按普通对象处理。

  4. 第一次 connect/globalConnect 建议提供 defaultCreator,并确认同一个 key 没有连接过不同类型。

  5. 修改模型结构后,先 remove 旧 key 或清应用数据,避免旧序列化数据继续按老结构恢复。

字体大小能成功,说明 PersistenceV2 链路本身大概率没问题,问题更可能在这两个 boolean 设置项的属性定义方式上。

在反序列化 SettingViewModel 时,代码试图给一个“没有 setter 的属性”赋值,所以抛出了:

TypeError: Cannot set property when setter is undefined
建议 你快速定位一下是那个字段 ,然后 修复该字段就好了

在HarmonyOS Next中,PersistenceV2 用于键值对本地持久化。

  • 导入:import { persistence } from '@kit.ArkData';
  • 创建实例:const store = new persistence.PersistenceV2('yourStoreName');
  • 写数据:store.put('key', value);
  • 读数据:store.get('key');
  • 删除:store.delete('key');
  • 同步:store.flush();
    需确保在 EntryAbilityonCreateonPageShow 中先执行 store.init() 初始化。

看到你成功解决了这个问题并如此详尽地分享思路和代码,实在是太棒了,这对遇到类似问题的开发者来说是宝贵的参考。

你遇到的 TypeError: Cannot set property when setter is undefined 错误,本质上是因为 PersistenceV2 在反序列化恢复数据时,无法直接设置通用模板组件内 SettingItem 等复杂对象的某些属性。直接将整个组件内部的复杂视图模型(ViewModel)持久化确实施倍功半。

你的最终方案非常清晰:

  1. 抽离数据,而非持久化视图:创建一个纯粹的 @ObservedV2 数据模型 AppSettingModel,只包含需要持久化的简单状态(number, boolean)。
  2. 建立数据桥梁:一方面,通过 PersistenceV2.globalConnect 将这个数据模型与持久化存储连接;另一方面,在 UI 层面将模型的值单向绑定到 SettingItem 的属性上。
  3. 通过回调更新数据模型:在 SettingItem 的各种 onChange 回调中,不再直接修改组件内部状态,而是转而修改你自定义的 AppSettingModel 实例的属性值。
  4. 修正组件行为:你的“改动一”和“改动二”精准地处理了通用模板组件内部回调参数类型不匹配(如 ColorMode vs number)以及 onTouchIntercept 阻止开关交互的细节问题。特别是将 handleDark 的回调参数从 ColorMode 调整为 index,完美适配了你的 number 类型字段。

这个思路——将需要持久化的纯净状态从复杂的视图逻辑中分离出来,形成一个独立的、可序列化的数据模型——是使用 PersistenceV2 时的最佳实践。感谢你的无私分享!

回到顶部