第11篇:数据持久化——本地存储 鸿蒙中文编程
本文介绍了HarmonyOS应用开发中的数据持久化技术,重点讲解了本地存储的三种方式。主要内容包括:1) 首选项存储(Preferences)用于保存键值对数据,如用户设置;2) 关系型数据库存储结构化数据;3) 文件存储机制。文章提供了完整的代码示例,演示如何实现设置页面的数据保存与读取功能,包括深色模式切换、字体大小调整和用户名存储等常见场景。通过本教程,开发者可以掌握HarmonyOS应用中
·
第11篇:数据持久化——本地存储
本课目标:掌握本地数据存储,能保存和读取数据
**作者:**中文编程倡导者—— 李金雨
预计课时:2课时(90分钟)
难度等级:⭐⭐⭐(进阶)
一、开篇引入
1.1 为什么需要本地存储?
想象你正在写一个待办事项应用:
- 你添加了10个待办事项
- 关闭应用,重新打开
- 数据全没了! 😱
这就是没有本地存储的问题!
1.2 本地存储的应用场景
| 场景 | 说明 |
|---|---|
| 用户设置 | 记住用户的主题、字体大小等偏好 |
| 登录状态 | 记住用户的登录信息 |
| 缓存数据 | 保存网络请求的结果,下次快速显示 |
| 离线数据 | 没有网络时也能查看之前的数据 |
| 草稿保存 | 自动保存未完成的输入 |
1.3 本课目标
今天我们要学习:
- 首选项存储(Preferences)——存简单键值对
- 关系型数据库——存结构化数据
- 文件存储——存文件
- 实战:设置页面、离线笔记
1.4 预期成果
完成本课后,你能做出这样的应用:
设置页面: 离线笔记:
┌─────────────┐ ┌─────────────┐
│ ⚙️ 设置 │ │ 📝 我的笔记 │
├─────────────┤ ├─────────────┤
│ │ │ │
│ 深色模式 ☑️ │ │ [+ 新建笔记] │
│ │ │ │
│ 字体大小 │ │ 📄 笔记1 │
│ [小] [中] [大] │ 2024-01-15 │
│ │ │ │
│ 通知提醒 ☑️ │ │ 📄 笔记2 │
│ │ │ 2024-01-14 │
│ [保存设置] │ │ │
│ │ │ 📄 笔记3 │
└─────────────┘ │ 2024-01-13 │
│ │
└─────────────┘
二、概念讲解
2.1 首选项存储(Preferences)
什么是Preferences?
Preferences 是用来存储简单的键值对数据,比如:
- 用户名:“张三”
- 是否夜间模式:true
- 字体大小:16
就像一个小笔记本,记下简单的设置。
导入模块
import dataPreferences from '@ohos.data.preferences'
基本操作
import dataPreferences from '@ohos.data.preferences'
import context from '@ohos.app.ability.UIAbilityContext'
class 首选项工具 {
static async 获取实例(上下文: Context, 名称: string) {
return await dataPreferences.getPreferences(上下文, 名称)
}
// 保存数据
static async 保存(实例: dataPreferences.Preferences, 键: string, 值: string | number | boolean) {
await 实例.put(键, 值)
await 实例.flush() // 提交保存
}
// 读取数据
static async 读取(实例: dataPreferences.Preferences, 键: string, 默认值?: any) {
return await 实例.get(键, 默认值)
}
// 删除数据
static async 删除(实例: dataPreferences.Preferences, 键: string) {
await 实例.delete(键)
await 实例.flush()
}
}
完整例子
// 完整可运行代码,复制到 Index.ets 即可运行
import dataPreferences from '@ohos.data.preferences'
@Entry
@Component
struct Index {
@State 深色模式: boolean = false
@State 字体大小: number = 16
@State 用户名: string = ""
private 首选项实例: dataPreferences.Preferences = null
async aboutToAppear() {
// 获取首选项实例
this.首选项实例 = await dataPreferences.getPreferences(
getContext(this),
"mySettings"
)
// 读取保存的设置
await this.读取设置()
}
async 读取设置() {
this.深色模式 = await this.首选项实例.get('darkMode', false) as boolean
this.字体大小 = await this.首选项实例.get('fontSize', 16) as number
this.用户名 = await this.首选项实例.get('username', '') as string
}
async 保存设置() {
await this.首选项实例.put('darkMode', this.深色模式)
await this.首选项实例.put('fontSize', this.字体大小)
await this.首选项实例.put('username', this.用户名)
await this.首选项实例.flush()
console.log('设置已保存')
}
build() {
Column({ space: 20 }) {
Text('⚙️ 设置')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin(20)
// 用户名设置
Row() {
Text('用户名')
.fontSize(16)
TextInput({ text: this.用户名 })
.width(200)
.onChange((值) => {
this.用户名 = 值
})
}
.width('90%')
.justifyContent(FlexAlign.SpaceBetween)
// 深色模式
Row() {
Text('深色模式')
.fontSize(16)
Toggle({ type: ToggleType.Switch, isOn: this.深色模式 })
.onChange((值) => {
this.深色模式 = 值
})
}
.width('90%')
.justifyContent(FlexAlign.SpaceBetween)
// 字体大小
Column({ space: 10 }) {
Text('字体大小')
.fontSize(16)
.alignSelf(ItemAlign.Start)
Row({ space: 10 }) {
Button('小')
.backgroundColor(this.字体大小 == 14 ? '#2196F3' : '#F5F5F5')
.fontColor(this.字体大小 == 14 ? '#FFFFFF' : '#333333')
.onClick(() => this.字体大小 = 14)
Button('中')
.backgroundColor(this.字体大小 == 16 ? '#2196F3' : '#F5F5F5')
.fontColor(this.字体大小 == 16 ? '#FFFFFF' : '#333333')
.onClick(() => this.字体大小 = 16)
Button('大')
.backgroundColor(this.字体大小 == 20 ? '#2196F3' : '#F5F5F5')
.fontColor(this.字体大小 == 20 ? '#FFFFFF' : '#333333')
.onClick(() => this.字体大小 = 20)
}
}
.width('90%')
// 保存按钮
Button('保存设置', { type: ButtonType.Capsule })
.width('90%')
.height(50)
.backgroundColor('#4CAF50')
.margin({ top: 30 })
.onClick(() => this.保存设置())
// 预览
Column({ space: 10 }) {
Text('预览效果')
.fontSize(14)
.fontColor('#999999')
Text('这是一段预览文字')
.fontSize(this.字体大小)
.fontColor(this.深色模式 ? '#FFFFFF' : '#333333')
}
.width('90%')
.padding(20)
.backgroundColor(this.深色模式 ? '#333333' : '#F5F5F5')
.borderRadius(10)
.margin({ top: 20 })
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
}
}
2.2 关系型数据库(RDB)
什么是关系型数据库?
关系型数据库用来存储结构化数据,比如:
- 用户表(ID、姓名、年龄、电话)
- 商品表(ID、名称、价格、库存)
- 订单表(ID、用户ID、商品ID、数量)
就像Excel表格,有行有列。
导入模块
import relationalStore from '@ohos.data.relationalStore'
基本操作
import relationalStore from '@ohos.data.relationalStore'
class 数据库工具 {
private static rdbStore: relationalStore.RdbStore = null
// 初始化数据库
static async 初始化(上下文: Context) {
const 配置 = {
name: 'MyDatabase.db', // 数据库文件名
securityLevel: relationalStore.SecurityLevel.S1
}
this.rdbStore = await relationalStore.getRdbStore(上下文, 配置)
// 创建表
await this.创建表()
}
// 创建表
private static async 创建表() {
const SQL = `
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
createTime TEXT,
updateTime TEXT
)
`
await this.rdbStore.executeSql(SQL)
}
// 插入数据
static async 插入笔记(标题: string, 内容: string): Promise<number> {
let 值 = {
title: 标题,
content: 内容,
createTime: new Date().toISOString(),
updateTime: new Date().toISOString()
}
return await this.rdbStore.insert('notes', 值)
}
// 查询所有数据
static async 查询所有笔记(): Promise<Array<object>> {
let 结果集 = await this.rdbStore.query(
new relationalStore.RdbPredicates('notes')
.orderByDesc('updateTime')
)
let 列表: Array<object> = []
while (结果集.goToNextRow()) {
列表.push({
id: 结果集.getLong(结果集.getColumnIndex('id')),
title: 结果集.getString(结果集.getColumnIndex('title')),
content: 结果集.getString(结果集.getColumnIndex('content')),
createTime: 结果集.getString(结果集.getColumnIndex('createTime'))
})
}
结果集.close()
return 列表
}
// 更新数据
static async 更新笔记(id: number, 标题: string, 内容: string) {
let 值 = {
title: 标题,
content: 内容,
updateTime: new Date().toISOString()
}
let 条件 = new relationalStore.RdbPredicates('notes')
条件.equalTo('id', id)
return await this.rdbStore.update(值, 条件)
}
// 删除数据
static async 删除笔记(id: number) {
let 条件 = new relationalStore.RdbPredicates('notes')
条件.equalTo('id', id)
return await this.rdbStore.delete(条件)
}
}
2.3 文件存储
什么是文件存储?
文件存储用来保存文件,比如:
- 图片
- 音频
- 视频
- 大文本文件
基本操作
import fs from '@ohos.file.fs'
class 文件工具 {
// 写入文本文件
static async 写入文件(路径: string, 内容: string) {
let 文件 = fs.openSync(路径, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
fs.writeSync(文件.fd, 内容)
fs.closeSync(文件)
}
// 读取文本文件
static async 读取文件(路径: string): Promise<string> {
let 文件 = fs.openSync(路径, fs.OpenMode.READ_ONLY)
let 状态 = fs.statSync(文件.fd)
let 数组 = new ArrayBuffer(状态.size)
fs.readSync(文件.fd, 数组)
fs.closeSync(文件)
let 文本解码器 = new util.TextDecoder()
return 文本解码器.decodeToString(数组)
}
// 删除文件
static 删除文件(路径: string) {
fs.unlinkSync(路径)
}
// 检查文件是否存在
static 文件是否存在(路径: string): boolean {
try {
fs.accessSync(路径)
return true
} catch {
return false
}
}
}
三、动手实践
3.1 基础练习:记住用户名
做一个登录页面,记住用户名:
// 完整可运行代码,复制到 Index.ets 即可运行
import dataPreferences from '@ohos.data.preferences'
@Entry
@Component
struct Index {
@State 用户名: string = ""
@State 密码: string = ""
@State 记住我: boolean = false
@State 保存的用户名: string = ""
private 首选项: dataPreferences.Preferences = null
async aboutToAppear() {
this.首选项 = await dataPreferences.getPreferences(getContext(this), 'login')
// 读取保存的用户名
this.保存的用户名 = await this.首选项.get('savedUsername', '') as string
if (this.保存的用户名 != '') {
this.用户名 = this.保存的用户名
this.记住我 = true
}
}
build() {
Column({ space: 20 }) {
Text('🔐 登录')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.margin(30)
// 用户名输入
TextInput({ placeholder: '用户名', text: this.用户名 })
.width('85%')
.height(50)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.onChange((值) => this.用户名 = 值)
// 密码输入
TextInput({ placeholder: '密码', text: this.密码 })
.width('85%')
.height(50)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.type(InputType.Password)
.onChange((值) => this.密码 = 值)
// 记住我
Row() {
Text('记住用户名')
.fontSize(14)
.fontColor('#666666')
Toggle({ type: ToggleType.Switch, isOn: this.记住我 })
.onChange((值) => this.记住我 = 值)
}
.width('85%')
.justifyContent(FlexAlign.SpaceBetween)
// 登录按钮
Button('登录', { type: ButtonType.Capsule })
.width('85%')
.height(50)
.backgroundColor('#2196F3')
.margin({ top: 20 })
.onClick(() => this.登录())
// 提示
if (this.保存的用户名 != '') {
Text(`已保存用户名:${this.保存的用户名}`)
.fontSize(12)
.fontColor('#999999')
.margin(10)
}
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
}
async 登录() {
if (this.用户名 == '' || this.密码 == '') {
console.log('请输入用户名和密码')
return
}
// 保存或清除用户名
if (this.记住我) {
await this.首选项.put('savedUsername', this.用户名)
} else {
await this.首选项.delete('savedUsername')
}
await this.首选项.flush()
console.log('登录成功!')
}
}
3.2 进阶练习:离线笔记应用
做一个完整的离线笔记应用:
import dataPreferences from '@ohos.data.preferences'
interface 笔记数据 {
id: string
标题: string
内容: string
时间: string
}
// 完整可运行代码,复制到 Index.ets 即可运行
@Entry
@Component
struct Index {
@State 笔记列表: 笔记数据[] = []
@State 当前标题: string = ""
@State 当前内容: string = ""
@State 编辑模式: boolean = false
@State 编辑ID: string = ""
@State 显示表单: boolean = false
private 首选项: dataPreferences.Preferences = null
private readonly 存储键: string = 'notes'
async aboutToAppear() {
this.首选项 = await dataPreferences.getPreferences(getContext(this), 'notesApp')
await this.加载笔记()
}
async 加载笔记() {
let 数据 = await this.首选项.get(this.存储键, '[]') as string
this.笔记列表 = JSON.parse(数据)
}
async 保存笔记() {
await this.首选项.put(this.存储键, JSON.stringify(this.笔记列表))
await this.首选项.flush()
}
build() {
Column() {
// 标题栏
Row() {
Text('📝 我的笔记')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text(`共${this.笔记列表.length}篇`)
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding(20)
.backgroundColor('#FFFFFF')
// 笔记列表或表单
Stack() {
if (!this.显示表单) {
this.列表界面()
} else {
this.编辑界面()
}
}
.width('100%')
.layoutWeight(1)
// 底部按钮
if (!this.显示表单) {
Button('+ 新建笔记', { type: ButtonType.Capsule })
.width('90%')
.height(50)
.backgroundColor('#4CAF50')
.margin({ bottom: 20 })
.onClick(() => this.新建笔记())
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
列表界面() {
if (this.笔记列表.length == 0) {
Column({ space: 15 }) {
Text('📝')
.fontSize(80)
Text('还没有笔记')
.fontSize(18)
.fontColor('#999999')
Text('点击下方按钮创建第一篇笔记')
.fontSize(14)
.fontColor('#CCCCCC')
}
.justifyContent(FlexAlign.Center)
} else {
List({ space: 10 }) {
ForEach(this.笔记列表, (笔记: 笔记数据) => {
ListItem() {
Column({ space: 8 }) {
Row() {
Text(笔记.标题 || '无标题')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
Text('编辑')
.fontSize(14)
.fontColor('#2196F3')
.onClick(() => this.编辑笔记(笔记))
}
.width('100%')
Text(this.截取内容(笔记.内容, 50))
.fontSize(14)
.fontColor('#666666')
.maxLines(2)
Row() {
Text(笔记.时间)
.fontSize(12)
.fontColor('#999999')
Blank()
Text('删除')
.fontSize(12)
.fontColor('#F44336')
.onClick(() => this.删除笔记(笔记.id))
}
.width('100%')
}
.width('100%')
.padding(15)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.onClick(() => this.编辑笔记(笔记))
}
})
}
.padding(15)
}
}
@Builder
编辑界面() {
Column({ space: 15 }) {
// 工具栏
Row() {
Text('< 返回')
.fontSize(16)
.fontColor('#666666')
.onClick(() => {
this.显示表单 = false
this.清空表单()
})
Text(this.编辑模式 ? '编辑笔记' : '新建笔记')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Text('保存')
.fontSize(16)
.fontColor('#4CAF50')
.onClick(() => this.保存当前笔记())
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding(15)
// 标题输入
TextInput({ placeholder: '标题', text: this.当前标题 })
.width('95%')
.height(50)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.onChange((值) => this.当前标题 = 值)
// 内容输入
TextArea({ placeholder: '请输入内容...', text: this.当前内容 })
.width('95%')
.layoutWeight(1)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.onChange((值) => this.当前内容 = 值)
}
.width('100%')
.height('100%')
.padding(10)
}
新建笔记() {
this.编辑模式 = false
this.清空表单()
this.显示表单 = true
}
编辑笔记(笔记: 笔记数据) {
this.编辑模式 = true
this.编辑ID = 笔记.id
this.当前标题 = 笔记.标题
this.当前内容 = 笔记.内容
this.显示表单 = true
}
async 保存当前笔记() {
if (this.当前标题.trim() == '' && this.当前内容.trim() == '') {
console.log('标题和内容不能都为空')
return
}
let 现在 = new Date()
let 时间字符串 = `${现在.getFullYear()}-${this.补零(现在.getMonth() + 1)}-${this.补零(现在.getDate())}`
if (this.编辑模式) {
// 更新现有笔记
let 索引 = this.笔记列表.findIndex(笔记 => 笔记.id == this.编辑ID)
if (索引 != -1) {
this.笔记列表[索引] = {
id: this.编辑ID,
标题: this.当前标题,
内容: this.当前内容,
时间: 时间字符串
}
}
} else {
// 添加新笔记
this.笔记列表.unshift({
id: Date.now().toString(),
标题: this.当前标题,
内容: this.当前内容,
时间: 时间字符串
})
}
await this.保存笔记()
this.显示表单 = false
this.清空表单()
}
async 删除笔记(id: string) {
this.笔记列表 = this.笔记列表.filter(笔记 => 笔记.id != id)
await this.保存笔记()
}
清空表单() {
this.当前标题 = ""
this.当前内容 = ""
this.编辑ID = ""
}
截取内容(内容: string, 长度: number): string {
if (内容.length <= 长度) return 内容
return 内容.substring(0, 长度) + '...'
}
补零(数字: number): string {
return 数字 < 10 ? '0' + 数字 : '' + 数字
}
}
四、知识总结
4.1 存储方式对比
| 方式 | 适用场景 | 数据类型 | 容量 |
|---|---|---|---|
| Preferences | 简单设置 | 键值对 | 小 |
| 关系型数据库 | 结构化数据 | 表格 | 大 |
| 文件存储 | 文件、大文本 | 文件 | 大 |
4.2 Preferences速查
import dataPreferences from '@ohos.data.preferences'
// 获取实例
let 首选项 = await dataPreferences.getPreferences(上下文, '名称')
// 保存
await 首选项.put('键', 值)
await 首选项.flush()
// 读取
let 值 = await 首选项.get('键', 默认值)
// 删除
await 首选项.delete('键')
await 首选项.flush()
4.3 常见错误提醒
| 错误现象 | 原因 | 解决方法 |
|---|---|---|
| 数据没保存 | 没调用flush() | 保存后调用flush() |
| 读取为null | 键不存在 | 提供默认值 |
| 类型错误 | 存取类型不一致 | 确保类型匹配 |
| 数据丢失 | 应用卸载 | 这是正常的,需备份 |
五、课后作业
5.1 巩固练习(必做)
练习1:主题设置
实现主题设置功能:
- 选择主题颜色
- 选择字体
- 记住用户选择
- 应用重启后保持
练习2:阅读进度
实现阅读进度保存:
- 显示文章列表
- 记录每篇文章的阅读位置
- 下次打开时自动回到上次位置
练习3:离线收藏
实现收藏功能:
- 收藏网络文章
- 保存到本地
- 离线时也能查看
- 可以删除收藏
5.2 创意编程(选做)
创意1:记账本
- 记录收入和支出
- 按日期分类
- 统计月度收支
- 数据本地保存
创意2:单词本
- 添加生词
- 记录学习进度
- 复习提醒
- 本地存储所有单词
创意3:日记应用
- 写日记
- 添加心情标签
- 插入图片
- 按日期查看
5.3 下篇预习
下一篇是综合实战,我们将综合运用所学知识完成一个完整应用。复习前面所有内容,准备大作业!
恭喜你完成了第11篇的学习! 🎉
现在你已经掌握了本地存储,可以保存用户数据了。记住:本地存储让应用有记忆,用户体验更连贯!
下节课是最后一篇,我们将完成一个综合实战项目!
更多推荐

所有评论(0)