B10_文件SharedPreferences与SQLite

Android 基础补强 B10|文件、SharedPreferences 与 SQLite,数据该放在哪里

摘要:分别保存学习草稿、界面偏好和文章记录,比较三种传统持久化方式的数据形态、更新成本及失败处理,再连接 DataStore 与 Room。

标签:Android、SQLite、SharedPreferences、文件存储、第一行代码

本文对应《第一行代码》第 3 版第 7 章,也为 28 天课程 D12、D13、D15 补充存储基础。选择存储工具之前,先问数据如何读取、如何更新、需要什么约束。文中的练习片段未编译运行,不修改你的项目数据库;真实迁移需要先明确已有 schema 和数据保留要求。

1. 三种数据,三种访问方式

一份学习草稿通常整体读取、整体编辑,文件容易表达;主题偏好是一小组键值,读取时需要默认值;收藏文章需要按 ID 去重、按时间排序、按条件查询,更适合关系型数据库。把收藏列表转成 JSON 塞进一个偏好键并非绝对做不到,但每次修改都要解码、替换和写回整份数据,约束也由自己承担。

文件格式自由意味着应用负责编码、完整性与版本。键值存储操作简洁,但不擅长复杂关联。数据库有 schema、查询和事务,学习成本更高,却能把某些正确性要求交给结构表达。选择时比较的是实际操作,不是给三种工具排一个抽象优劣名次。

2. 文件:路径、编码和关闭资源

应用专属内部存储适合不需要直接共享给其他应用的文件。通过 Context 提供的目录或文件 API 获取路径,不硬编码不同设备上的绝对目录;也要区分长期文件与可被系统回收的缓存目录。应用专属文件指南

下面把草稿写入内部文件,放在 Repository 的挂起函数中。ioDispatcher 通过构造器注入,便于替换测试调度器;context 使用合适的应用级 Context。代码演示资源关闭和离开主线程,不包含备份或原子替换等完整文档编辑能力。

kotlin 复制代码
suspend fun saveDraft(text: String) = withContext(ioDispatcher) {
    context.openFileOutput("learning_draft.txt", Context.MODE_PRIVATE)
        .bufferedWriter(Charsets.UTF_8).use { writer ->
            writer.write(text)
        }
}

suspend fun readDraft(): String = withContext(ioDispatcher) {
    context.openFileInput("learning_draft.txt")
        .bufferedReader(Charsets.UTF_8).use { reader -> reader.readText() }
}

首次读取文件可能不存在,应在调用边界把"没有保存过草稿"与"读取失败"分开处理。文件写入中断也可能留下不完整内容;重要草稿可以采用受支持的原子文件机制或临时文件替换策略,并验证失败恢复。一个 use 解决关闭资源,不自动保证整份业务数据原子提交。

3. SharedPreferences:理解旧工程的键值更新

传统设置可以通过 getSharedPreferences("settings", MODE_PRIVATE) 读取,使用 Editor 写入。读取时给出的默认值,是键不存在时的业务选择,不说明磁盘里一定已经保存了这个值。键名及值类型也属于持久格式,后续版本不能随意把同一键由字符串改成数字。

kotlin 复制代码
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val showAuthor = prefs.getBoolean("show_author", true)
prefs.edit().putBoolean("show_author", false).apply()

apply 先更新内存并异步写磁盘,不返回磁盘写入是否成功;commit 同步执行并返回结果,因此不应在主线程随意进行可能阻塞的写入。也不能由"异步"推断完全没有性能代价。新功能的小型设置应结合 DataStore 指南选择,学习 SharedPreferences 主要帮助阅读书本与维护存量实现。SharedPreferences 官方说明

如果两个地方分别读取旧值、加一、写回,即便单次写入能够提交,也不代表整个读改写过程没有竞争。学习存储时要把"单次写入"和"跨多步操作的原子性"分开,不能因为 API 简单就忽略并发语义。

4. SQLite:结构、参数与事务

