引言
在写一个小的Android Compose App时, 遇到了一个小问题
先看下行为
我的收藏列表是根据收藏名称从数据库查询到的(不要问为什么这么建立数据库, 我也知道是屎山, 这里只探讨这个Flow的行为), 可以看到, 在我们更新了收藏的名称之后, 收藏列表消失了.
其中收藏列表的渲染由FavorItem(selectedFavorItemListInTmp)
实现.
具体获取收藏列表实现如下
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddFavorScreen(
queryUserViewModel: QueryUserViewModel,
) {
val selectedFavorItemList by queryUserViewModel.selectedFavorItemList.collectAsState()
var selectedFavorItemListInTmp by remember(selectedFavorItemList) {mutableStateOf(selectedFavorItemList)}
val favorName by queryUserViewModel.favorName.collectAsState()
var favorNameTmp by remember(favorName) { mutableStateOf(favorNameFromOtherPage ?: favorName) }
LaunchedEffect(Unit) {queryUserViewModel.queryFavorItems()}
//Some code change favorNameTmp
Button(onClick = {
queryUserViewModel.saveUserFavor(
selectedFavorItemListInTmp,
favorNameTmp,
remarkTmp
);
}) { Text("保存") }
FavorItem(selectedFavorItemListInTmp)
}
@Composable
fun FavorItem(userList: List<ItemDetailState>) {
...
}
class QueryUserViewModel(){
val favorName = MutableStateFlow("")
val remark = MutableStateFlow("")
val selectedFavorItemList = MutableStateFlow<List<ItemDetailState>>(emptyList())
fun queryFavorItems() {
val favorName = favorName.value
viewModelScope.launch {
remark.value = UserFavorRepository.queryFavorRemark(favorName) ?: ""
UserFavorRepository.queryFavorItems(favorName).collect { user ->
selectedFavorItemList.value = user.map { UserDataClassToItemDetailState(it) }
}}
_queryState.value = SaveState.Success
}
fun saveUserFavor(
selectedFavorItemList: List<ItemDetailState>,
favorName: String,
favorRemark: String
) {
viewModelScope.launch {
UserFavorRepository.saveUserFavor(selectedFavorItemList, favorName, favorRemark)
}
}
}
原因
为什么收藏列表为空?
首先要知道这两句的含义
val selectedFavorItemList by queryUserViewModel.selectedFavorItemList.collectAsState()
var selectedFavorItemListInTmp by remember(selectedFavorItemList) {mutableStateOf(selectedFavorItemList)}
其中selectedFavorItemList
托管给了queryUserViewModel.selectedFavorItemList
selectedFavorItemListInTmp
是会根据selectedFavorItemList
变化而变化的.
那么在初始化的时候, 我们的selectedFavorItemList
为空列表, selectedFavorItemListInTmp
监控到selectedFavorItemList
发生变化, 也被委托复制为了空列表.
接着我们会进行
LaunchedEffect(Unit) {queryUserViewModel.queryFavorItems()}
初始化UI的时候, 进行一次数据库查询, 此时查询的参数为queryUserViewModel.favorName
, 这是外部赋值的, 不为空.
此时selectedFavorItemList
会变为查询结果, 同理selectedFavorItemListInTmp
也变为查询结果.
当我们更新了queryUserViewModel.favorName
时, 并没有触发我们自己定义的查询数据库函数queryUserViewModel.queryFavorItems()
UserFavorRepository.queryFavorItems(favorName).collect { user ->
selectedFavorItemList.value = user.map { UserDataClassToItemDetailState(it) }
其中queryFavorItems
返回类型为Flow<T>
, 则根据Room的Flow特性, 会将selectedFavorItemList.value
的值一直与user.map { UserDataClassToItemDetailState(it)
的值绑定, user
是从数据库获取的, 与数据库查询结果一直一致, 那么可推得selectedFavorItemList.value
的值也是与数据库绑定的.
则我们在更新了数据库的内容时, Room的Flow会"帮助"我们重新查询一次数据库, 并将值赋给selectedFavorItemList
, 从而使得我们的selectedFavorItemList
发生变化并同时影响到selectedFavorItemListInTmp
.
因为我们的查询参数并没有发生改变, 一直是queryUserViewModel.favorName
, 我们并没有更新viewModel
里面的参数, 但我们的数据库发生了改变, 原来的favorName
肯定查不到更新后的结果, 所以会变为空.
Android Room的Flow特性
如果我们使用Room, 这样写了一个查询
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
}
使用这种方式时, 每当数据库中的相关表发生变化, Room 会自动重新查询并发送新值到 Flow, 但变量本身不会自动更新, 需要在 collect 回调中更新变量, 我们在上文中使用了collect对其赋值, 所以只要我们的数据库一更新, 立马就会触发重新赋值的操作
实现原理
SQLite 触发器机制: Room 在底层使用了 SQLite 的触发器(Triggers)功能. 当我们定义一个返回 Flow 的查询方法时, Room 会自动为相关表创建 SQLite 触发器, 这些触发器会在表数据发生变化时被激活.
表无效化跟踪: Room 维护一个叫做
InvalidationTracker
的组件, 它负责跟踪数据库中各个表的变化状态. 当表数据发生修改(插入、更新或删除)时, 对应的触发器会通知InvalidationTracker
.Flow 集成: 当我们定义返回 Flow 的 DAO 方法时:
@Dao interface UserDao { @Query("SELECT * FROM users") fun getAllUsers(): Flow<List<User>> }
Room 会生成代码, 将查询与
InvalidationTracker
连接起来, 使用callbackFlow
或类似机制创建 Flow.数据库变化时的行为:
- 当数据库相关表被修改时, 触发器激活
InvalidationTracker
接收到通知- Room 重新执行查询
- 将新结果发送到 Flow 中
- 收集此 Flow 的组件(如 ViewModel)收到更新
解决方案
- 第一种解决方案就是在更新数据库后, 同时也更新查询参数, 但这样需要一次额外的查询
- 因为我们数据库的更新结果和
selectedFavorItemListInTmp
是一致的, 所以我们只需要selectedFavorItemListInTmp
不被更新即可,
UserFavorRepository.queryFavorItems(favorName).collect { user ->
selectedFavorItemList.value = user.map { UserDataClassToItemDetailState(it) }
}}
将这种写法替换成
val user = UserFavorRepository.queryFavorItems(favorName.first())
直接用first()截取flow流, 切断与数据库的连接