系列文章《鸿蒙开发从入门到专家》第 5 篇(中级篇)
前言
页面搭好了,导航通了,下一步是数据——应用重启后数据不能丢,用户配置要保存,业务数据要持久化。鸿蒙提供了多种数据存储方案,但官方文档的问题在于:每种方案都讲得很详细,却没有告诉你"什么时候该用哪种"。
我见过太多开发者一上来就用 RelationalStore 存用户配置,用 Preferences 存聊天记录,最后性能和可维护性双双崩盘。本文把鸿蒙的数据持久化方案按场景分类,帮你建立清晰的选型直觉。
一、Preferences:轻量键值存储的首选
1.1 基本用法
Preferences 是鸿蒙提供的轻量级键值存储方案,适合保存用户配置、开关状态、简单的缓存数据。
import { preferences } from '@kit.ArkData'
class AppPreferences {
private static instance: AppPreferences
private pref: preferences.Preferences | null = null
static async getInstance(): Promise<AppPreferences> {
if (!AppPreferences.instance) {
AppPreferences.instance = new AppPreferences()
let context = getContext()
AppPreferences.instance.pref = await preferences.getPreferences(context, {
name: 'app_config'
})
}
return AppPreferences.instance
}
async setString(key: string, value: string): Promise<void> {
await this.pref?.put(key, value)
await this.pref?.flush() // 立即写入磁盘
}
async getString(key: string, defaultValue: string = ''): Promise<string> {
return (await this.pref?.get(key, defaultValue)) as string
}
async setNumber(key: string, value: number): Promise<void> {
await this.pref?.put(key, value)
await this.pref?.flush()
}
async getNumber(key: string, defaultValue: number = 0): Promise<number> {
return (await this.pref?.get(key, defaultValue)) as number
}
async setBoolean(key: string, value: boolean): Promise<void> {
await this.pref?.put(key, value)
await this.pref?.flush()
}
async getBoolean(key: string, defaultValue: boolean = false): Promise<boolean> {
return (await this.pref?.get(key, defaultValue)) as boolean
}
}
// 使用示例
async function demo() {
let prefs = await AppPreferences.getInstance()
await prefs.setString('user_nickname', '张三')
await prefs.setBoolean('dark_mode', true)
await prefs.setNumber('font_size', 16)
let nickname = await prefs.getString('user_nickname')
console.info(`昵称: ${nickname}`)
}
1.2 API 26 新增能力
HarmonyOS NEXT 5.0 对 Preferences 做了增强:
支持 BigInt 和 Object 类型:之前只能存 string、number、boolean,现在可以存大整数和简单对象
支持指定 key 的数据变更订阅:可以监听某个 key 的变化
// 订阅特定 key 的变化
this.pref?.on('change', (key: string) => {
if (key === 'dark_mode') {
console.info('主题模式发生变化')
}
})
1.3 Preferences 的适用边界
性能基准:单次读写约 1-5ms,数据量大时 flush() 会阻塞,建议批量写入后再统一 flush。
二、RelationalStore:SQLite 的鸿蒙封装
当数据有结构化需求、需要多表关联查询、或者数据量超过 Preferences 的承载能力时,就需要上关系型数据库了。
2.1 基础 CRUD 操作
import { relationalStore } from '@kit.ArkData'
class TodoDatabase {
private rdbStore: relationalStore.RdbStore | null = null
private static instance: TodoDatabase
static async getInstance(): Promise<TodoDatabase> {
if (!TodoDatabase.instance) {
TodoDatabase.instance = new TodoDatabase()
let context = getContext()
const config: relationalStore.StoreConfig = {
name: 'todo.db',
securityLevel: relationalStore.SecurityLevel.S1
}
TodoDatabase.instance.rdbStore = await relationalStore.getRdbStore(context, config)
// 创建表
await TodoDatabase.instance.rdbStore.executeSql(`
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
priority INTEGER DEFAULT 0,
completed INTEGER DEFAULT 0,
create_time INTEGER,
update_time INTEGER
)
`)
}
return TodoDatabase.instance
}
// 插入
async insertTodo(todo: TodoItem): Promise<number> {
let values = {
title: todo.title,
content: todo.content,
priority: todo.priority,
completed: todo.completed ? 1 : 0,
create_time: Date.now(),
update_time: Date.now()
}
return await this.rdbStore?.insert('todos', values) || -1
}
// 查询全部
async queryAllTodos(): Promise<TodoItem[]> {
let predicates = new relationalStore.RdbPredicates('todos')
predicates.orderByDesc('create_time')
let resultSet = await this.rdbStore?.query(predicates, [
'id', 'title', 'content', 'priority', 'completed', 'create_time'
])
let todos: TodoItem[] = []
while (resultSet?.goToNextRow()) {
todos.push({
id: resultSet.getLong(resultSet.getColumnIndex('id')),
title: resultSet.getString(resultSet.getColumnIndex('title')),
content: resultSet.getString(resultSet.getColumnIndex('content')),
priority: resultSet.getLong(resultSet.getColumnIndex('priority')),
completed: resultSet.getLong(resultSet.getColumnIndex('completed')) === 1,
createTime: resultSet.getLong(resultSet.getColumnIndex('create_time'))
})
}
resultSet?.close()
return todos
}
// 条件查询:未完成的待办
async queryUncompleted(): Promise<TodoItem[]> {
let predicates = new relationalStore.RdbPredicates('todos')
predicates.equalTo('completed', 0)
.and()
.greaterThan('priority', 0)
.orderByDesc('priority')
let resultSet = await this.rdbStore?.query(predicates)
// ... 解析结果
return []
}
// 更新
async updateTodo(todo: TodoItem): Promise<void> {
let values = {
title: todo.title,
content: todo.content,
priority: todo.priority,
completed: todo.completed ? 1 : 0,
update_time: Date.now()
}
let predicates = new relationalStore.RdbPredicates('todos')
predicates.equalTo('id', todo.id)
await this.rdbStore?.update(values, predicates)
}
// 删除
async deleteTodo(id: number): Promise<void> {
let predicates = new relationalStore.RdbPredicates('todos')
predicates.equalTo('id', id)
await this.rdbStore?.delete(predicates)
}
}
interface TodoItem {
id?: number
title: string
content: string
priority: number
completed: boolean
createTime?: number
}
2.2 API 26 增强特性
HarmonyOS NEXT 5.0 对 RDB 的增强非常实用:
支持 BigInt 类型:处理大整数 ID 不再受限
attach/detach 多数据库:可以在一个连接中跨库查询
数据库变更通知携带数据 ID:订阅者能精确知道哪条数据变了
支持 TS 同步 API:不再需要处处写 await(注意:同步 API 会阻塞线程,UI 操作仍建议用异步)
新增只读模式配置:
isReadOnly: true,适合只读场景
2.3 RDB 的适用边界
三、ORM 封装:让数据库操作更优雅
手写 SQL 和 RdbPredicates 很繁琐,企业级项目通常需要一层 ORM 封装。鸿蒙生态中有一些社区 ORM 方案,但最稳妥的做法是自己封装一个轻量级的:
// 简化版 ORM 基类
abstract class BaseDao<T> {
protected abstract tableName: string
protected abstract primaryKey: string
protected rdbStore: relationalStore.RdbStore
constructor(rdbStore: relationalStore.RdbStore) {
this.rdbStore = rdbStore
}
async insert(entity: T): Promise<number> {
return await this.rdbStore.insert(this.tableName, entity as relationalStore.ValuesBucket)
}
async deleteById(id: number | string): Promise<void> {
let predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo(this.primaryKey, id)
await this.rdbStore.delete(predicates)
}
async queryById(id: number | string): Promise<T | null> {
let predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo(this.primaryKey, id)
let resultSet = await this.rdbStore.query(predicates)
// ... 解析结果
return null
}
}
// 具体 DAO 实现
class TodoDao extends BaseDao<TodoItem> {
protected tableName = 'todos'
protected primaryKey = 'id'
}
四、分布式数据:鸿蒙的杀手级能力
这是鸿蒙区别于 Android 和 iOS 的核心特性。基于分布式软总线,鸿蒙设备之间可以自动发现、自动组网,应用数据可以在手机、平板、PC 之间无缝流转。
4.1 分布式键值存储
import { distributedKVStore } from '@kit.ArkData'
class DistributedPreferences {
private kvManager: distributedKVStore.KVManager | null = null
private kvStore: distributedKVStore.SingleKVStore | null = null
async init(): Promise<void> {
let context = getContext()
this.kvManager = distributedKVStore.createKVManager({
context: context,
bundleName: context.applicationInfo.name
})
let options: distributedKVStore.Options = {
createIfMissing: true,
encrypt: false,
backup: false,
autoSync: true, // 自动同步到其他设备
kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION,
securityLevel: distributedKVStore.SecurityLevel.S1
}
this.kvStore = await this.kvManager.getKVStore('distributed_config', options)
}
async put(key: string, value: string | number | boolean): Promise<void> {
await this.kvStore?.put(key, value)
}
async get(key: string): Promise<distributedKVStore.ValueType | undefined> {
return await this.kvStore?.get(key)
}
// 监听数据同步事件
subscribeSync(callback: (entries: distributedKVStore.Entry[]) => void): void {
this.kvStore?.on('dataChange', distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_ALL, callback)
}
}
4.2 分布式关系型数据
对于更复杂的场景,RelationalStore 也支持分布式能力:
const config: relationalStore.StoreConfig = {
name: 'distributed_todo.db',
securityLevel: relationalStore.SecurityLevel.S1,
// 关键配置:启用分布式
customDir: 'distributed',
isReadOnly: false
}
// 数据变更会自动同步到同一华为账号下的其他设备
4.3 分布式数据的限制
五、选型决策树
需要持久化存储?
├── 数据量极小(< 几条)且结构简单?
│ └── 用 Preferences
├── 结构化数据,需要查询?
│ ├── 单设备使用?
│ │ └── 用 RelationalStore
│ └── 需要跨设备同步?
│ └── 用分布式 KVStore 或分布式 RDB
├── 图片/音视频等大文件?
│ └── 用文件系统(core.fileIO)或 MediaLibrary
└── 需要端云同步?
└── 用 CloudDB 或自己搭建后端
六、实战:一个完整的设置页数据管理
// SettingsViewModel.ets
class SettingsViewModel {
private prefs: AppPreferences
constructor() {
AppPreferences.getInstance().then(p => this.prefs = p)
}
// 主题设置
async setDarkMode(enabled: boolean): Promise<void> {
await this.prefs.setBoolean('dark_mode', enabled)
}
async isDarkMode(): Promise<boolean> {
return await this.prefs.getBoolean('dark_mode', false)
}
// 字体大小
async setFontSize(size: number): Promise<void> {
await this.prefs.setNumber('font_size', size)
}
async getFontSize(): Promise<number> {
return await this.prefs.getNumber('font_size', 16)
}
// 清除所有数据
async clearAllData(): Promise<void> {
await this.prefs.setString('user_token', '')
await this.prefs.setString('user_info', '')
// 同时清除数据库缓存
let todoDb = await TodoDatabase.getInstance()
// ... 执行清理操作
}
}
// SettingsPage.ets
@Entry
@Component
struct SettingsPage {
@State isDarkMode: boolean = false
@State fontSize: number = 16
private viewModel: SettingsViewModel = new SettingsViewModel()
async aboutToAppear() {
this.isDarkMode = await this.viewModel.isDarkMode()
this.fontSize = await this.viewModel.getFontSize()
}
build() {
Column() {
// 深色模式开关
Row() {
Text('深色模式')
.fontSize(16)
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.isDarkMode })
.onChange(async (value: boolean) => {
this.isDarkMode = value
await this.viewModel.setDarkMode(value)
})
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
Divider()
// 字体大小选择
Row() {
Text('字体大小')
.fontSize(16)
.layoutWeight(1)
Text(`${this.fontSize}px`)
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.onClick(() => {
// 弹出字体大小选择弹窗
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
小结与下篇预告
本文按场景梳理了鸿蒙的三种核心持久化方案:
Preferences:轻量 key-value,存配置、token、简单状态
RelationalStore:SQLite 封装,存结构化业务数据,支持复杂查询
分布式数据:鸿蒙独有,同一账号设备间自动同步
选型记住一句话:配置用 Preferences,业务数据用 RDB,跨设备用分布式,大文件用文件系统。
下一篇(第 6 篇)我们进入网络层:HTTP 请求的封装、数据流转与错误处理最佳实践。包括请求拦截、Token 刷新、缓存策略和离线优先架构。