15、协程与Room数据库:Room协程支持、Flow与Room的响应式查询、使用Flow监听数据库变化、复杂查询与协程
说实话,Room 和协程的组合,是我在 Android 开发中最喜欢的搭档之一。记得我刚接触 Room 时,它还在用 AsyncTask 做异步查询,那代码写起来真是又臭又长。后来 Room 2.1 开始支持协程,我第一时间就迁移了过去——嗯,那感觉就像从绿皮车换成了高铁。
这一章,我们就来聊聊 Room 怎么跟协程、Flow 深度结合。说白了,就是让你的数据库操作既快又优雅。
Room 的协程支持:从 suspend 到 Flow
Room 从 2.1 版本开始,直接支持 suspend 函数。这意味着你可以在 DAO 里这样写:
@Dao
interface UserDao {
@Insert
suspend fun insertUser(user: User)
@Update
suspend fun updateUser(user: User)
@Delete
suspend fun deleteUser(user: User)
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User?
}
每个 suspend 函数都会在后台线程执行,不会阻塞主线程。我个人习惯在 Repository 层用 viewModelScope 调用这些方法:
class UserRepository(private val userDao: UserDao) {
suspend fun insertUser(user: User) {
withContext(Dispatchers.IO) {
userDao.insertUser(user)
}
}
}
这里有个小细节:Room 的 suspend 函数默认就在后台线程执行,你其实不需要再包一层 withContext(Dispatchers.IO)。但我见过不少项目里还是这么写,可能是为了保险起见。我个人建议:如果 DAO 方法已经声明为 suspend,直接调用就好,Room 内部会处理好线程切换。
Flow 与 Room 的响应式查询
这才是重头戏。Room 对 Flow 的支持,让数据库查询变成了「订阅-推送」模式。你想想看,以前我们要手动刷新列表,现在只要数据库一变化,UI 自动就更新了。
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY name ASC")
fun getAllUsers(): Flow<List<User>>
@Query("SELECT * FROM users WHERE age > :minAge")
fun getUsersOlderThan(minAge: Int): Flow<List<User>>
}
在 ViewModel 里,我们可以这样收集:
class UserViewModel(private val userDao: UserDao) : ViewModel() {
val allUsers: StateFlow<List<User>> = userDao.getAllUsers()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
}
这里我用了 SharingStarted.WhileSubscribed(5000),意思是当订阅者消失后,再等 5 秒才停止上游查询。为什么要等 5 秒?因为屏幕旋转时配置会重建,如果立刻停止再重启,会浪费一次数据库查询。5 秒的缓冲期足够应对配置变更了。
SharingStarted.Eagerly,Flow 会在 ViewModel 创建时就启动查询,即使没有订阅者。这在某些场景下会造成资源浪费。我个人更推荐 WhileSubscribed,配合一个合理的超时时间。
使用 Flow 监听数据库变化
Flow 的响应式查询背后,其实是 Room 的 invalidation tracker 在起作用。当数据库中的表数据发生变化时,Room 会通知所有订阅了该表的 Flow 重新执行查询。
来看一个实际场景:假设我们有一个聊天应用,需要实时显示未读消息数。
@Dao
interface MessageDao {
@Query("SELECT COUNT(*) FROM messages WHERE isRead = 0")
fun observeUnreadCount(): Flow<Int>
@Query("UPDATE messages SET isRead = 1 WHERE id = :messageId")
suspend fun markAsRead(messageId: Int)
}
在 UI 层,我们可以这样监听:
@Composable
fun UnreadBadge(viewModel: MessageViewModel) {
val unreadCount by viewModel.unreadCount.collectAsState()
if (unreadCount > 0) {
Badge(text = "$unreadCount")
}
}
每次调用 markAsRead 后,Room 会自动触发 observeUnreadCount 重新查询,Flow 会发射新值,UI 自动更新。整个过程不需要你写任何通知代码。
复杂查询与协程
当查询变得复杂时,协程的优势就更明显了。比如我们需要分页加载、多表关联、或者条件组合查询。
分页查询
@Dao
interface PostDao {
@Query("SELECT * FROM posts ORDER BY createdAt DESC LIMIT :limit OFFSET :offset")
suspend fun getPostsPage(limit: Int, offset: Int): List<Post>
@Query("SELECT COUNT(*) FROM posts")
suspend fun getPostCount(): Int
}
在 Repository 里,我们可以用协程做分页加载:
class PostRepository(private val postDao: PostDao) {
suspend fun loadPage(page: Int, pageSize: Int = 20): List<Post> {
val offset = (page - 1) * pageSize
return postDao.getPostsPage(pageSize, offset)
}
}
多表关联查询
Room 支持返回嵌套对象,配合协程非常方便:
data class UserWithPosts(
@Embedded val user: User,
@Relation(
parentColumn = "id",
entityColumn = "userId"
)
val posts: List<Post>
)
@Dao
interface UserDao {
@Transaction
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserWithPosts(userId: Int): UserWithPosts
}
这里用了 @Transaction 注解,确保查询的一致性。我遇到过一个问题:如果不加 @Transaction,在多表查询时可能会读到不一致的数据。比如用户信息更新了,但关联的帖子还没更新完。加上事务就能保证要么全部读到新数据,要么全部读到旧数据。
条件组合查询
有时候查询条件不是固定的,需要动态拼接。Room 不支持动态 SQL,但我们可以用协程做组合:
class ProductRepository(private val productDao: ProductDao) {
suspend fun searchProducts(
query: String?,
categoryId: Int?,
minPrice: Double?,
maxPrice: Double?
): List<Product> {
// 先构建基础查询
val baseQuery = StringBuilder("SELECT * FROM products WHERE 1=1")
val args = mutableListOf<Any>()
query?.let {
baseQuery.append(" AND name LIKE ?")
args.add("%$it%")
}
categoryId?.let {
baseQuery.append(" AND categoryId = ?")
args.add(it)
}
minPrice?.let {
baseQuery.append(" AND price >= ?")
args.add(it)
}
maxPrice?.let {
baseQuery.append(" AND price <= ?")
args.add(it)
}
// 使用 rawQuery 执行
return productDao.rawSearch(baseQuery.toString(), *args.toTypedArray())
}
}
知识体系总览
下面这张图总结了 Room + 协程 + Flow 的核心关系:
避坑指南
最后,分享几个我踩过的坑:
- Flow 查询不要做耗时操作:Flow 的查询是在 Room 的线程池执行的,如果你在查询里做了复杂计算,会阻塞其他数据库操作。
- 注意 Flow 的生命周期:在 Fragment 或 Activity 里收集 Flow 时,记得用
repeatOnLifecycle,否则可能造成内存泄漏。 - 不要滥用 @Transaction:只有需要保证多表查询一致性时才加。单表查询加事务反而会降低性能。
- Flow 的冷流特性:每次有新的订阅者,Flow 都会重新执行查询。如果你有多个订阅者,考虑用
shareIn或stateIn共享数据流。
好了,这一章的内容就到这里。Room 和协程的组合,说白了就是让数据库操作变得「声明式」——你只需要告诉 Room 你想要什么数据,剩下的它帮你搞定。这种编程体验,真的让人上瘾。