HarmonyOS 应用实战 92:设置开关别只改页面变量:Preferences、AppStorage 和 UI 同步

页面 State 里的设置重启就丢,直接散落多个 key 又难以迁移和重置。这个问题不能只靠页面上补一个提示解决,因为真正的断点在 libraryHAR/src/main/ets/repositories/PreferencesStore.ets、状态字段和服务 owner 之间。下面按现有工程事实展开:先看源码,再说明风险链路,最后给出可以落地的补强方式。

在这里插入图片描述

这篇解决什么:设置项持久化

角度 结论 落点
当前事实 PreferencesStore 提供 getBoolean/setBoolean,可以承载开关类设置。 libraryHAR/src/main/ets/repositories/PreferencesStore.ets
主要风险 页面 State 里的设置重启就丢,直接散落多个 key 又难以迁移和重置。 设置项需要集中 key、默认值、运行时 AppStorage 信号和迁移路径。
补强方向 SettingViewState 设置保存成功后再更新 AppStorage 信号,失败时回滚页面开关。
页面责任 设置项持久化 页面不直接调用 PreferencesStore 的底层 key。

设置项持久化 来说,这张表先把边界固定住:当前事实来自 libraryHAR/src/main/ets/repositories/PreferencesStore.ets,风险落在 设置项需要集中 key、默认值、运行时 AppStorage 信号和迁移路径。,补强方案只围绕 SettingViewState 展开,不把问题扩散成泛泛的 HarmonyOS 状态管理讨论。

现有代码锚点:设置项持久化 对应 libraryHAR/src/main/ets/repositories/PreferencesStore.ets

下面这段来自 The_Book_of_Answers 当前工程,锚点是 async setBoolean。它不是装饰性引用,而是判断这篇文章能不能落地的依据。

