第11篇:数据持久化——本地存储

本课目标:掌握本地数据存储,能保存和读取数据
**作者:**中文编程倡导者—— 李金雨
预计课时:2课时(90分钟)
难度等级:⭐⭐⭐(进阶)


一、开篇引入

1.1 为什么需要本地存储?

想象你正在写一个待办事项应用:

  • 你添加了10个待办事项
  • 关闭应用,重新打开
  • 数据全没了! 😱

这就是没有本地存储的问题!

1.2 本地存储的应用场景

场景 说明
用户设置 记住用户的主题、字体大小等偏好
登录状态 记住用户的登录信息
缓存数据 保存网络请求的结果,下次快速显示
离线数据 没有网络时也能查看之前的数据
草稿保存 自动保存未完成的输入

1.3 本课目标

今天我们要学习:

  1. 首选项存储(Preferences)——存简单键值对
  2. 关系型数据库——存结构化数据
  3. 文件存储——存文件
  4. 实战:设置页面、离线笔记

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篇的学习! 🎉

现在你已经掌握了本地存储,可以保存用户数据了。记住:本地存储让应用有记忆,用户体验更连贯

下节课是最后一篇,我们将完成一个综合实战项目!

Logo

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

更多推荐