第2章 Room数据库集成与Flow查询

Room 是 Android 官方 ORM 框架,原生支持 Kotlin 协程和 Flow。当 DAO 方法返回 Flow<T> 时,Room 自动监听数据库表变化,一旦表发生 insert、update、delete 操作,Flow 立即重新发射查询结果。这一特性天然适合离线状态恢复:本地缓存数据变化后,UI 层无需手动轮询即可获得最新快照。

2.1 离线缓存表设计要点

离线缓存表不仅要存储业务数据,还需记录元信息以支持数据过期、状态管理和增量更新。下表列出核心字段及其设计理由:

字段 类型 说明 索引建议
id INTEGER / TEXT 主键,与服务端 ID 保持一致 PRIMARY KEY
data TEXT / BLOB JSON 或 Protobuf 序列化的业务数据 无
last_updated INTEGER 时间戳(毫秒),用于判断缓存是否过期 索引加速过期清理
sync_status INTEGER 0=待同步,1=已同步,2=冲突 索引用于离线队列
version INTEGER 乐观锁字段,避免并发写入覆盖 无

设计时还需注意:

  • 联合索引:(sync_status, last_updated) 可加速待同步数据的查询。
  • 避免冗余字段:能通过计算得到的字段(如过期标识)不要直接存储。
  • 数据记录不超过 1MB;大数据量建议拆分为多行或使用 ContentProvider 分页。

2.2 Room DAO 返回 Flow 的实现流程

  1. 定义实体类:使用 @Entity 标记,指定表名和主键。
  2. 编写 DAO 接口:抽象方法返回 Flow<List<Entity>> 或 Flow<Entity?>。
  3. 构建 Room 数据库:包含 DAO 抽象类。
  4. 注入 Repository:将 DAO 的 Flow 暴露为 StateFlow 或直接由 ViewModel 订阅。
  5. UI 层收集:使用 repeatOnLifecycle 或 flowWithLifecycle 安全收集。
关键点:Room 内部使用 InvalidationTracker 监听表变化,每次表修改后触发重新查询。Flow 的每个发射都包含完整的结果集,不是增量补丁。

2.3 代码示例

// Entity
@Entity(tableName = "user_cache")
data class UserEntity(
    @PrimaryKey val id: String,
    val name: String,
    val avatar: String,
    val lastUpdated: Long = System.currentTimeMillis(),
    val syncStatus: Int = 0
)

// DAO
@Dao
interface UserDao {
    @Query("SELECT * FROM user_cache WHERE id = :userId")
    fun observeUser(userId: String): Flow<UserEntity?>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsert(user: UserEntity)

    @Query("DELETE FROM user_cache WHERE lastUpdated < :threshold")
    suspend fun cleanExpired(threshold: Long)
}

// Database
@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

// Repository
class UserRepository(private val userDao: UserDao) {
    fun observeUser(userId: String): Flow<UserEntity?> {
        return userDao.observeUser(userId)
            .map { entity ->
                // 可在此做数据转换或过期过滤
                entity?.takeIf { it.lastUpdated > System.currentTimeMillis() - 24 * 60 * 60 * 1000 }
            }
    }
}

2.4 数据变化自动推送机制

Room 的 Flow 推送基于“订阅-观察”模式:

  • 首次收集 Flow 时,Room 执行 SQL 查询并发射结果。
  • Row 级变化通过 InvalidationTracker 记录“哪些表被修改”。
  • 在下一个协程调度时,Room 重新执行查询并发射新数据。
  • 若表没有变化,Flow 保持挂起状态,不发射重复数据。
注意:如果 DAO 方法返回 Flow<T> 但内部使用了 UNION、JOIN 或窗口函数,Room 可能无法正确追踪所有关联表的变化。此时需手动调用 invalidateTable() 触发刷新。

2.5 调试与验证

在开发阶段,可添加日志观察 Flow 的发射次数:

userDao.observeUser("123")
    .onStart { Log.d("Flow", "开始监听") }
    .onEach { Log.d("Flow", "发射数据: $it") }
    .catch { Log.e("Flow", "错误: $it") }
    .flowOn(Dispatchers.IO)
    .collect()

插入数据后检查 logcat 是否出现新发射。若未出现,检查:

  • DAO 方法是否用 suspend 修饰?Room Flow 不需要 suspend,但写操作需要。
  • 表名是否与实体注解一致。
  • 数据库版本升级后是否导致表结构丢失。

2.6 常见错误

错误表现 原因 解决方案
Flow 只发射一次,后续不更新 DAO 方法中使用了 @Transaction 或返回了 LiveData 转换的 Flow 确保方法直接返回 @Query 的 Flow,不要做 asFlow() 包装
数据库写入后 UI 不刷新 写入发生在不同协程作用域,导致 InvalidationTracker 未触发 确认写入操作使用同一个 RoomDatabase 实例
Flow 背压导致 UI 卡顿 数据库频繁写入导致连续发射,收集端来不及处理 使用 .debounce() 或 .conflate() 合并获取最新值

2.7 性能与稳定性优化

  • 设置合理的查询粒度:避免在热点表上使用 SELECT *,明确列出需要的字段。
  • 使用 flowOn(Dispatchers.IO):Room 的查询默认在 IO 线程执行,但可显式指定以确保上层协程不受影响。
  • 避免长时间持有 Flow 订阅:在 ViewModel 中使用 viewModelScope 收集,或在 Fragment 中使用 repeatOnLifecycle(Lifecycle.State.STARTED)。
  • 定期清理过期缓存:通过 @Query 删除旧数据,减少表体积以提升查询性能。
当离线缓存表包含大量记录(超过万级)时,建议在最后更新字段上加索引,并在清理操作中使用 DELETE ... LIMIT 分批执行,避免长时间锁定数据库。
Room + Flow 数据变化自动推送流程 UI Layer Activity / Fragment ViewModel StateFlow Repository 缓存逻辑 Room DAO Flow<T> SQLite 表变更 InvalidationTracker collect 调用 Flow SQL查询 表变更 触发重新查询 Flow 自动推送最新数据给 UI

2.8 小结

Room 与 Flow 的集成大幅简化了离线数据的实时同步。开发者只需关注 DAO 的返回类型和缓存表设计,数据变化自动推送由 Room 底层 InvalidationTracker 完成。通过合理的时间戳、同步状态字段和索引,可以构建高效、可维护的离线缓存层。