I have a movie app built using jetpack compose. In it I am showing the favorite movies selected by the user. The favorites should exist in the UI perfectly, but when I delete an item from the UI, the UI does not immediately reorganize to reflect the change. what to do?
This is my user interface
@composable fun favscreen( movieviewmodel: movieviewmodel ) { remember { mutablestateof(movieviewmodel.getfavmovies()) } val shouldshowdialog = remember { mutablestateof(false) } val uistates = movieviewmodel.favs.collectasstate() column { row { text( text = "fav movies", fontsize = 25.sp, fontfamily = fontfamily(font(r.font.nsb)), modifier = modifier.padding(20.dp).weight(1f) ) iconbutton(onclick = { shouldshowdialog.value = true }) { icon(icons.filled.delete, contentdescription = "") } } when(val currentstate = uistates.value){ is movieviewmodel.uistates.loading -> { box(modifier = modifier.fillmaxsize(), contentalignment = alignment.center) { circularprogressindicator() } } is movieviewmodel.uistates.favs -> { val data = currentstate.data if(data.isempty()){ box(modifier = modifier.fillmaxsize(), contentalignment = alignment.center) { text( text = "no fav movies", fontsize = 25.sp, fontfamily = fontfamily(font(r.font.nsb)), modifier = modifier.padding(20.dp)) } } else { lazycolumn(){ items(data){ fav -> row(modifier = modifier .padding(20.dp) .fillmaxwidth() .clip(roundedcornershape(16.dp)) .background(color = color.darkgray)) { asyncimage( model = utils.image_url + fav.imageurl, contentdescription = "", contentscale = contentscale.crop, filterquality = filterquality.high, modifier = modifier .width(150.dp) .height(150.dp)) column(modifier = modifier.padding(20.dp)) { text( text = fav.title!!, fontsize = 20.sp, fontfamily = fontfamily(font(r.font.nsb))) text( text = fav.rating.tostring() + "/10 imdb", fontsize = 15.sp, fontfamily = fontfamily(font(r.font.nsb))) text( text = "movie id : #" + fav.movieid, fontsize = 15.sp, fontfamily = fontfamily(font(r.font.nsb))) } } } } } } is movieviewmodel.uistates.error -> { box(modifier = modifier.fillmaxsize(), contentalignment = alignment.center){ text( text = "no fav movies", fontsize = 25.sp, fontfamily = fontfamily(font(r.font.nsb)), modifier = modifier.padding(20.dp)) } } else -> {} } } if (shouldshowdialog.value){ alertdialog( ondismissrequest = { shouldshowdialog.value = false }, confirmbutton = { textbutton(onclick = { movieviewmodel.deletemovies() shouldshowdialog.value = false }) { text(text = "proceed", fontfamily = fontfamily(font(r.font.nsm))) } }, dismissbutton = { textbutton(onclick = { shouldshowdialog.value = false }) { text(text = "cancel", fontfamily = fontfamily(font(r.font.nsm))) } }, shape = roundedcornershape(16.dp), text = { text(text = "favs deletion",fontfamily = fontfamily(font(r.font.nsb)))}, title = { text(text = "do you want to delete all fav movies ?", fontfamily = fontfamily(font(r.font.nsb)))} ) } }
This is my view model
@HiltViewModel class MovieViewModel @Inject constructor( private val movieRepoImpl: MovieRepoImpl ) : ViewModel() { private val _favs : MutableStateFlow<UiStates> = MutableStateFlow(UiStates.INITIAL) val favs get() = _favs.asStateFlow() // MOVIE DAO fun getFavMovies() = viewModelScope.launch { try { _favs.value = UiStates.LOADING movieRepoImpl.getFavs().collectLatest { _favs.value = UiStates.FAVS(it) } } catch (ex : Exception){ _favs.value = UiStates.ERROR(ex.localizedMessage!!) } } fun insertMovie(favModel: FavModel) = viewModelScope.launch { movieRepoImpl.insertFav(favModel) } fun deleteMovies() = viewModelScope.launch { movieRepoImpl.deleteAllMovies() } sealed class UiStates { object LOADING : UiStates() data class FAVS(val data : MutableList<FavModel>) : UiStates() data class ERROR(val error : String) : UiStates() object INITIAL : UiStates() } }
The problem becomes very obvious when you look at the written usage of _favs
:
Only getfavmovies
will update it.
While this sounds like "you should update your UI state in the delete function", I'd like to propose an alternative solution:
Harness the power of room
flows
!
First define loadingstate
in the viewmodel, which will be stored as a stream:
sealed class loadingstate { object loading : loadingstate() data class success : loadingstate() data class error(val error : string) : loadingstate() object initial : loadingstate() } private val loadingstate = mutablestateflow<loadingstate>(initial)
Then, change the getfavmovies()
function to use this mechanism:
fun getfavmovies() { viewmodelscope.launch { try { _loadingstate.value = loadingstate.loading movierepoimpl.loadfavs() // a function which triggers e.g. a network call to actually load your movies _loadingstate.value = loadingstate.success } catch (ex: exception) { _loadingstate.value = loadingstate.error(ex.localizedmessage!!) } } }
Finally, combine your status and data to make magic happen. This will automatically update once one of them changes :)
val favs: flow<uistates> = combine( movierepoimpl.getmovies(), loadingstate ) { movies, loadingstate -> when (loadingstate) { is loadingstate.loading -> uistates.loading is loadingstate.success -> uistates.success(movies) is loadingstate.error -> uistates.error(error) is loadingstate. initial -> uistates.initial } }
Disclaimer: There may be spelling errors in this, I don't have a handy ide
Let's finish this answer by improving the code:
Avoid using =
with functions that return nothing (getfavmovies
, insertmovie
, deletemovies
). Otherwise, you would expect e.g. getfavmovies
to return a list of movies
remember { mutablestateof(movieviewmodel.getfavmovies()) }
doesn't really remember
anything, does it? This is so annoying. I suggest moving it into the viewmodel: init { movieviewmodel.getfavmovies() }
val uistates = movieviewmodel.favs.collectasstate()
: The naming means this is multiple ui states, please use the singular; uistates
The same goes for the class itself. In addition, if ".value" is used directly here, you can skip the redeclaration here: when(val currentstate = uistates.value)
Your uistate contains mutablelist<favmodel>
. Using mutable values for ui state is a bad idea, for example it may mess up your stateflow
. Be sure to avoid this situation. Just use the plain old list :) https://www.php.cn/link/ff685590317f1330efc73f396ac92cd7
Use collectasstatewithlifecycle()
instead of collectasstate()
. See https://medium.com/androiddevelopers/consuming-flows -safely in jetpack-compose-cde014d0d5a3
You should definitely not access it in the viewmodel repoimpl
: The whole idea of splitting the repository into interfaces and implementations is to hide the implementation
It is a good idea to define a screen-composable, it does not require a viewmodel, only data, similar to https://developer.android.com/jetpack/compose/state#state-hoisting:
.
@Composable private fun FavScreen( val viewModel: MovieViewModel, ) { FawScreen( uiState = viewModel.fav.collectAsStateWithLifecycle().value, shouldShowDialog = viewModel.shouldShowDialog.collectAsStateWithLifecycle().value ) } @Composable private fun FavScreen( val uiState: UiStates, val shouldShowDialog: Boolean ) { // your current ui }
The above is the detailed content of UI not reassembled after deleting all items in jetpack using room database. For more information, please follow other related articles on the PHP Chinese website!