第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 的实现流程
- 定义实体类:使用
@Entity标记,指定表名和主键。 - 编写 DAO 接口:抽象方法返回
Flow<List<Entity>>或Flow<Entity?>。 - 构建 Room 数据库:包含 DAO 抽象类。
- 注入 Repository:将 DAO 的 Flow 暴露为
StateFlow或直接由 ViewModel 订阅。 - 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 分批执行,避免长时间锁定数据库。2.8 小结
Room 与 Flow 的集成大幅简化了离线数据的实时同步。开发者只需关注 DAO 的返回类型和缓存表设计,数据变化自动推送由 Room 底层 InvalidationTracker 完成。通过合理的时间戳、同步状态字段和索引,可以构建高效、可维护的离线缓存层。