When I change ViewModel variable, Composable Doesn’t Update the View and I’m not sure what to do.
This is my MainActivity:
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)
}
}
}
}
}
}
I have a a View Model:
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)
}
}
}
}
}
And a Composable View upon user autentication:
@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)
)
}
}
}
}
When my Firestore method returns, I update the profile
var, and "expect" it to be updated in the composable, here:
ProfileVModel.profile?.let {
Text(it.username);
}
However, nothing is changing?
When I was adding firebase functions from inside composable, I could just do:
context.startActivity(Intent(context, MainActivity::class.java))
And it would update the view. However, I’m not quite sure how to do this from inside a ViewModel, since "context" is a Composable-specific feature?
I’ve tried to look up Live Data, but every tutorial is either too confusing or differs from my code. I’m coming from SwiftUI MVVM so when I update something in a ViewModel, any view that’s using the value updates. It doesn’t seem to be the case here, any help is appreciated.
Thank you.
3
Answers
Profile in view model should be State<*>
In composable
I recommend using
MutableStateFlow
.a simple sample is described in this Medium article :
https://farhan-tanvir.medium.com/stateflow-with-jetpack-compose-7d9c9711c286
Part 1: Obtaining a
ViewModel
correctlyOn the marked line below you are setting your view model to a new
ProfileViewModel
instance on every recomposition of yourMenu
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.You can fix this by always obtaining your
ViewModel
s from theViewModelStore
. In that way theViewModel
will have the correct owner (correct lifecycle owner) and thus the correct lifecycle.Compose has a helper for obtaining
ViewModel
s with theviewModel()
call.This is how you would use the call in your code
See also
ViewModels in Compose
that outlines the fundamentals related toViewModel
s in Compose.Part 2: Avoid
GlobalScope
(unless you know exactly why you need it) and watch out for exceptionsAs described in Avoid Global Scope you should avoid using
GlobalScope
whenever possible. AndroidViewModel
s come with their own coroutine scope accessible throughviewModelScope
. You should also watch out for exceptions.Example for your code
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 createMutableState<T>
instances withmutableStateOf<T>(value: T)
, where thevalue
parameter is the value you want to initialize the state with.You could keep the state in your view model like this
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 librariesThis means that you could also use a
MutableStateFlow<T>
to track changes inside theViewModel
and expose it outside your view model as aStateFlow<T>
.And then use
StateFlow<T>.collectAsState()
inside your composable to get theState<T>
that is needed by Compose.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.