引言

在写一个小的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)  
        }  
    }  
}    

原因

为什么收藏列表为空?

我们使用的数据库为SQLite, 兼容层为Room

首先要知道这两句的含义

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对其赋值, 所以只要我们的数据库一更新, 立马就会触发重新赋值的操作

实现原理

  1. SQLite 触发器机制: Room 在底层使用了 SQLite 的触发器(Triggers)功能. 当我们定义一个返回 Flow 的查询方法时, Room 会自动为相关表创建 SQLite 触发器, 这些触发器会在表数据发生变化时被激活.

  2. 表无效化跟踪: Room 维护一个叫做 InvalidationTracker 的组件, 它负责跟踪数据库中各个表的变化状态. 当表数据发生修改(插入、更新或删除)时, 对应的触发器会通知 InvalidationTracker.

  3. Flow 集成: 当我们定义返回 Flow 的 DAO 方法时:

    @Dao
    interface UserDao {
        @Query("SELECT * FROM users")
        fun getAllUsers(): Flow<List<User>>
    }
    

    Room 会生成代码, 将查询与 InvalidationTracker 连接起来, 使用 callbackFlow 或类似机制创建 Flow.

  4. 数据库变化时的行为:

    • 当数据库相关表被修改时, 触发器激活
    • InvalidationTracker 接收到通知
    • Room 重新执行查询
    • 将新结果发送到 Flow 中
    • 收集此 Flow 的组件(如 ViewModel)收到更新

解决方案

  1. 第一种解决方案就是在更新数据库后, 同时也更新查询参数, 但这样需要一次额外的查询
  2. 因为我们数据库的更新结果和selectedFavorItemListInTmp是一致的, 所以我们只需要selectedFavorItemListInTmp不被更新即可,
UserFavorRepository.queryFavorItems(favorName).collect { user ->  
                selectedFavorItemList.value = user.map { UserDataClassToItemDetailState(it) }  
            }}

将这种写法替换成

val user = UserFavorRepository.queryFavorItems(favorName.first())

直接用first()截取flow流, 切断与数据库的连接