Can I use observerAsState() on a MutableLiveData?

I have ran into an issue while building a small movie library application. My structure is as follows: I have a MovieSearchScreen function which calls a SearchBar function as well as a MovieSearchResults function. In the Searchbar function, I have a TextField, a leadingIcon and a trailingIcon which has a clickable modifier which calls my updateMovieResource function which is in my viewModel:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code> @Composable
fun Create(navController: NavHostController, viewModel: MovieViewModel) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.DarkGray)
) {
SearchBar(viewModel = viewModel)
MovieSearchResults(navController = navController)
}
}
</code>
<code> @Composable fun Create(navController: NavHostController, viewModel: MovieViewModel) { Column( modifier = Modifier .fillMaxSize() .background(color = Color.DarkGray) ) { SearchBar(viewModel = viewModel) MovieSearchResults(navController = navController) } } </code>
    @Composable
    fun Create(navController: NavHostController, viewModel: MovieViewModel) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .background(color = Color.DarkGray)
        ) {
            SearchBar(viewModel = viewModel)
            MovieSearchResults(navController = navController)
        }
    }

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code> @Composable
private fun SearchBar(viewModel: MovieViewModel) {
var userInput: String by remember {
mutableStateOf("")
}
TextField(
value = userInput,
onValueChange = {
userInput = it
},
modifier = Modifier
.fillMaxWidth(),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Gray,
unfocusedContainerColor = Color.Gray,
focusedTextColor = Color.White,
unfocusedTextColor = Color.White
),
placeholder = {
Text(
text = stringResource(id = R.string.search_bar),
color = Color.White
)
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Clear,
contentDescription = "Clear",
modifier = Modifier
.clickable {
userInput = ""
},
tint = Color.White
)
},
trailingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = "Search",
tint = Color.White,
modifier = Modifier.clickable {
if (userInput.isNotBlank()) {
viewModel.updateMovieResource(userInput)
}
}
)
}
)
}
</code>
<code> @Composable private fun SearchBar(viewModel: MovieViewModel) { var userInput: String by remember { mutableStateOf("") } TextField( value = userInput, onValueChange = { userInput = it }, modifier = Modifier .fillMaxWidth(), colors = TextFieldDefaults.colors( focusedContainerColor = Color.Gray, unfocusedContainerColor = Color.Gray, focusedTextColor = Color.White, unfocusedTextColor = Color.White ), placeholder = { Text( text = stringResource(id = R.string.search_bar), color = Color.White ) }, leadingIcon = { Icon( imageVector = Icons.Rounded.Clear, contentDescription = "Clear", modifier = Modifier .clickable { userInput = "" }, tint = Color.White ) }, trailingIcon = { Icon( imageVector = Icons.Rounded.Search, contentDescription = "Search", tint = Color.White, modifier = Modifier.clickable { if (userInput.isNotBlank()) { viewModel.updateMovieResource(userInput) } } ) } ) } </code>
    @Composable
    private fun SearchBar(viewModel: MovieViewModel) {
        var userInput: String by remember {
            mutableStateOf("")
        }
        TextField(
            value = userInput,
            onValueChange = {
                userInput = it
                            },
            modifier = Modifier
                .fillMaxWidth(),
            colors = TextFieldDefaults.colors(
                focusedContainerColor = Color.Gray,
                unfocusedContainerColor = Color.Gray,
                focusedTextColor = Color.White,
                unfocusedTextColor = Color.White
            ),
            placeholder = {
                Text(
                    text = stringResource(id = R.string.search_bar),
                    color = Color.White
                )
                          },
            leadingIcon = {
                Icon(
                    imageVector = Icons.Rounded.Clear,
                    contentDescription = "Clear",
                    modifier = Modifier
                        .clickable {
                            userInput = ""
                                   },
                    tint = Color.White
                )
                          },
            trailingIcon = {
                Icon(
                    imageVector = Icons.Rounded.Search,
                    contentDescription = "Search",
                    tint = Color.White,
                    modifier = Modifier.clickable {
                        if (userInput.isNotBlank()) {
                            viewModel.updateMovieResource(userInput)
                        }
                    }
                )
            }
        )
    }

