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 数据流与协程交互图

Room 数据流与协程交互图 SQLite Database 物理存储文件 Room DAO @Insert / @Query / @Update / @Delete @Transaction 原子操作 Kotlin 协程 viewModelScope Dispatchers.IO launch / async Flow 收集 collect / stateIn ViewModel / UI 状态更新 说明: DAO 中的 suspend 方法自动在 IO 调度器执行,Flow 查询由 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 章将在此基础上加入网络同步与数据冲突处理。