第2章 Room与Flow基础概念:Room DAO返回Flow、Flow冷流特性、数据变更自动通知
2.1 原理概述
为避免主线程ANR,Room官方推荐将DAO方法返回类型定义为Flow<...>。当数据库表数据发生变化时,Room会自动触发Flow重新发射查询结果,从而无需手动刷新。Flow的冷流特性保证只有存在收集者(Collector)时才执行数据库查询,避免后台空转。结合协程调度器,可将数据库操作切至Dispatchers.IO,而结果收集仍在主线程安全地更新UI。
2.2 Room DAO返回Flow
定义一个返回Flow<List<User>>的DAO查询方法:
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY id ASC")
fun getAllUsers(): Flow<List<User>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
@Delete
suspend fun deleteUser(user: User)
}
关键点:
- 返回值类型:必须是
Flow<T>,Room才能包装可观察查询。 - 非挂起函数:返回Flow的函数不应标注
suspend ,因为Flow本身是冷的,不需要立即执行。 - 自动通知:Room会内部注册
InvalidationTracker,监听涉及该查询表的变化,发现变更后重新执行SQL并发射新数据。
2.3 Flow冷流特性
Flow是冷数据流,意味着:
- 没有收集者时,不执行任何数据库查询。
- 每个收集者都会独立触发一次完整的查询执行。
- 适合与
Dispatchers.IO配合,将查询逻辑移至后台线程。
对比与LiveData:
| 特性 | Flow(冷流) | LiveData |
|---|---|---|
| 背压处理 | 原生支持(buffer/flowOn) | 不支持 |
| 线程切换 | 通过flowOn显式控制 | 自动在主线程分发 |
| 执行时机 | 只有collect时才执行 | 观察者绑定即开始 |
| 自动通知 | Room InvalidationTracker驱动 | Room LiveData驱动 |
2.4 数据变更自动通知机制
Room使用InvalidationTracker监听SQLite表级别的变更。当执行@Insert、@Update、@Delete后,Room会标记相关表为“失效”。下一次仅当Flow被收集时,InvalidationTracker会触发重新查询并发射新数据。流程如下:
- UI通过
collect启动Flow收集。 - Flow内部调用
Transactor执行SQL查询,返回游标结果。 - Room注册对应表的观察者。
- 任意数据变更后,InvalidationTracker通知Flow重新执行查询。
- 新Flow数据通过协程调度器发射到收集者。
2.5 完整实现:避免ANR的配置
在ViewModel中收集Flow,并设置flowOn(Dispatchers.IO)确保数据库操作在后台线程:
class UserViewModel(application: Application) : AndroidViewModel(application) {
private val userDao: UserDao =
AppDatabase.getInstance(application).userDao()
val allUsers: Flow<List<User>> = userDao.getAllUsers()
.flowOn(Dispatchers.IO) // 数据库查询在IO线程
.catch { e ->
// 错误处理,避免崩溃
emit(emptyList())
}
}
// Fragment或Activity中收集
lifecycleScope.launch {
viewModel.allUsers.collect { users ->
// 此回调在主线程(Dispatchers.Main)
adapter.submitList(users)
}
}
注意点:
- 不要在
collect内部直接切换线程,Collection默认在调用协程的上下文中执行(通常是主线程)。 - 使用
flowOn将上游(数据库查询)切至IO线程,下游(UI更新)保留在主线程。 - 配合
lifecycleScope和repeatOnLifecycle防止Activity销毁后继续收集。
2.6 调试技巧
- 开启Room日志验证查询频次:
Room.databaseBuilder(context, AppDatabase::class.java, "mydb") .setQueryCallback { sql, bindArgs -> Log.d("Room", "SQL: $sql, args: ${bindArgs?.joinToString()}") } .build() - 观察Flow是否重复发射:在
flowOn之前添加conflate()可合并密集通知,减少UI刷新次数。 - 使用
flow的onStart、onCompletion打印生命周期。
2.7 常见错误与避坑
错误1:在DAO方法上添加suspend — 导致Flow无法返回,编译错误。
错误2:在collect内部切换线程 — 使用withContext(Dispatchers.IO)会破坏冷流特性,应在上游用flowOn。
错误3:忘记处理catch — 数据库查询可能抛出异常(如磁盘满),需用.catch { emit(default) }避免崩潰。
优化提示:若查询频繁触发导致UI卡顿,可在flowOn后加buffer(Channel.CONFLATED)或conflate()丢弃中间值。
2.8 性能与稳定性优化
- 使用
distinctUntilChanged():避免相同数据重复发射到UI。 - 限制表监听范围:若只关心部分列变化,可定义
@RawQuery配合InvalidationTracker手动控制。 - 主线程安全:确保所有
collect都在Dispatchers.Main,所有耗时查询在Dispatchers.IO。 - 取消收集:使用
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED)自动管理生命周期。