收藏文章需要唯一 ID,可以在表结构中定义主键;按照时间倒序展示,使用带排序的查询;按 ID 删除,则用参数绑定表达目标。下面是 SQL 练习,并非完整 SQLiteOpenHelper,问号应由查询 API 的参数传入,不与用户输入直接拼接。

sql 复制代码
CREATE TABLE bookmark (
  article_id INTEGER PRIMARY KEY,
  title TEXT NOT NULL,
  created_at INTEGER NOT NULL
);

SELECT article_id, title, created_at
FROM bookmark
WHERE created_at >= ?
ORDER BY created_at DESC, article_id DESC;

DELETE FROM bookmark WHERE article_id = ?;

使用 Cursor 时需要关闭,访问磁盘也不应阻塞主线程。多个写入必须同时成功时使用事务,异常时回滚,不能只依赖"这几行代码挨着写"。修改表结构还需要迁移方案,直接删表重建会丢失用户收藏,不是通用升级策略。SQLite 存储指南

Room 在这些概念之上提供实体、DAO、查询校验与迁移组织,并不取消主键、索引或事务的意义。先读懂一条 SQL,再看对应 DAO 注解,可以避免把数据库理解成只会调用 insert 的黑盒。

5. 用同一组失败检查存储边界

文件实验保存带换行的中文草稿,重启后读取,预期编码和换行保留;再删除测试文件,确认首次无文件不会被误报成内容为空且保存成功。偏好实验切换作者显示开关,再重启,预期恢复选择;清除测试键后应回到约定默认值。

数据库实验连续插入相同 ID,观察所选冲突策略,然后按时间查询并核对相同时间的稳定次序。再在包含两个写入的测试事务中制造异常,预期两步共同回滚。每次只改一个条件,并在专用测试数据上操作,避免把真实收藏当作故障样本。

这些是操作与预期,未代表实际测试通过。完成后记录输入、存储结果和重启行为,便能说明三种方式分别保存了什么,而不是只展示调用过几个 API。

6. 原创面试问答与追问

问一:为什么不把所有数据都放 SharedPreferences? 复杂列表查询、唯一约束与关联更新会变成手工维护。追问:很小的列表一定不能放吗?可以,但要承认格式、并发和整份重写的成本。

问二:apply 返回后就保证磁盘保存成功吗? 不能这样理解,它异步写盘且不返回结果。追问:改 commit 就能主线程随便写吗?不能,同步磁盘操作可能阻塞界面。

问三:Room 出现后还有必要学 SQLite 吗? 有,DAO 查询仍依赖 SQL 语义。追问:最先掌握什么?主键、过滤、排序、参数绑定、事务与迁移的数据保留规则。

问答为原创补强,非面试鸭原题。验收时能为草稿、偏好与收藏各选一种方式并说明理由,能解释失败与重启行为,再连接 D12 的 Room 和 D15 的 DataStore,就完成了第 7 章的基础闭环。

相关推荐
鲲极3 小时前
居间业务数据统计看板 三条取数口径怎么归一才对得上
数据库·鲲极·鲲极系统
ao-weilai3 小时前
MySQL数据库:基本查询
android·数据库·mysql
java1234_小锋3 小时前
【技术专题】Mysql8 数据库 - Mysql8 查询数据
数据库·mysql
ToddyBear5 小时前
云厂商技术支持部门的原话:元数据库删了,我们也恢复不了[特殊字符]
数据库·产品运营
IanSkunk5 小时前
从检查设备到训练方案:眼科与视光中心设备联动的数据通路、字段口径与校验规则
服务器·数据库·负载均衡
ShineWinsu5 小时前
对于Redis:事务的解析
数据库·redis·mysql·缓存·面试·事务·acid
ao-weilai7 小时前
MySQL数据库:内置函数
android·数据库·mysql
LaughingZhu7 小时前
Product Hunt 每日热榜 | 2026-10-02
数据库·人工智能·深度学习·神经网络·搜索引擎
天疆说8 小时前
腾讯会议 Linux 版在 NVIDIA 显卡上看不到画面:EGL 像素配置与窗口视觉不匹配的排查与修复
linux·数据库·腾讯会议