skip to Main Content

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


  1. Profile in view model should be State<*>

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

    In composable

    ProfileVModel.profile.value?.let {
       Text(it.username);
    }
    
    Login or Signup to reply.
  2. I recommend using MutableStateFlow.

    a simple sample is described in this Medium article :

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

    Login or Signup to reply.
  3. 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
    
        // ...
    }
    

    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>()
    
        // ...
    }
    

    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() {
            // ...
        }
        // ...
    }
    

    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
        // ...
    }
    

    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)
        }
    }
    

    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);
            }
            // ...
        }
    }
    

    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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search