Kotlin ViewModel的 StateFlow:正确更新状态变更
这个问题是为了理解在ViewModel(视图模型)中的StateFlow。如何正确地对同一值进行多次更新。因此,出现一个颇为有趣的问题:第一次更新的值发生在调用 authV1 时,但在调用 authV2 或 reset 时却没有更新。如何确保状态被正确复制,以确保state-flow得到更新,并让视图显示最新的值。请注意,视图本身只是显示一个 Column,其中包含一个 Text 来显示状态,还有一些 Button 视图用于更新state-flow。
class AuthViewModel: ViewModel() { // transient value/state
private val _authEntityMutableStateFlow = MutableStateFlow<AuthEntity>(AuthEntity(token = ""))
val authEntityStateFlow = _authEntityMutableStateFlow.asStateFlow()
fun update(data: AuthEntity) {
// required in kotlin because it does not update by value
_authEntityMutableStateFlow.value = _authEntityMutableStateFlow.value.copy(token = data.token) // or authEntityStateFlow?
}
fun authV1() {
viewModelScope.launch {
update(AuthRepo.instance.getTestTokenV1()) // this method uses suspend call withContext
}
}
fun authV2() {
viewModelScope.launch {
AuthRepo.instance.getTestTokenV2() // this method uses return withContext(Dispatchers.IO) { Result.success for testing with fixed value
.onSuccess { authRespEntity ->
update(authRespEntity.data)
}
}
}
fun reset() {
update(AuthEntity(token = ""))
}
}
@Composable
fun AuthScreen(authViewModel: AuthViewModel = AuthViewModel()) {
val authEntityStateFlow = authViewModel.authEntityStateFlow.collectAsStateWithLifecycle()
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("authEntityStateFlow: ${authEntityStateFlow.value}")
Button(onClick = {
authViewModel.authV1()
}) {
Text("authV1")
}
Button(onClick = {
authViewModel.authV2()
}) {
Text("authV2")
}
Button(onClick = {
authViewModel.reset()
}) {
Text("reset")
}
}
}
以下是代码中使用的模型类:
import kotlinx.serialization.Serializable
@Serializable
data class AuthRespEntity(val data: AuthEntity)
@Serializable
data class AuthEntity(val token: String)
解决方案
这与Flow无关,这个问题源自对ViewModel的实例化不当。
你在你的可组合函数(Composable)中这样使用:
authViewModel: AuthViewModel = AuthViewModel()
这会在可组合函数每次重新组合时创建一个新的ViewModel实例。重新组合不能被直接控制,但至少在 authEntityStateFlow 的值每次改变时就会发生。因此,每当该值改变时,都会创建一个新的ViewModel实例,带有一个新的Flow,而这个新的Flow初始时包含空字符串 ""。这就是为什么每次按钮点击都会导致文本被清空。
要修复这个问题,你需要让ViewModel实例在重新组合时保持存活。你希望每次都使用同一个ViewModel实例。为实现这一点,你需要使用一个工厂函数,在某处保存ViewModel实例,以便在每次后续调用时都能返回同一个实例。幸运的是,库 androidx.lifecycle:lifecycle-viewmodel-compose 已经提供了一个方便的辅助函数来实现这一点:
authViewModel: AuthViewModel = viewModel()
如果你在使用依赖注入框架Hilt或 Koin,你必须改用 hiltViewModel() 或 koinViewModel()。
顺便提一句,如果你需要MutableStateFlow的“旧”值来计算“新”值,不应该使用 value 属性,因为它并非线程安全。请改用 update 函数:
_authEntityMutableStateFlow.update { it.copy(token = data.token) }