In this viewModel, I have a couple of values, the important ones for now are my _movieRepository from where I manage my api calls, _movieResource, movieResource and the updateMovieResource method:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>class MovieViewModel: ViewModel() {
private lateinit var _selectedMovie: MutableState<Movie>
var selectedMovie: Movie
get() = _selectedMovie.value
set(movie){
_selectedMovie.value = movie
}
private val _movieRepository: MovieRepository = MovieRepository()
val movieResource: LiveData<Resource<ResponseResult>>
get() = _movieResource
private val _movieResource: MutableLiveData<Resource<ResponseResult>> =
MutableLiveData(Resource.Empty())
fun updateMovieResource(movie: String) {
_movieResource.value = Resource.Loading()
viewModelScope.launch {
_movieResource.value = _movieRepository.getMovie(movie)
}
}
}
</code>
<code>class MovieViewModel: ViewModel() { private lateinit var _selectedMovie: MutableState<Movie> var selectedMovie: Movie get() = _selectedMovie.value set(movie){ _selectedMovie.value = movie } private val _movieRepository: MovieRepository = MovieRepository() val movieResource: LiveData<Resource<ResponseResult>> get() = _movieResource private val _movieResource: MutableLiveData<Resource<ResponseResult>> = MutableLiveData(Resource.Empty()) fun updateMovieResource(movie: String) { _movieResource.value = Resource.Loading() viewModelScope.launch { _movieResource.value = _movieRepository.getMovie(movie) } } } </code>
class MovieViewModel: ViewModel() {

    private lateinit var _selectedMovie: MutableState<Movie>

    var selectedMovie: Movie
        get() = _selectedMovie.value
        set(movie){
            _selectedMovie.value = movie
        }

    private val _movieRepository: MovieRepository = MovieRepository()

    val movieResource: LiveData<Resource<ResponseResult>>
        get() = _movieResource

    private val _movieResource: MutableLiveData<Resource<ResponseResult>> =
        MutableLiveData(Resource.Empty())

    fun updateMovieResource(movie: String) {
        _movieResource.value = Resource.Loading()

        viewModelScope.launch {
            _movieResource.value = _movieRepository.getMovie(movie)
        }
    }
}

I use the movieResource to figure out wether my api call was succesful, still loading or returned an error so I know what to display. I use this movieResource in my MovieSearchResults composable here:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>@Composable
private fun MovieSearchResults(
navController: NavHostController,
viewModel: MovieViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
){
val movieResource: Resource<ResponseResult>? by viewModel.movieResource.observeAsState()
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
when(movieResource) {
is Resource.Success ->
LazyVerticalGrid(
columns = GridCells.Fixed(3)
) {
items(
items = (movieResource as Resource.Success<ResponseResult>).data!!.results,
itemContent = {
AsyncImage(
model = it.backdropPath,
contentDescription = it.title,
modifier = Modifier.clickable {
viewModel.selectedMovie = it
navController.navigate(MovieLibraryScreens.MovieDetailsScreen.name)
}
)
}
)
}
is Resource.Error ->
Text(
text = (movieResource as Resource.Error<ResponseResult>).message!!,
fontSize = 30.sp,
color = Color.Red
)
is Resource.Empty ->
Text(
text = "Search for a movie!",
fontSize = 30.sp,
color = Color.White
)
is Resource.Loading ->
Text(
text = "Loading...",
fontSize = 30.sp,
color = Color.White
)
else ->
Text(
text = "Something went wrong...",
fontSize = 30.sp,
color = Color.White
)
}
}
}
</code>
<code>@Composable private fun MovieSearchResults( navController: NavHostController, viewModel: MovieViewModel = androidx.lifecycle.viewmodel.compose.viewModel() ){ val movieResource: Resource<ResponseResult>? by viewModel.movieResource.observeAsState() Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { when(movieResource) { is Resource.Success -> LazyVerticalGrid( columns = GridCells.Fixed(3) ) { items( items = (movieResource as Resource.Success<ResponseResult>).data!!.results, itemContent = { AsyncImage( model = it.backdropPath, contentDescription = it.title, modifier = Modifier.clickable { viewModel.selectedMovie = it navController.navigate(MovieLibraryScreens.MovieDetailsScreen.name) } ) } ) } is Resource.Error -> Text( text = (movieResource as Resource.Error<ResponseResult>).message!!, fontSize = 30.sp, color = Color.Red ) is Resource.Empty -> Text( text = "Search for a movie!", fontSize = 30.sp, color = Color.White ) is Resource.Loading -> Text( text = "Loading...", fontSize = 30.sp, color = Color.White ) else -> Text( text = "Something went wrong...", fontSize = 30.sp, color = Color.White ) } } } </code>
@Composable
    private fun MovieSearchResults(
        navController: NavHostController,
        viewModel: MovieViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
    ){
        val movieResource: Resource<ResponseResult>? by viewModel.movieResource.observeAsState()
        
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            when(movieResource) {
                is Resource.Success ->
                    LazyVerticalGrid(
                        columns = GridCells.Fixed(3)
                    ) {
                        items(
                            items = (movieResource as Resource.Success<ResponseResult>).data!!.results,
                            itemContent = {
                                AsyncImage(
                                    model = it.backdropPath,
                                    contentDescription = it.title,
                                    modifier = Modifier.clickable {
                                        viewModel.selectedMovie = it
                                        navController.navigate(MovieLibraryScreens.MovieDetailsScreen.name)
                                    }
                                )
                            }
                        )
                    }
                is Resource.Error ->
                    Text(
                        text = (movieResource as Resource.Error<ResponseResult>).message!!,
                        fontSize = 30.sp,
                        color = Color.Red
                    )
                is Resource.Empty ->
                    Text(
                        text = "Search for a movie!",
                        fontSize = 30.sp,
                        color = Color.White
                    )
                is Resource.Loading ->
                    Text(
                        text = "Loading...",
                        fontSize = 30.sp,
                        color = Color.White
                    )
                else ->
                    Text(
                        text = "Something went wrong...",
                        fontSize = 30.sp,
                        color = Color.White
                    )
            }
        }
    }

