当我更改 ViewModel var 时,Kotlin + Compose 中的 Composable 不会更新

Her*_*. F 7 android kotlin firebase android-jetpack-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)
                    }
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我有一个视图模型:

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)
                    }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

以及用户身份验证时的可组合视图:

@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)
                )
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

当我的 Firestore 方法返回时,我更新profilevar,并“期望”它在可组合项中更新,如下所示:

    ProfileVModel.profile?.let {
        Text(it.username);
    }
Run Code Online (Sandbox Code Playgroud)

然而,什么都没有改变吗?

当我从可组合内部添加 firebase 函数时,我可以这样做:

context.startActivity(Intent(context, MainActivity::class.java))
Run Code Online (Sandbox Code Playgroud)

它会更新视图。但是,我不太确定如何从 ViewModel 内部执行此操作,因为“上下文”是可组合特定的功能?

我尝试查找实时数据,但每个教程要么太混乱,要么与我的代码不同。我来自 SwiftUI MVVM,因此当我更新 ViewModel 中的某些内容时,任何使用该值的视图都会更新。这里的情况似乎并非如此,感谢任何帮助。

谢谢。

Ma3*_*a3x 7

Part 1: Obtaining a ViewModel correctly

On the marked line below you are setting your view model to a new ProfileViewModel instance on every recomposition of your Menu composable, which means your view model (and any state tracked by it) will reset on every recomposition. That prevents your view model to act as a view state holder.

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

    // ...
}
Run Code Online (Sandbox Code Playgroud)

You can fix this by always obtaining your ViewModels from the ViewModelStore. In that way the ViewModel will have the correct owner (correct lifecycle owner) and thus the correct lifecycle. Compose has a helper for obtaining ViewModels with the viewModel() call.

This is how you would use the call in your code

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

    // ...
}
Run Code Online (Sandbox Code Playgroud)

See also ViewModels in Compose that outlines the fundamentals related to ViewModels in Compose.

Note: if you are using a DI (dependency injection) library (such as Hilt, Koin...) then you would use the helpers provided by the DI library to obtain ViewModels.

Part 2: Avoid GlobalScope (unless you know exactly why you need it) and watch out for exceptions

As described in Avoid Global Scope you should avoid using GlobalScope whenever possible. Android ViewModels come with their own coroutine scope accessible through viewModelScope. You should also watch out for exceptions.

Example for your code

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() {
        // ...
    }
    // ...
}
Run Code Online (Sandbox Code Playgroud)

More coroutine best practices are covered in Best practices for coroutines in Android.

Part 3: Managing state in Compose

Compose tracks state through State<T>. If you want to manage state you can create MutableState<T> instances with mutableStateOf<T>(value: T), where the value parameter is the value you want to initialize the state with.

You could keep the state in your view model like this

// 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
    // ...
}
Run Code Online (Sandbox Code Playgroud)

then every time you would change the profile variable, composables that use it in some way (i.e. read it) would recompose.

However, if you don't want your view model ProfileViewModel to depend on the Compose runtime then there are other options to track state changes while not depending on the Compose runtime. From the documentation section Compose and other libraries

Compose comes with extensions for Android's most popular stream-based solutions. Each of these extensions is provided by a different artifact:

  • Flow.collectAsState() doesn't require extra dependencies. (because it is part of kotlinx-coroutines-core)

  • LiveData.observeAsState() included in the androidx.compose.runtime:runtime-livedata:$composeVersion artifact.

  • Observable.subscribeAsState() included in the androidx.compose.runtime:runtime-rxjava2:$composeVersion or > androidx.compose.runtime:runtime-rxjava3:$composeVersion artifact.

These artifacts register as a listener and represent the values as a State. Whenever a new value is emitted, Compose recomposes those parts of the UI where that state.value is used.

This means that you could also use a MutableStateFlow<T> to track changes inside the ViewModel and expose it outside your view model as a 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

And then use StateFlow<T>.collectAsState() inside your composable to get the State<T> that is needed by Compose.

A general Flow<T> can also be collected as State<T> with Flow<T : R>.collectAsState(initial: R), where the initial value has to be provided.

@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);
        }
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

To learn more about working with state in Compose see the documentation section on Managing State. This is fundamental information to be able to work with state in Compose and trigger recompositions efficiently. It also covers the fundamentals of state hoisting. If you prefer a coding tutorial here is the code lab for State in Jetpack Compose.

An introduction to handling the state as the complexity increases is in the video from Google about Using Jetpack Compose's automatic state observation.


小智 2

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

private val _viewState: MutableState<Profile?> = mutableStateOf(null)
val viewState: State<Profile?> = _viewState
Run Code Online (Sandbox Code Playgroud)

可组合的

ProfileVModel.profile.value?.let {
   Text(it.username);
}
Run Code Online (Sandbox Code Playgroud)