拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

鸿蒙App开发:用户首选项Preferences实战与工程化封装

鸿蒙App开发:用户首选项Preferences实战与工程化封装 做鸿蒙应用开发也算踩了不少坑最近在整理一个偏好设置模块时发现很多刚接触 HarmonyOS App 开发的朋友对用户首选项Preferences的理解还停留在“会用接口”的层面。实际上这个 API 虽然看起来简单但用得好不好直接决定了一个应用在“记住用户设置”这件事上是从容还是翻车。尤其你做的是像网约车 App、工具类应用、内容类应用这类需要保存登录态、主题、筛选条件的项目用户首选项几乎是绕不开的第一站数据存储方案。这篇东西我不会讲大而全的鸿蒙理论就围绕“用户首选项应用 App 开发”这个场景把我在实际项目里怎么选型、怎么写、怎么踩坑、怎么封装的完整过程摊开来说。适合那些已经在用 DevEco Studio 写过 Hello World、想快速把本地数据持久化做扎实的开发者。刚入门的也能看复杂概念我会拿生活里的例子打比方。1. 用户首选项到底解决什么问题1.1 先给用户首选项一个准确画像用户首选项Preferences是 HarmonyOS 提供的一种轻量级键值对存储能力官方定位是“用于保存应用的配置信息和用户偏好”。你可以把它理解成一张只有两个列的表格一个 key一个 value。存进去的时候按 key 写入取出来的时候按 key 读取没有复杂的查询语句、没有表结构设计就是这么直接。在 Android 开发里有一个非常像的东西叫 SharedPreferences如果你以前用 Android Studio 开发过安卓 App 项目上手鸿蒙的 Preferences 会感觉极其亲切。数据默认保存在应用沙箱内的文件中应用卸载即清除不需要你手动管理文件路径也不需要申请存储权限更不需要考虑多线程并发冲突——框架层面已经帮你把多数脏活累活干完了。那它和“用户首选项应用 App 开发”这个题目有什么关系其实绝大多数 App 里所谓的“记住我”功能拆开来看就是几个键值对用户ID、昵称、是否深色模式、上次选中的城市、消息推送开关。这些东西单独拿出来每一个都很小但合在一起就是完整的用户体验。Preferences 就是为这种场景量身定做的。1.2 为什么偏偏选它来做轻量持久化我在项目里见过不少新人在需要持久化时第一反应直接上关系型数据库比如 RDB。说实话如果只是存几个开关状态这有点杀鸡用牛刀。我拿实际对比给你看维度Preferences 用户首选项关系型数据库RDB文件读写数据模型键值对表、行、列支持 SQL任意格式使用难度极低几行代码高要建表写 SQL中要处理序列化适合数据量KB 级单个键值建议小MB 级以上视格式而定读取速度快适合频繁读中有解析开销慢全量读入典型场景设置项、状态位业务数据、列表日志、导出文件做用户首选项应用开发核心诉求就两个快、简单。Preferences 读取是同步的拿过来直接就能用不需要 await 数据库查询也不会有 Cursor 没关闭这种低级错误。而且它内部有缓存机制多次读同一个 key 不会每次都走磁盘这在设置页里频繁读取开关状态时体感特别明显。我当时选它还有一个原因不用引入额外的依赖和初始化代码。在 HarmonyOS 工程里Preferences 属于系统能力 SDK 的一部分直接在代码里 import 就能用不折腾 Gradle 依赖不折腾版本冲突。这对小项目来说太重要了。1.3 哪些场景适合、哪些场景千万别用虽然 Preferences 很好用但它不是万能的。我见过有人试图用它存整个列表数据结果数据量大以后读取越来越慢这就是典型的用错场景。适合用 Preferences 的场景用户登录态token、userId、过期时间界面偏好深色模式开关、字体大小、语言选择筛选条件上次选择的城市、排序方式启动状态是否首次启动、是否已经引导过音量、播放进度等轻量状态千万别用 Preferences 的场景大数据列表比如消息记录、订单历史这种应该用关系型数据库或分布式数据管理图片、文件资源应该存文件Preferences 只存路径高频修改的超大 JSON如果单条数据超过 KB 级哪怕只有一个 key也不建议塞这里读写和序列化开销会让你后悔需要跨设备实时同步的核心业务数据这种情况请优先考虑分布式数据库或云服务你想想网约车 App 里的订单列表会存 Preferences 吗肯定不会那必须是数据库加服务端接口。但网约车 App 里的“记住上次选的常用地址”“深色模式”“免密支付开关”这种用 Preferences 就非常合适。搞清楚边界比学会 API 更重要。2. 环境准备与工程搭建的细节2.1 开发工具与 SDK 版本怎么定写 HarmonyOS App 开发官方 IDE 是 DevEco Studio。如果你之前用惯了 Android Studio会发现两者很多地方类似但鸿蒙这边从工程创建到构建链路都是独立的一套。版本选择上我的建议是别用太老的版本。Preferences API 从 API 9 开始就很稳定了但如果你的目标设备是较新版本系统直接用 API 12 甚至更新的 SDK 编译能享受到更完善的类型提示和 ArkTS 语法支持。我目前常用的组合是 DevEco Studio 的稳定版加配套 SDK编译 SDK 选 API 12 以上兼容性没什么大问题。创建工程时模板选Empty Ability就够了Preferences 不需要额外的工程配置。这里要注意一点华为账号登录、签名配置这些可以后面再做本地开发调试用 auto signing 即可不影响你写业务逻辑。2.2 创建一个最小工程骨架工程建好后你会看到典型的鸿蒙工程结构entry模块应用主入口src/main/etsArkTS 源码目录entryability/EntryAbility.etsUIAbility 入口pages/Index.ets默认页面很多第一次从 Android 转过来的朋友会到处找 XML 布局文件其实鸿蒙用的是 ArkUI 声明式语法直接在.ets 文件里用build()方法写 UI 结构类似 Flutter 和 SwiftUI 的写法。我刚开始也不太习惯写多了发现这种声明式的好处是数据和 UI 绑定特别自然状态变量一改界面自动刷新。如果你的目标是开发一个鸿蒙 App 小项目比如做本地工具类应用这个骨架完全够了。App 可以用什么框架开发HarmonyOS 这边不需要纠结跨端框架直接用官方 ArkTS/ArkUI 就是最省事、性能最稳的方案。2.3 工程结构里那些容易被忽略的配置有两个配置点新人不注意会浪费不少时间第一module.json5 里的权限声明。Preferences 不需要任何权限所以你不需要往requestPermissions里加东西。如果你看到网上有些老文章让你申请存储权限那是早期版本 API 的误导别照抄。第二混淆和构建配置。Preferences 用的是字符串 key不涉及代码混淆适配所以不用像某些 SDK 那样配置 keep 规则。但你要注意key 的命名一旦线上发布后续尽量不要改动因为用户本地已经存了旧 key 的数据改名等于旧数据全部失效。这一点在设计阶段就要想清楚。启动流程上我的习惯是在EntryAbility的onCreate里初始化 Preferences 工具类传入 UIAbility 的上下文这样后续所有页面都能直接用同一个实例。后面讲到封装的时候我再详细说明。3. 核心 API 逐行拆解与读写实战3.1 获取 Preferences 实例的正确姿势Preferences 的所有操作都基于一个 Preferences 实例。获取实例的代码如下import { preferences } from kit.ArkData; import { common } from kit.AbilityKit; let context getContext(this) as common.UIAbilityContext; let pref preferences.getPreferencesSync(context, user_settings);第一行的kit.ArkData是新版本推荐的数据管理 Kit 导入方式如果你查老资料看到的是ohos.data.preferences那是旧包名功能一致但新项目建议直接按新方式写。getPreferencesSync接收两个参数第一个是上下文用来定位应用沙箱目录第二个是存储文件名叫user_settings也好、app_config也好本质上是把你偏好数据归到不同的“房间”里。我的建议是一个 App 只用一个 Preferences 文件不要按页面拆成多个。拆太碎会让初始化代码变多而且多个实例同时写文件还可能引入你完全不想排查的时序问题。同步接口拿实例后续的 get 和 put 也有同步版直接读就直接用不需要写一堆异步回调这对写设置页特别友好。3.2 数据写入put、flush 与异步落盘的秘密写入操作很简单pref.putSync(dark_mode, true); pref.putSync(nick_name, 小张); pref.putSync(login_token, token_string_here);这里有个关键点putSync只修改了内存中的缓存并没有马上写入磁盘。要真正持久化必须调用await pref.flush();flush()是异步的作用是把内存里的数据一次性落盘。你可能会问为什么不 put 一下就马上写盘因为频繁写磁盘很伤性能合并成一次 flush 效率最高。但副作用就是如果你 put 完直接杀进程没等 flush 完成数据就可能丢了。实际开发里我的做法是连续多个 put 之后统一 flush 一次比如保存整个设置页时凑齐所有字段再调 flush。如果单个操作非常重要比如登录 token那必须立即 flush不能省。这是我吃过亏换来的教训后面排查问题部分会再讲。3.3 数据读取默认值决定了你的代码能活多久读取接口长这样let darkMode pref.getSync(dark_mode, false) as boolean; let nickName pref.getSync(nick_name, ) as string;第二个参数是默认值也就是 key 不存在的时候返回什么。这个参数极其重要因为你不能保证每个 key 都存在用户第一次安装还没写过设置、旧版本升级过来没有新字段、数据被手动清空过都有可能。我见过有人图省事不传默认值直接getSync(dark_mode)返回 null 后一处理不好就崩了或者界面出现各种诡异状态。写默认值不只是为了兜底更是给你的类型转换兜底。ArkTS 对类型要求比较严格as转换之前你必须保证默认值的类型和实际存储类型一致否则拿到 undefined 后一切皆有可能。3.4 删除、监听与跨页面联动删除单个 keypref.deleteSync(login_token); await pref.flush();清空所有配置pref.clearSync(); await pref.flush();clearSync慎用最好是配合“恢复默认设置”这种明确的用户操作。以前我做一个设置页用户点“恢复默认”时直接 clear 整个文件结果把登录态也清了用户体验非常差。后来改成只删那些真正属于“用户设置”的 key登录态单独放另一个文件或者从清空列表里排除。这个设计问题一定要提前想。Preferences 还支持监听数据变化pref.on(change, (key: string) { console.info(偏好数据变更: ${key}); // 在这里刷新页面状态 });这个监听器在 App 内部所有页面更新同一个 Preferences 文件时都会触发非常适合做跨页面联动。比如深色模式开关在设置页改了首页、列表页都要跟着换主题靠这个回调广播一下各页面各自处理自己的刷新逻辑比手动用 EventHub 转一圈简单不少。注意监听器用完后要off注销特别是在页面销毁时要清理否则会内存泄漏。在aboutToDisappear里注销是标准操作。3.5 数据导出与备份能力Preferences 还提供了把数据导出成文件、再导入恢复的能力// 导入 preferences.importPreferences(getContext(this), backup_file_path); // 导出 preferences.exportPreferences(getContext(this), export_file_path);这个能力我一开始完全没注意直到做一个“设置备份”功能时才翻到。如果你做的是工具类 App给用户提供“备份设置到本地/云盘恢复设置”的功能这俩接口能省下不少序列化代码。但要注意这两个接口操作的是整个 Preferences 文件不是单个 key所以模块拆分设计还是要提前规划好。4. 完整实现做一个可用的偏好设置页4.1 页面 UI 与交互设计下面我用一个最简单的设置页串一遍完整流程。页面包含两块昵称输入框和深色模式开关另外加一个“保存设置”按钮保存时统一写入并落盘。UI 用 ArkUI 声明式写法Entry Component struct SettingPage { State nickName: string ; State darkMode: boolean false; build() { Column({ space: 16 }) { Text(用户偏好设置) .fontSize(20) .fontWeight(FontWeight.Bold) TextInput({ placeholder: 请输入昵称, text: this.nickName }) .onChange((value: string) { this.nickName value; }) Row() { Text(深色模式) Toggle({ type: ToggleType.Switch, isOn: this.darkMode }) .onChange((isOn: boolean) { this.darkMode isOn; }) } Button(保存设置) .width(100%) .onClick(() { saveUserSettings(); }) } .padding(20) .width(100%) } }这里的 State 变量就是页面级状态输入和开关变化时自动更新。注意按钮的设计我故意把保存操作集中到按钮上而不是每次 onChange 都写盘。这样一是减少磁盘写入频率二是逻辑清晰——用户改完所有项点一下保存全部生效。但如果你是做自动保存的交互比如开关一拨就生效那就在对应 onChange 里单独调用保存函数并立即 flush看产品需求。4.2 读取并回显用户偏好页面显示出来的时候需要从 Preferences 里把上次保存的值读出来回显到输入框和开关上。我在aboutToAppear里做这件事aboutToAppear() { const pref getUserPreferences(); this.nickName pref.getSync(nick_name, ) as string; this.darkMode pref.getSync(dark_mode, false) as boolean; }getUserPreferences()是封装好的获取实例函数内部会判断上下文和存储文件名。读取是同步操作在这里不会卡 UI因为 Preferences 有缓存首次启动也就一次磁盘读后续都在内存。实测这个页面启动速度没有明显感知差异。这里我想强调一个 ArkUI 的习惯不要直接在 build 里读取 Preferences。build 会因为各种状态变化被频繁调用你在 build 里做 IO 操作等于每次刷新都读一遍文件缓存性能白白浪费。要读就在生命周期函数里读比如aboutToAppear、onPageShow。4.3 保存与全局状态联动保存函数这样写import { preferences } from kit.ArkData; import { common } from kit.AbilityKit; function getUserPreferences(): preferences.Preferences { let context getContext(this) as common.UIAbilityContext; return preferences.getPreferencesSync(context, user_settings); } function saveUserSettings() { let pref getUserPreferences(); pref.putSync(nick_name, this.nickName); pref.putSync(dark_mode, this.darkMode); pref.flush().then(() { console.info(用户设置保存成功); }).catch((err: Error) { console.error(保存失败: ${err.message}); }); }如果你做了很深色模式保存后还得通知全局状态变化。我推荐用AppStorage来存放全局的深色模式标记保存时同步更新AppStorage.setOrCreate(darkMode, this.darkMode);其他页面通过StorageProp(darkMode)或者StorageLink(darkMode)绑定这个值UI 会自动响应。这样 Preferences 负责持久化AppStorage 负责运行时的 UI 状态分发两边各司其职是鸿蒙开发里很标准的配合姿势。4.4 多页面下数据变更的同步策略如果你的 App 有多个页面都会读写相同 key只靠 AppStorage 可能还是不够因为 AppStorage 生命周期和应用进程相关进程杀掉重启后就没了最终还是得从 Preferences 读。这种场景我建议在封装的工具类里加一层“版本号”机制或者在每个页面aboutToAppear时重新读取相关 key。你也可以利用 3.4 节提到的on(change)监听在非当前页面接收变更事件手动刷新。比如设置页修改主题后首页在后台收到了 change 回调就重新读取 dark_mode 并更新自己的状态。这个方案适合页面少、逻辑简单的小项目页面多了之后还是建议在统一的状态管理里做避免回调满天飞。5. 问题排查实录与性能调优5.1 数据丢失十有八九是没 flush我遇到最多的问题就是明明 put 了重启 App 之后数据没了。排查下来大多数是同一个原因没调用 flush或者 flush 还没完成进程就被杀了。这里提醒一下putSync 和数据落盘之间是异步的你 put 完后紧接着this.finish()退出应用进程直接被回收内存里的数据根本没机会写进磁盘。正确的做法是在退出前、切后台前、或者做完关键写入后显式调await flush()。如果是在 Ability 的onBackground里做就得等 flush 完成再走别图省事丢一个 pending promise 就不管了。有一个例外HarmonyOS 在系统正常退出时会尽量回收未完成的 flush但这不是官方保证的行为千万别赌。5.2 类型错乱一个 key 只能有一种性格Preferences 支持 string、number、boolean 以及它们的数组类型。同一个 key你用字符串写入再用布尔读取虽然不会直接报错但返回结果会不可预期。举个例子pref.putSync(dark_mode, false); // 字符串 false let value pref.getSync(dark_mode, true) as boolean; // 实际是字符串 falsevalue并不是 boolean 的 false而是字符串 “false”然后你拿去做三元判断会发现永远走的是 truthy 分支。这种 bug 极难排查因为编译不报错、运行不崩溃就是逻辑不对。我的解决方案很简单粗暴key 命名带类型前缀。比如settings_dark_mode_boolean、settings_nick_name_string或者在单独的常量类里定义所有 key写注释标明类型。这样读代码的时候一眼就知道该用什么类型读写避免别人接手时无意间改错。这属于很低成本但收益很高的工程习惯。5.3 界面卡顿别把 Preferences 当数据库有朋友说设置页打开有点卡我让他把相关的读取代码发给我结果他在aboutToAppear里循环读了 100 多个 key而且每个 key 都存了很大的数组。Preferences 单次读写虽然快但扛不住高频大量操作。虽然 get 是同步且内存缓存的但首次读取要加载整个文件到内存。文件被撑得越大第一次读取就越慢。为此我给的优化建议是单文件的键值对数量控制在几十个以内单个 value 尽量控制在小 KB 以内高频读写的状态可以和低频配置拆分到两个 Preferences 文件批量写入时先连续 putSync再统一 flush避免每写一个 key 就 flush 一次如果你发现自己确实需要存更大的数据那说明该引入关系型数据库 RDB 或者分布式数据了Preferences 的定位就是轻量非要拿它扛大件只会两边都难受。5.4 多模块共享统一入口才是正解项目一大多个模块都会读写 Preferences。如果每个模块都自己调getPreferencesSync容易出现两个问题文件名不统一各写各的文件数据互相看不到或者同一个文件被多处实例同时操作出现写覆盖。我的做法是做一个全局单例的 PreferencesUtil在入口统一初始化所有模块走同一个工具类// PreferencesUtil.ets import { preferences } from kit.ArkData; import { common } from kit.AbilityKit; const PREFERENCES_NAME app_user_preferences; class PreferencesUtil { private pref?: preferences.Preferences; init(context: common.UIAbilityContext) { this.pref preferences.getPreferencesSync(context, PREFERENCES_NAME); } put(key: string, value: preferences.ValueType) { this.pref?.putSync(key, value); this.pref?.flush(); } get(key: string, defaultValue: preferences.ValueType): preferences.ValueType { return this.pref?.getSync(key, defaultValue) ?? defaultValue; } delete(key: string) { this.pref?.deleteSync(key); this.pref?.flush(); } } export default new PreferencesUtil();然后在 EntryAbility 的onCreate里初始化一次onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { PreferencesUtil.init(this.context); }之后所有页面import PreferencesUtil from ./PreferencesUtil就能直接用了。我在第 6 节还会展开讲这个封装的更多扩展点。5.5 问题排查速查表现象大概率原因处理方案重启后数据丢失未调用 flush 或进程过早被杀写入后立即 flush关键数据等待完成回调读取结果类型不对同一 key 被不同类型读写key 命名带类型标识统一管理页面启动卡顿Preferences 文件过大拆文件、降单条数据大小、减少同步读取量设置文件被莫名清空误调 clearSync慎用 clear改为逐个 delete 指定 key多模块数据对不上多实例、文件名不统一全局单例工具类统一入口6. 工程化封装与项目扩展心得6.1 把 Preferences 包成一个更顺手的工具类上面的单例工具类只是最基础的一层。我实际项目里还会加几个能力第一个是key 常量管理。所有 key 单独放一个文件甚至用枚举类export enum SettingKey { NickName settings_nick_name_string, DarkMode settings_dark_mode_boolean, LoginToken settings_login_token_string, }这样写代码时不怕手滑拼错字符串DevEco 的代码提示也能帮你。第二个是类型安全的 gettergetString(key: string, defaultValue: string): string { return this.get(key, defaultValue) as string; } getBoolean(key: string, defaultValue: boolean): boolean { return this.get(key, defaultValue) as boolean; } getNumber(key: string, defaultValue: number): number { return this.get(key, defaultValue) as number; }把as转换收敛到工具类内部业务代码里就不需要到处做类型断言了看起来干净也更好维护。第三个是统一异常兜底。Preferences 操作虽然简单但沙箱异常、磁盘空间不足时也会抛错。我在工具类里捕获一下打日志但不会让它崩到上层。特别是flush失败时最好能留一条清晰日志方便线上排查。6.2 接上状态管理让数据驱动 UI把 Preferences 和 AppStorage 联动起来是鸿蒙开发中很舒服的一种写法。每个 key 在 App 启动时读取一次注册进 AppStorage后续 UI 只依赖 AppStorage 的状态变量设置项每次变更时同步写 Preferences 和 AppStorage。可能有人担心这样双重维护会不会冗余我的经验是完全不会因为定位不同——Preferences 管“跨启动持久化”AppStorage 管“运行时 UI 刷新”。一个小项目中用好这两个能力基本不需要引入额外的状态管理框架。如果你用到了StorageLink有一点要留意AppStorage 状态的初始值不一定来自 Preferences首次启动时 Preferences 里可能还没有数据。你需要在初始化时先从 Preferences 读取默认值写入 AppStorage再让页面绑定这个值。顺序反了页面上就会出现一瞬间的默认值闪烁。6.3 从个人项目到上架时间与成本估算很多朋友做鸿蒙 App 小项目时会关心“开发一个 app 并上架大概要多少钱”。我个人的经验是像用户首选项这种基础能力的开发本身不产生额外直接成本——SDK、IDE 都是官方免费提供的。成本主要在你的时间上一个设置页从零到跑通熟练的话半天到一天如果涉及主题联动、多页面同步、数据迁移再加两三天。至于上架需要准备应用签名、审核材料等个人开发者建议提前规划应用名称、图标、隐私说明这类基础素材真正走流程时能省不少来回沟通的时间。小步快跑先把核心功能做扎实比一开始就堆功能上线更有价值。6.4 一点额外的小技巧最后分享一个我自己比较受用的小习惯凡是进入 Preferences 的 key我都会在常量文件里统一管理并且命名带上模块和类型。例如settings_dark_mode_boolean而不是darksettings_login_token_string而不是token。这个习惯让我少踩了很多类型错乱的坑也让别人接手代码时能快速判断这个 key 能存什么。另外测试时别忘了用 DevEco Studio 自带的设备文件浏览器直接查看 Preferences 的落盘文件确认数据真的写进去了。我在排查时经常用它验证“到底是我代码没写对还是 UI 刷新问题”十次里有八次能立刻定位问题。开发阶段多花一分钟看文件上线后少花一小时猜 bug这笔账怎么算都划算。
返回列表