当我更改 ViewModel var 时,可组合在 Kotlin + Compose 中不会更新



当我更改ViewModel变量时,Composable不会更新视图,我不知道该怎么办。

这是我的主要活动:

class MainActivity : ComponentActivity() {
companion object  {
val TAG: String = MainActivity::class.java.simpleName
}
private val auth by lazy {
Firebase.auth
}
var isAuthorised: MutableState<Boolean> = mutableStateOf(FirebaseAuth.getInstance().currentUser != null)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val user = FirebaseAuth.getInstance().currentUser
setContent {
HeroTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
if (user != null) {
Menu(user)
} else {
AuthTools(auth, isAuthorised)
}
}
}
}
}
}

我有一个视图模型:

class ProfileViewModel: ViewModel() {
val firestore = FirebaseFirestore.getInstance()
var profile: Profile? = null
val user = Firebase.auth.currentUser
init {
fetchProfile()
}
fun fetchProfile() {
GlobalScope.async {
getProfile()
}
}
suspend fun getProfile() {
user?.let {
val docRef = firestore.collection("Profiles")
.document(user.uid)
return suspendCoroutine { continuation ->
docRef.get()
.addOnSuccessListener { document ->
if (document != null) {
this.profile = getProfileFromDoc(document)
}
}
.addOnFailureListener { exception ->
continuation.resumeWithException(exception)
}
}
}
}
}

以及用户授权的可组合视图:

@Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel = ProfileViewModel()
Column(
modifier = Modifier
.background(color = Color.White)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text("Signed in!");

ProfileVModel.profile?.let {
Text(it.username);
}
Row(
horizontalArrangement =  Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
TextButton(onClick = {
FirebaseAuth.getInstance().signOut()
context.startActivity(Intent(context, MainActivity::class.java))
}) {
Text(
color = Color.Black,
text = "Sign out?",
modifier = Modifier.padding(all = 8.dp)
)
}
}
}
}

当我的Firestore方法返回时,我更新profilevar,并且";期望";它将在可组合中更新,这里:

ProfileVModel.profile?.let {
Text(it.username);
}

然而,什么都没有改变?

当我从compositable内部添加firebase函数时,我可以做:

context.startActivity(Intent(context, MainActivity::class.java))

它会更新视图。然而,我不太确定如何从ViewModel内部做到这一点,因为";上下文";是可组合的特定功能吗?

我试着查找Live Data,但每个教程要么太令人困惑,要么与我的代码不同。我来自SwiftUI MVVM,所以当我更新ViewModel中的某些内容时,任何使用该值的视图都会更新。这里的情况似乎并非如此,我们非常感谢您的帮助。

谢谢。

第1部分:正确获取ViewModel

在下面的标记线上,您将在每次重新组合Menu可组合时将视图模型设置为新的ProfileViewModel实例,这意味着每次重新组合时都会重置视图模型(以及它跟踪的任何状态)。这将阻止视图模型充当视图状态持有者。

@Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel = ProfileViewModel() // <-- view model resets on every recomposition
// ...
}

您可以通过始终从ViewModelStore获取ViewModel来解决此问题。通过这种方式,ViewModel将具有正确的所有者(正确的生命周期所有者),从而具有正确的生命期。Compose有一个助手,用于通过viewModel()调用获取ViewModel

这就是您在代码中使用呼叫的方式

@Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel: ProfileViewModel = viewModel()
// or this way, if you prefer
// val ProfileVModel = viewModel<ProfileViewModel>()
// ...
}

另请参阅ViewModels in Compose,它概述了Compose中与ViewModel相关的基本原理。

注意:如果您使用DI(依赖注入)库(如Hilt、Koin…),则您将使用DI库提供的助手来获得ViewModels。

第2部分:避免GlobalScope(除非您确切知道为什么需要它)并注意异常

如避免全局作用域中所述,应尽可能避免使用GlobalScope。安卓ViewModel自带可通过viewModelScope访问的协同程序范围。您还应该注意例外情况。

代码示例

class ProfileViewModel: ViewModel() {
// ...
fun fetchProfile() {
// Use .launch instead of .async if you are not using
// the returned Deferred result anyway
viewModelScope.launch {
// handle exceptions
try {
getProfile()
} catch (error: Throwable) {
// TODO: Log the failed attempt and/or notify the user
}
}
}
// make it private, in most cases you want to expose
// non-suspending functions from VMs that then call other
// suspend factions inside the viewModelScope like fetchProfile does
private suspend fun getProfile() {
// ...
}
// ...
}

Android协同程序的最佳实践中涵盖了更多的协同程序最佳实践。

第3部分:在Compose中管理状态

Compose通过State<T>跟踪状态。如果要管理状态,可以使用mutableStateOf<T>(value: T)创建MutableState<T>实例,其中value参数是要初始化状态的值。

你可以像这个一样在你的视图模型中保持状态

// This VM now depends on androidx.compose.runtime.*
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
class ProfileViewModel: ViewModel() {
var profile: Profile? by mutableStateOf(null)
private set
// ...
}

那么每次更改profile变量时,以某种方式使用它(即读取它)的可组合程序都会重新组合。

但是,如果您不希望视图模型ProfileViewModel依赖于Compose运行时,那么还有其他选项可以跟踪状态更改,而不依赖于Composer运行时。来自文档部分Compose和其他库

Compose为Android最流行的基于流的应用程序提供了扩展解决方案。这些扩展中的每一个都由不同的工件:

  • Flow.collectAsState()不需要额外的依赖项。(因为它是kotlinx-coroutines-core的一部分)

  • 包括在CCD_ 26伪影中的CCD_。

  • CCD_ 28中包含的CCD_>CCD_ 29伪影。

这些工件注册为侦听器,并将值表示为CCD_ 30。每当发出新值时,Compose都会重新组合这些零件其中CCD_ 31被使用。

这意味着您还可以使用MutableStateFlow<T>来跟踪ViewModel内部的更改,并将其作为StateFlow<T>暴露在视图模型外部。

// This VM does not depend on androidx.compose.runtime.* anymore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class ProfileViewModel : ViewModel() {
private val _profileFlow = MutableStateFlow<Profile?>(null)
val profileFlow = _profileFlow.asStateFlow()
private suspend fun getProfile() {
_profileFlow.value = getProfileFromDoc(document)
}
}

然后在compeable中使用StateFlow<T>.collectAsState()来获得Compose所需的State<T>

一般的Flow<T>也可以与Flow<T : R>.collectAsState(initial: R)一起收集为State<T>,其中必须提供初始值。

@Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel: ProfileViewModel = viewModel()
val profile by ProfileVModel.profileFlow.collectAsState()
Column(
// ...
) {
// ...
profile?.let {
Text(it.username);
}
// ...
}
}

要了解有关在Compose中使用状态的更多信息,请参阅有关管理状态的文档部分。这是能够有效地处理Compose中的状态并触发重新计算的基本信息。它还涵盖了状态提升的基本原理。如果你更喜欢编码教程,这里是Jetpack Compose中State的代码实验室。

在谷歌关于使用Jetpack Compose的自动状态观察的视频中,介绍了如何随着复杂性的增加来处理状态。

视图模型中的配置文件应为State<>

private val _viewState: MutableState<Profile?> = mutableStateOf(null)
val viewState: State<Profile?> = _viewState

在可组合中

ProfileVModel.profile.value?.let {
Text(it.username);
}

我建议使用MutableStateFlow

在这篇Medium文章中描述了一个简单的示例:

https://farhan-tanvir.medium.com/stateflow-with-jetpack-compose-7d9c9711c286

最新更新