I retrieve this data from my repository here:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>class MovieRepository {
private val _apiService: ApiService = Api().createApi()
private val _apiKey: String = API_KEY
suspend fun getMovie(movie: String): Resource<ResponseResult> {
val response = try {
withTimeout(5_000) {
_apiService.getMovie(movie, _apiKey)
}
} catch (e: Exception) {
Log.e("MovieRepository", e.message ?: "No exception message available")
return Resource.Error("An unknown error occurred while fetching data for the movie: $movie")
}
Log.d("Result", response.results.toString())
return Resource.Success(response)
}
}
</code>
<code>class MovieRepository { private val _apiService: ApiService = Api().createApi() private val _apiKey: String = API_KEY suspend fun getMovie(movie: String): Resource<ResponseResult> { val response = try { withTimeout(5_000) { _apiService.getMovie(movie, _apiKey) } } catch (e: Exception) { Log.e("MovieRepository", e.message ?: "No exception message available") return Resource.Error("An unknown error occurred while fetching data for the movie: $movie") } Log.d("Result", response.results.toString()) return Resource.Success(response) } } </code>
class MovieRepository {

    private val _apiService: ApiService = Api().createApi()

    private val _apiKey: String = API_KEY

    suspend fun getMovie(movie: String): Resource<ResponseResult> {
        val response = try {
            withTimeout(5_000) {
                _apiService.getMovie(movie, _apiKey)
            }
        } catch (e: Exception) {
            Log.e("MovieRepository", e.message ?: "No exception message available")
            return Resource.Error("An unknown error occurred while fetching data for the movie: $movie")
        }
        Log.d("Result", response.results.toString())
        return Resource.Success(response)
    }
}

When I Log.d my response.results, it logs the correct data which I call from my api but when I return my Resource.Success(response). The _movieResource back in my viewModel seems like it is not updating to Resource.Success(response). Furthermore, back in my MovieSearchResults composable, the when statement doesn’t seem to notice that the _movieResource is updated to Resource.Loading() at the start when the updateMovieResource method is called. I am completely stuck with this and feel like I am doing something wrong with the MutableLiveData but I am not completely sure. Please help me!

I have logged both the result which returned the expected api response as well as my movieResource when the updateMovieResource finished running which ended up returning Resource.Empty(). After this I attempted changing the MutableLiveData to standard mutableStateOf() but this is not possible as I have multiple different Resource responses.

1

Although you apparrently fixed the current problem, I want to go more in depth and provide an explanation what the problem was.

But first off, you should update your view model. You still use LiveData (which is a remnant of the pre-Compose era and is now obsolete) and MutableState, which should only be used in composables. Both need to be replaced by Flows:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>class MovieViewModel : ViewModel() {
private val _selectedMovie = MutableStateFlow<Movie?>(null)
val selectedMovie = _selectedMovie.asStateFlow()
private val _movieRepository: MovieRepository = MovieRepository()
private val _movieResource = MutableStateFlow<Resource<ResponseResult>>(Resource.Empty())
val movieResource: StateFlow<Resource<ResponseResult>> = _movieResource.asStateFlow()
fun updateMovieResource(movie: String) {
_movieResource.value = Resource.Loading()
viewModelScope.launch {
_movieResource.value = _movieRepository.getMovie(movie)
}
}
fun selectMovie(movie: Movie) {
_selectedMovie.value = movie
}
}
</code>
<code>class MovieViewModel : ViewModel() { private val _selectedMovie = MutableStateFlow<Movie?>(null) val selectedMovie = _selectedMovie.asStateFlow() private val _movieRepository: MovieRepository = MovieRepository() private val _movieResource = MutableStateFlow<Resource<ResponseResult>>(Resource.Empty()) val movieResource: StateFlow<Resource<ResponseResult>> = _movieResource.asStateFlow() fun updateMovieResource(movie: String) { _movieResource.value = Resource.Loading() viewModelScope.launch { _movieResource.value = _movieRepository.getMovie(movie) } } fun selectMovie(movie: Movie) { _selectedMovie.value = movie } } </code>
class MovieViewModel : ViewModel() {
    private val _selectedMovie = MutableStateFlow<Movie?>(null)
    val selectedMovie = _selectedMovie.asStateFlow()