0127       return typeof v === 'boolean' ? v : fallback;
0128     } catch (e) {
0129       const err = e as BusinessError;
0130       hilog.warn(DOMAIN, TAG, 'getBoolean fail %{public}s %{public}s', key, JSON.stringify(err));
0131       return fallback;
0132     }
0133   }
0134 
0135   async setBoolean(storeName: string, key: string, value: boolean): Promise<void> {
0136     const s = this.store(storeName);
0137     try {
0138       await s.put(key, value);
0139       await s.flush();
0140     } catch (e) {
0141       const err = e as BusinessError;
0142       hilog.error(DOMAIN, TAG, 'setBoolean fail %{public}s %{public}s', key, JSON.stringify(err));
0143       throw err as Error;

这段代码说明:PreferencesStore 提供 getBoolean/setBoolean,可以承载开关类设置。 所以后续补强不能绕开这个事实;如果把 设置项持久化 直接写成页面局部变量,第一次交互可能看起来没问题,跨页面、重进或发布回归时就会暴露。

失败链路:问题通常不是突然出现

页面 State 里的设置重启就丢,直接散落多个 key 又难以迁移和重置。

环节 内容 排查动作
触发点 页面读取 SettingsService 确认输入来自哪里
断点 设置项需要集中 key、默认值、运行时 AppStorage 信号和迁移路径。 确认服务层有没有拦住
外显现象 设置重启丢失 确认页面有没有清楚反馈

这张断点表是给 设置项持久化 做回归时用的。先复现触发点,再检查 设置项需要集中 key、默认值、运行时 AppStorage 信号和迁移路径。 是否存在,最后看用户能不能从 设置重启丢失 这种现象里得到下一步动作。

先排除误区:设置项持久化 不是页面补丁

遇到 设置项持久化,最容易犯的错是先在页面里加一个局部状态,让当前场景看起来正常。这个做法短期能遮住现象,但不会处理 设置重启丢失 这类断点。

临时做法 看起来解决了什么 后续会留下什么
页面里直接改 @State 当前界面刷新了 服务和仓储没有真实状态
保存失败后直接返回 用户看不到报错 数据可能没有写入
每个入口各写一份判断 某个页面正常 其他入口继续出错
只看成功路径 操作流程很顺 失败分支无法复现
function unsafeHandleArticle92(reason: string): void {
  if (!reason) {
    return;
  }
  promptAction.showToast({ message: reason });
  // 只提示不收口,下一次进入仍可能遇到同一个 设置项持久化 问题。
}

这段反例的目的不是讲语法,而是提醒边界:设置项持久化 的根因要回到 设置保存成功后再更新 AppStorage 信号,失败时回滚页面开关。,页面只能表达反馈,不能替服务层决定数据是否已经可靠。

在这里插入图片描述

按照图里的顺序看,真正要拦住的是这一句:设置项需要集中 key、默认值、运行时 AppStorage 信号和迁移路径。 只要这个点没有收口,用户看到的现象就会在刷新、保存、返回、导入或发布时反复变化。

责任边界:设置项持久化 不该由谁兜底

SettingsService 管理持久化,页面只绑定 SettingViewState。

层级 应该负责 不应该负责
页面 页面不直接调用 PreferencesStore 的底层 key。 直接改底层 key 或吞掉失败
服务 设置保存成功后再更新 AppStorage 信号,失败时回滚页面开关。 让每个页面复制一套规则
仓储/配置 只保存和读取真实数据 理解页面交互语义

排查 设置项持久化 时先问 owner,再问信号,最后才看样式。像 设置重启丢失,通常不是单纯的 UI 细节,而是 只写页面 State 没被服务层收住。

状态模型:SettingViewState

建议把这件事压成一个明确模型。下面代码是建议补强,不表示当前工程已经存在同名接口。

interface SettingViewState {
  hapticEnabled: boolean; // 触感反馈开关
  animationEnabled: boolean; // 抽取动画开关
  lastUpdatedAt: number; // 设置更新时间
  loaded: boolean; // 是否已从 Preferences 读取
}

function createSettingViewState(input: Partial<SettingViewState>): SettingViewState {
  return {
    hapticEnabled: typeof input.hapticEnabled === 'boolean' ? input.hapticEnabled : false,
    animationEnabled: typeof input.animationEnabled === 'boolean' ? input.animationEnabled : false,
    lastUpdatedAt: typeof input.lastUpdatedAt === 'number' ? input.lastUpdatedAt : 0,
    loaded: typeof input.loaded === 'boolean' ? input.loaded : false,
  };
}

SettingViewState 至少要能回答三个问题:输入从哪里来、失败怎么表达、成功之后谁刷新。字段少一点没关系,但不能让页面靠猜来决定 页面不直接调用 PreferencesStore 的底层 key。

提交顺序:设置项持久化 要先有提交点

把状态模型写出来以后,还要约束提交顺序。否则接口名字很好看,实际调用仍可能在页面、服务和仓储之间来回绕。

  1. 页面先收集用户动作,只生成 SettingViewState 或等价输入。
  2. 服务读取必要的当前状态,执行 设置保存成功后再更新 AppStorage 信号,失败时回滚页面开关。
  3. 通过边界后再写仓储、Preferences 或运行时信号。
  4. 页面根据结果展示成功、失败、刷新或恢复入口。
async function submitArticle92(state: SettingViewState): Promise<void> {
  const result: SettingViewStateResult = await commitSettingViewState(state);
  if (!result.ok) {
    SettingViewStateNotice(result.reason);
    return;
  }
  AppStorage.setOrCreate('article92ChangedAt', result.changedAt);
}

这个顺序的价值在失败分支。只要 设置项持久化 没通过服务闸门,就不应该提前写入运行时信号,也不应该让页面表现得像已经保存成功。

服务闸门:先判断能不能提交

设置项持久化 的服务层应该先判断边界,再执行写入或刷新。下面的写法把 reason 返回给页面,是为了让 开关显示反了 能被解释,而不是只弹一个“失败”。

interface SettingViewStateResult {
  ok: boolean;
  reason: string;
  changedAt: number;
}

function explainSettingViewState(state: SettingViewState): string {
  if (!state.hapticEnabled) {
    return 'hapticEnabled 还没有确认';
  }
  return '';
}

async function commitSettingViewState(state: SettingViewState): Promise<SettingViewStateResult> {
  const reason: string = explainSettingViewState(state);
  if (reason) {
    return { ok: false, reason, changedAt: Date.now() };
  }
  return { ok: true, reason: 'ready', changedAt: Date.now() };
}

这里的关键是:设置项持久化 不满足边界时不要写仓储,也不要悄悄修改运行时状态。先返回原因,页面再决定展示刷新、重试、回退还是放弃入口。

页面接入:提示可以清楚,规则不要搬到 UI

页面要做的是把用户动作表达清楚。比如 页面不直接调用 PreferencesStore 的底层 key。,这类信息适合放到 UI;但真正的校验和写入顺序仍然应该在服务层。

@Builder
function SettingViewStateNotice(reason: string) {
  if (reason) {
    Column({ space: 8 }) {
      Text('设置项持久化')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
      Text(reason)
        .fontSize(13)
        .fontColor(AppColor.danger)
    }
    .padding(12)
    .border({ width: 1, color: AppColor.danger })
    .borderRadius(AppRadius.md)
  }
}

这种接入方式对 设置项持久化 有一个好处:页面可以保持轻量,服务仍然是规则 owner。以后再加列表页、设置页、恢复页或发布回归,也不用围绕 SettingViewState 复制判断条件。

结构收口:从源码到验证只走一条线

在这里插入图片描述

设置项持久化 的结构图要能互相对应:源码锚点证明当前事实,SettingViewState 描述新增边界,服务规则决定提交方式,验证点证明 关闭动画后重启应用仍保持关闭。 没有被吞掉。

验证路径:成功和失败都要跑

序号 验证点
1 关闭动画后重启应用仍保持关闭。
2 写入失败时,开关 UI 回到旧值。
3 新增设置项时有明确默认值。
4 清空设置后,页面恢复默认值并刷新。
Write-Host "查看 设置项持久化 的源码锚点"
rg -n "async\ setBoolean|SettingViewState" "D:\ProgramData\huawei\lesson\The_Book_of_Answers\libraryHAR\src\main\ets\repositories\PreferencesStore.ets"

Write-Host "查看相关状态字段"
rg -n "hapticEnabled|animationEnabled|lastUpdatedAt|AppStorageKey|AppPrefKey|StorageLink|updatedAt" "D:\ProgramData\huawei\lesson\The_Book_of_Answers"

这些命令只能检查 设置项持久化 的源码落点和关键字段,不代表已经完成真机交互。涉及 关闭动画后重启应用仍保持关闭。 这种场景时,还需要在 DevEco/设备侧补完整流程验证。

负向回归:设置项持久化 要故意跑坏一次

只跑成功路径很容易误判。设置项持久化 至少要准备一组负向样本,让服务层证明它会拒绝错误输入,而不是靠页面刚好没有触发问题。

负向样本 期望反应 说明
缺少 hapticEnabled 返回明确原因,不写仓储 验证主输入不是默认通过
触发 设置重启丢失 保留当前页面状态 验证失败分支不会静默返回
模拟 乐观更新失败未回滚 页面显示下一步动作 验证用户能继续处理
重复执行提交 结果保持一致 验证服务闸门能承受重进
async function runNegativeArticle92(state: SettingViewState): Promise<string> {
  const result: SettingViewStateResult = await commitSettingViewState(state);
  if (result.ok) {
    return 'unexpected success';
  }
  return result.reason;
}

这组负向样本能把 设置项持久化 从“看起来能用”推进到“失败时也能解释”。写文章时把这部分放出来,读者才能按自己的工程复现。

常见问题:先定位断点

现象 常见原因 优先检查
设置重启丢失 只写页面 State 使用 SettingsService 持久化
开关显示反了 乐观更新失败未回滚 写入失败后恢复旧值
新增设置 undefined 没有默认值和迁移 集中 SettingsKey 与默认值

遇到 设置项持久化 的异常时不要先改样式。先确认是哪一层没有交接:页面是否保留 hapticEnabled,服务是否统一处理规则,仓储或配置是否留下旧值,运行时信号是否发出。

落地顺序:先最小闭环,再补体验

  1. libraryHAR/src/main/ets/repositories/PreferencesStore.ets 附近确认当前链路,避免改错文件。
  2. 增加 SettingViewState 或等价状态模型,字段只保留能解释失败的部分。
  3. 设置项持久化 的规则放到服务层,页面只展示原因和下一步动作。
  4. 跑成功路径和失败路径,特别是 关闭动画后重启应用仍保持关闭。
const regressionForArticle92: string[] = [
  '关闭动画后重启应用仍保持关闭。',
  '写入失败时,开关 UI 回到旧值。',
  '新增设置项时有明确默认值。'
];

收口

设置项持久化 的处理方法可以压成一句话:SettingsService 管理持久化,页面只绑定 SettingViewState。 当前工程已经有可依赖的源码基础,文章里的新增接口和函数属于建议补强;真正落地时,优先保持 owner 清楚,再补页面反馈。

结论 内容 边界
可以直接复用 PreferencesStore 提供 getBoolean/setBoolean,可以承载开关类设置。 libraryHAR/src/main/ets/repositories/PreferencesStore.ets
建议新增 SettingViewState 设置保存成功后再更新 AppStorage 信号,失败时回滚页面开关。
暂不声明 真机行为、CSDN 上传、发布结果 没有实际执行就不写成已完成
结论 内容 边界
可以直接复用 PreferencesStore 提供 getBoolean/setBoolean,可以承载开关类设置。 libraryHAR/src/main/ets/repositories/PreferencesStore.ets
建议新增 SettingViewState 设置保存成功后再更新 AppStorage 信号,失败时回滚页面开关。
暂不声明 真机行为、CSDN 上传、发布结果 没有实际执行就不写成已完成
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