Search code examples
viewmodelandroid-jetpack-compose

Jetpack Compose MutableState not updating on the screen


I want to show a movie list from Firebase realtime database, but I'm not getting the list from the ViewModel. When I update the apiResource.data in the Repository, it is not updating the mutableList on the screen.

Screen:

           val movies = viewModel.data.value.data?.toMutableList()

           if (viewModel.data.value.loading == true) {
               CircularProgressIndicator()
           } else {
              
               movies?.forEach {
                   Log.d("Movies", it.name)  // I'm not getting the movie list on the screen
               }

           }

ViewModel:

val data: MutableState<ApiResource<List<Movie>, Boolean, Exception>> = mutableStateOf(
        ApiResource(null, true, Exception(""))
    )

    private fun getMovies() {

        viewModelScope.launch {
            data.value.loading = true
            data.value = repository.getAllMovies()

            if (data.value.data.toString() != "[]") {
                data.value.loading = false
            }
        }
    }

Repository:

class Repository {
    private val apiResource = ApiResource<List<Movie>,Boolean, Exception>()

    suspend fun getAllMovies(): ApiResource<List<Movie>, Boolean, java.lang.Exception> {

        try {
            apiResource.loading = true

            var movies: List<Movie>

            Firebase.database.reference.child("movies").addValueEventListener(object : ValueEventListener {
                override fun onDataChange(dataSnapshot: DataSnapshot) {
                    movies = dataSnapshot.children.map { snapshot ->
                        snapshot.getValue(Movie::class.java)!!
                    }

                    apiResource.data = movies  // set data in apiResource
                    if (apiResource.data.toString() != "[]") {
                        apiResource.loading = false
                    }

                }

                override fun onCancelled(error: DatabaseError) {}
            })
        } catch (exception: java.lang.Exception) {
            apiResource.e = exception
        }
        
        return apiResource
    }
}

Solution

  • Compose cannot track changes of an object contained by a mutable state. It can only track updates to the value of the mutable state.

    Possible solutions:

    1. Updating your mutable state by creating a copy of the object with the updated properties. Data class is extremely handy in this case:

      data class ApiResource(val isLoading: Boolean = false)
      
      class VM : ViewModel() {
          var apiResource by mutableStateOf(ApiResource())
      
          fun update() {
              apiResource = apiResource.copy(isLoading = true)
          }
      }
      
    2. Making the necessary properties of ApiResource mutable states.

      class ApiResource {
          var isLoading by mutableStateOf(false)
      }
      
      class VM: ViewModel() {
          val apiResource = ApiResource()
      
          fun update() {
              apiResource.isLoading = true
          }
      }
      

    The second problem with your code, is that you're messing coroutines with asynchronous code. addValueEventListener is not gonna wait until onDataChange is called, so getAllMovies always returns an empty object.

    To make a coroutine wait until an async call finishes, you need to use suspendCoroutine.

    Also, listener provided to addValueEventListener can be called multiple times if result changes. Your current code doesn't expect that, I suggest you using addListenerForSingleValueEvent instead.

    val movies = suspendCoroutine<List<Movie>> { continuation ->
        Firebase.database.reference.child("movies").addListenerForSingleValueEvent(object : ValueEventListener {
            override fun onDataChange(dataSnapshot: DataSnapshot) {
    
                continuation(
                    dataSnapshot.children.map { snapshot ->
                        snapshot.getValue(Movie::class.java)!!
                    }
                )
            }
    
            override fun onCancelled(error: DatabaseError) {}
        })
    }