    private val _movieRepository: MovieRepository = MovieRepository()

    private val _movieResource = MutableStateFlow<Resource<ResponseResult>>(Resource.Empty())
    val movieResource: StateFlow<Resource<ResponseResult>> = _movieResource.asStateFlow()

    fun updateMovieResource(movie: String) {
        _movieResource.value = Resource.Loading()

        viewModelScope.launch {
            _movieResource.value = _movieRepository.getMovie(movie)
        }
    }

    fun selectMovie(movie: Movie) {
        _selectedMovie.value = movie
    }
}

Instead of observeAsState you need to call collectAsStateWithLifecycle() in your composables to convert the flows into State objects.


With this out of the way, back to the initial problem. MovieSearchResults has a viewModel parameter. The culprit here is the default value viewModels(): This creates a new instance of the view model, so there are actually two view models involved: The first is used by SearchBar, the second is used by MovieSearchResults. Only the first one retrieves data from the repository, so the second will always be empty.

To fix that you could pass the same view model instance to MovieSearchResults that you also pass to SearchBar. But actually, you should never pass view models to composables. Instead, retrieve an instance in your top-most composable that needs the view model (seems like that is Create in your case) and pass only the view model’s state and callbacks down to the other composables:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>@Composable
fun Create(navController: NavHostController) {
val viewModel: MovieViewModel = viewModel()
val movieResource: Resource<ResponseResult> by viewModel.movieResource.collectAsStateWithLifecycle()
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.DarkGray),
) {
SearchBar(
updateMovieResource = viewModel::updateMovieResource,
)
MovieSearchResults(
navController = navController,
movieResource = movieResource,
selectMovie = viewModel::selectMovie,
)
}
}
</code>
<code>@Composable fun Create(navController: NavHostController) { val viewModel: MovieViewModel = viewModel() val movieResource: Resource<ResponseResult> by viewModel.movieResource.collectAsStateWithLifecycle() Column( modifier = Modifier .fillMaxSize() .background(color = Color.DarkGray), ) { SearchBar( updateMovieResource = viewModel::updateMovieResource, ) MovieSearchResults( navController = navController, movieResource = movieResource, selectMovie = viewModel::selectMovie, ) } } </code>
@Composable
fun Create(navController: NavHostController) {
    val viewModel: MovieViewModel = viewModel()
    val movieResource: Resource<ResponseResult> by viewModel.movieResource.collectAsStateWithLifecycle()
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.DarkGray),
    ) {
        SearchBar(
            updateMovieResource = viewModel::updateMovieResource,
        )

        MovieSearchResults(
            navController = navController,
            movieResource = movieResource,
            selectMovie = viewModel::selectMovie,
        )
    }
}

With SearchBar and MovieSearchResults declared like this:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>@Composable
private fun SearchBar(
updateMovieResource: (String) -> Unit,
) { /*...*/ }
@Composable
private fun MovieSearchResults(
navController: NavHostController,
movieResource: Resource<ResponseResult>,
selectMovie: (Movie) -> Unit,
) { /*...*/ }
</code>
<code>@Composable private fun SearchBar( updateMovieResource: (String) -> Unit, ) { /*...*/ } @Composable private fun MovieSearchResults( navController: NavHostController, movieResource: Resource<ResponseResult>, selectMovie: (Movie) -> Unit, ) { /*...*/ } </code>
@Composable
private fun SearchBar(
    updateMovieResource: (String) -> Unit,
) { /*...*/ }

@Composable
private fun MovieSearchResults(
    navController: NavHostController,
    movieResource: Resource<ResponseResult>,
    selectMovie: (Movie) -> Unit,
) { /*...*/ }

This way your composables are independent of view models, are more robust to recompositions (because view models are not Stable), are better testable (because you don’t need a view model, you can supply simple dummy parameters), can be more easily reused and can be better previewed (@Preview).

It also fixes the issue you had.

FIXED I have resolved my issue by moving the movieResource val from the MovieSearchResults method to the main Create method and giving it as a parameter to the MovieSearchResults method.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật