# 前言

俗话说得好,写代码一小时,修 bug 八小时。

今天在处理 Room 异步数据更新与 StateFlow 观察时出了一点 bug,这里浅浅记录一下。

Jetpack Compose 学习曲线还是陡啊……

# 问题描述

一个控件的回调函数中,使用 rememberCoroutineScope 进行异步执行 ViewModel 中的 从Room中删除指定Id记录 的函数,然后调用 Compose 中的 allItems 变量(该变量值为 ViewModel 内 Room Flow<List<T>> 类型的数据流通过 collectAsState 转化后的数据),判断 allItems 集合是否为空。

测试时 allItems 仅有一条记录,当执行完删除代码后按理说 allItems 的 isNotEmpty () 应该为 false,但测试后发现为 true

代码结构如下:

1
2
3
4
5
6
7
8
scope.launch {
viewModel.delete(item)
if (viewModel.allItem.isNotEmpty()) {
// ...
} else {
// ...
}
}

# 问题排查

一开始没注意是协程同步的问题,以为是 Room 异步 Delete 太慢,导致判断 isNotEmpty () 时还没删除完成,于是换了判断用的数据源,新增 Room 查询函数,直接从 Room 返回 List<T> 实例,结果还是出现问题。

问了一下 DeepSeek,排查总结如下:

  1. 确保 DAO 的 delete 方法是挂起函数
  2. 修改 Repository 中的删除方法为挂起函数
  3. 修改 ViewModel 中的删除方法为挂起函数
  4. 在 Compose 中调用时,确保使用协程作用域

按照 DeepSeek 给出的思路一步步排查,终于发现问题所在:

ViewModel 中用于删除记录的删除函数 viewModel.delete(item) 没有使用 suspend 进行修饰来将该函数设置为挂起函数,而是作为普通函数使用 viewModelScope.launch 将数据库异步删除操作给包裹了起来,导致协程不同步

也就是说,在 scope.launch 这个异步操作内, viewModel.delete(item) 对于 viewModel.allItem.isNotEmpty() 也是一个异步操作而不是同步操作,导致还未完成删除就进入判断环节。

# 解决方法

viewModel.delete(item) 函数添加 suspend 修饰符并直接调用 Room 的异步删除操作,统一由最外围的 rememberCoroutineScope(scope.launch)来控制协程,使得协程同步。

# 后记

这几天都在学 Jetpack Compose,从传统的 Android XML 结构的 app 转向 Android Jetpack Compose 还是有点难度。以前更新控件直接用 binding 就可以,现在要用回调函数通过状态提升来更新控件,还有 Flow 与 State 之间的处理(头秃)

以前 ViewModel 除了用于延长 UI 属性的生命周期外都不带用的,现在 Jetpack Compose 不用 ViewModel 玩不了一点。不过设计理念还是蛮先进的,不知道是不是过于先进,网上中文资料少得可怜,幸好有 DeepSeek 来帮忙(躺)