4. 本地数据存储实现:Room 数据库增删改查、Kotlin 协程与 Flow、事务处理
第 4 章进入离线优先应用的核心基石——本地持久化。我们选择 Room 作为数据库层,配合 Kotlin 协程 与 Flow 实现响应式数据操作,并借助 Room 事务 保证原子性。本章通过原理讲解、代码实现、调试技巧、常见陷阱与优化策略,让你掌握一套生产级本地存储方案。
4.1 Room 架构与核心组件
Room 是 Android 官方推出的 SQLite 抽象层,提供编译期 SQL 校验、自动对象映射、协程与 Flow 原生支持。其架构分为三层:
| 组件 | 职责 | 注解 |
|---|---|---|
| Entity | 数据表结构,每字段映射一列 | @Entity, @PrimaryKey, @ColumnInfo |
| DAO | 数据访问对象,定义查询、插入、更新、删除 | @Dao, @Insert, @Query, @Update, @Delete |
| Database | 数据库容器,提供 DAO 实例 | @Database(entities = […], version = …) |
4.2 定义 Entity 与 DAO
以下以 Task 实体为例,包含 id、title、completed、createdAt 四个字段。
@Entity(tableName = "tasks")
data class Task(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "is_completed") val completed: Boolean = false,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
)
DAO 层使用 suspend 函数支持协程,返回 Flow 实现实时观察。
@Dao
interface TaskDao {
@Query("SELECT * FROM tasks ORDER BY created_at DESC")
fun getAllTasks(): Flow<List<Task>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: Task): Long
@Update
suspend fun updateTask(task: Task)
@Delete
suspend fun deleteTask(task: Task)
@Query("SELECT * FROM tasks WHERE id = :id")
fun getTaskById(id: Long): Flow<Task?>
}
最佳实践: DAO 中的查询应优先返回
Flow<T> 而非 LiveData,便于与 Kotlin 协程无缝集成,且 Flow 天然支持背压和冷流特性。
4.3 Database 与依赖注入
继承 RoomDatabase 并声明 DAO,使用 room.databaseBuilder 构建。推荐配合 Hilt 或 手动 DI 管理单例。
@Database(entities = [Task::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
companion object {
// 单例模式
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
4.4 Kotlin 协程与 Flow 集成
Room 的 DAO 方法默认在主线程执行会导致崩溃。通过 suspend 函数将操作切换到后台线程,Flow 则自动在 Dispatchers.IO 上执行查询。
典型使用流程:
- 在 ViewModel 中通过
viewModelScope.launch调用 suspend DAO 方法。 - 通过
.collect收集 Flow 中的列表数据,更新 UI。
// ViewModel 示例
class TaskViewModel(private val dao: TaskDao) : ViewModel() {
val allTasks: Flow<List<Task>> = dao.getAllTasks()
.flowOn(Dispatchers.IO) // 明确指定调度器
fun addTask(title: String) {
viewModelScope.launch(Dispatchers.IO) {
val task = Task(title = title)
dao.insertTask(task)
}
}
fun toggleCompleted(task: Task) {
viewModelScope.launch(Dispatchers.IO) {
dao.updateTask(task.copy(completed = !task.completed))
}
}
}
常见错误: 直接在主线程调用
suspend DAO 方法会导致 IllegalStateException: Cannot access database on the main thread。始终使用 Dispatchers.IO 或指定 .allowMainThreadQueries()(仅用于测试)。
4.5 事务处理
当需要执行多个原子操作(如同时更新两条记录)时,使用 @Transaction 注解。Room 会确保在同一数据库连接中串行执行,失败时回滚。
@Dao
interface TaskDao {
@Transaction
suspend fun moveTask(fromId: Long, toId: Long) {
// 模拟交换两个任务的排序字段
val taskA = getTaskByIdDirect(fromId)
val taskB = getTaskByIdDirect(toId)
val tempOrder = taskA.order
updateTask(taskA.copy(order = taskB.order))
updateTask(taskB.copy(order = tempOrder))
}
// 注意:事务内部不能使用 Flow 查询,应使用 suspend 版本
@Query("SELECT * FROM tasks WHERE id = :id")
suspend fun getTaskByIdDirect(id: Long): Task
}
事务的优点:
- 原子性 —— 全部成功或全部失败。
- 性能 —— 减少 SQLite 锁切换次数。
- 嵌套自动合并 —— Room 会合并嵌套事务。
4.6 数据迁移( Schema 变更)
版本升级时需编写 Migration 对象,否则会触发 fallbackToDestructiveMigration 导致数据丢失。
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE tasks ADD COLUMN priority INTEGER NOT NULL DEFAULT 0")
}
}
// 构建时传入 migration
Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
.addMigrations(MIGRATION_1_2)
.build()
4.7 内联 SVG:Room 数据流与协程交互图
4.8 调试与常见错误
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
| Cannot access database on the main thread | 在主线程调用 DAO 方法 | 使用 Dispatchers.IO 或 .allowMainThreadQueries()(仅用于测试) |
| Migration didn't properly handle ... | 缺失 Migration 或 Migration 错误 | 编写 Migration 并传入 addMigrations |
| java.lang.IllegalStateException: Room cannot verify the data integrity | 数据库 schema 与当前 Entity 不匹配 | 检查版本号、添加 Migration 或使用 fallbackToDestructiveMigration |
| Flow 不触发更新 | 使用了 @Transaction 内部调用 Flow 查询 | 事务内使用 suspend 查询而非 Flow |
| 数据库文件过大 | 未开启 WAL 模式或未清理旧数据 | 构建时调用 .setJournalMode(JournalMode.WRITE_AHEAD_LOGGING) |
4.9 性能与稳定性优化
- 启用 WAL 模式:提高并发读性能,构建时调用
.setJournalMode(JournalMode.WRITE_AHEAD_LOGGING)。 - 批量操作:插入多条记录时使用
@Insert传入List<Task>,并加上@Transaction提升性能。 - 索引敏感字段:对
SELECT频繁使用的字段(如用户 ID)添加@Index注解。 - 避免在 Flow 链中进行复杂变换:应在 ViewModel 中预计算,避免阻塞主线程。
- 合理使用
DistinctUntilChanged:避免 UI 层收到重复数据触发无谓重组。
总结: Room + Coroutine + Flow 是现代 Android 离线存储的标准组合。掌握 Entity/DAO/Database 的编写、事务的正确使用、迁移策略,是构建可靠离线优先应用的关键。第 5 章将在此基础上加入网络同步与数据冲突处理。