问题复线
一段只有一个输入框的demo代码:
kotlin
class SomeVM {
val textState = MutableStateFlow("")
fun update(text: String) {
textState.value = text
}
}
@Composable
@Preview
fun App() {
MaterialTheme {
val vm = remember(::SomeVM)
val text by vm.textState.collectAsState()
TextField(text, onValueChange = vm::update)
}
}
运行后,连续快速按1、2两个按键,数次后发生输入错乱。
问题排查
上面示例使用了StateFlow,首先去掉StateFlow,换成Compose内置的State,即
kotlin
class SomeVM {
val textState = mutableStateOf("")
fun update(text: String) {
textState.value = text
}
}
@Composable
@Preview
fun App() {
MaterialTheme {
val vm = remember(::SomeVM)
val text by vm.textState
TextField(text, onValueChange = vm::update)
}
}
问题消失。原生的State没问题,StateFlow被大量使用有问题的概率也不大,因此看将StateFlow转换为State的方法:
原来collectAsState方法的参数context有默认值 EmptyCoroutineContext
,而其对应的 Dispatchers.Default
就是在异步线程在执行了。
所以问题就很明了了,用户输入和其他UI变化在主线程,而StateFlow的监听却到后台线程倒了一下手,主线程取到错误的文本长度导致焦点位置错误就是正常的了。
解决方案
解决的方法就是主动传入下Dispatchers.Main保证状态监听也在主线程就可以了。
kotlin
class SomeVM {
val textState = MutableStateFlow("")
fun update(text: String) {
textState.value = text
}
}
@Composable
@Preview
fun App() {
MaterialTheme {
val vm = remember(::SomeVM)
val text by vm.textState.collectAsState(Dispatchers.Main.immediate)
TextField(text, onValueChange = vm::update)
}
}
当然还要注意,Compose在JVM平台使用的Swing框架,需要添加依赖协程库才能知道哪个线程是主线程。
kotlin
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.8.0")
更多相关
为何在VM中要把状态定义成StateFlow,直接用Compose的State不就啥事情都没有了?
定义成StateFlow可以方便地对数据进行一些操作,且保证VM中不包含任何UI代码,便于测试。
而且上面例子也是比较特殊的,大多数场景,点击XX按钮,不会像输入框这样有这么高的实时性要求。另外我个人认为,这里默认值用EmptyCoroutineContext是不合适的,就监听个状态还能把主线程累死吗。