In my application I get the list of films by api and can add them to the list of favourites which is stored in the database room. So on the screen with the list of favourite films when I try to delete them which requires redrawing the screen the whole list is updated but not only the deleted item and the application crashes with this error.
“ang.IllegalArgumentException: Key “968031” was already used. If you are using LazyColumn/Row please make sure you provide a unique key for each item.”
@HiltViewModel
class MoviesViewModel @Inject constructor(
private val getMoviesListUseCase: GetMoviesListUseCase,
private val addMovieUseCase: AddMovieUseCase,
private val getMoviesStreamUseCase: GetMoviesStreamUseCase,
private val deleteFromFavoriteUseCase: DeleteFavoriteMovieUseCase,
) : ViewModel() {
private val _allMovies = MutableStateFlow(MoviesListState())
val allMovies: StateFlow<MoviesListState> = _allMovies.asStateFlow()
private val _favMovies = MutableStateFlow(FavoriteMoviesState())
val favMovies: StateFlow<FavoriteMoviesState> = _favMovies.asStateFlow()
private val _searchText = MutableStateFlow("")
val searchText = _searchText.asStateFlow()
private val _originalMovies = MutableStateFlow<List<MovieDetail>>(emptyList())
private val _fMovies = MutableStateFlow<List<MovieInfo>>(emptyList())
var selectedButton by mutableStateOf(BottomBarButton.Favorites)
init {
getMovies()
getFavoriteMovies()
}
private fun getMovies() {
getMoviesListUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_allMovies.value = MoviesListState(movies = result.data)
_originalMovies.value = result.data?.items ?: emptyList()
}
is Resource.Error -> {
_allMovies.value =
MoviesListState(error = result.message ?: "An unexpected error occurred")
}
is Resource.Loading -> {
_allMovies.value = MoviesListState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
private fun getFavoriteMovies() {
viewModelScope.launch {
getMoviesStreamUseCase().filterNotNull().collect { result ->
when (result) {
is Resource.Success -> {
_favMovies.value = FavoriteMoviesState(movies = result.data)
_fMovies.value = result.data ?: emptyList()
}
is Resource.Error -> {
_favMovies.value =
FavoriteMoviesState(error = result.message ?: "An unexpected error occurred")
}
is Resource.Loading -> {
_favMovies.value = FavoriteMoviesState(isLoading = true)
}
}
}
}
}
private suspend fun deleteMovie(movieId: Int) {
deleteFromFavoriteUseCase(movieId)
}
private suspend fun addMovie(movieInfo: MovieInfo) {
addMovieUseCase(movieInfo.toFavoriteMovie())
}
fun toggleFavourite(movieId: Int, movieInfo: MovieInfo) {
Log.d("MoviesViewModel", "Toggling favourite: ${movieId}")
viewModelScope.launch {
val isFavorite = isFavourite(movieId)
if (isFavorite) {
deleteMovie(movieId)
} else {
addMovie(movieInfo)
}
getFavoriteMovies()
}
}
fun onSearchTextChange(text: String, isFav: Boolean) {
_searchText.value = text
if (isFav) {
filterFavoriteMovies(query = text)
} else {
filterPopularMovies(query = text)
}
}
private fun filterPopularMovies(query: String) {
val filteredMovies = if (query.isEmpty()) {
_originalMovies.value
} else {
_originalMovies.value.filter { it.nameRu.contains(query, ignoreCase = true) }
}
_allMovies.value = _allMovies.value.copy(
movies = _allMovies.value.movies?.copy(items = filteredMovies)
)
}
private fun filterFavoriteMovies(query: String) {
val filteredMovies = if (query.isEmpty()) {
_fMovies.value
} else {
_fMovies.value.filter { it.nameOriginal.contains(query, ignoreCase = true) }
}
_favMovies.value = _favMovies.value.copy(
movies = filteredMovies
)
}
fun isFavourite(movieId: Int): Boolean {
return _favMovies.value.movies?.any { it.kinopoiskId == movieId } ?: false
}
}
screens
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FavoriteMoviesScreen(
navController: NavController,
viewModel: MoviesViewModel = hiltViewModel()
) {
val state by viewModel.favMovies.collectAsState()
var isSearching by remember { mutableStateOf(false) }
val searchText by viewModel.searchText.collectAsState()
val selectedButton = viewModel.selectedButton
Scaffold(
topBar = {
TopAppBar(
title = {
TopAppBarMain(
text = stringResource(id = R.string.favorite),
isSearching = isSearching,
searchText = searchText,
onSearchToggle = { isSearching = !isSearching },
onSearchTextChange = { query ->
viewModel.onSearchTextChange(query, true)
},
onClearSearch = {
isSearching = false
viewModel.onSearchTextChange("", true)
},
)
},
actions = {
IconButton(onClick = { isSearching = !isSearching }) {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = stringResource(id = R.string.search),
tint = colorResource(id = R.color.blue)
)
}
}
)
},
bottomBar = {
BottomAppBar(
actions = {
BottomAppBarMain(
modifier = Modifier.weight(1f),
onPopularClick = {
navController.navigate(Screen.MoviesListScreen.route)
},
selectedButton = selectedButton,
onButtonSelected = { button ->
viewModel.selectedButton = button
},
)
},
containerColor = Color.Transparent
)
}
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
when {
state.isLoading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
state.error.isNotEmpty() -> {
ShowError(navController = navController, error = state.error)
}
state.movies.isNullOrEmpty() -> {
if (searchText.isNotBlank()) {
EmptyScreen(
modifier = Modifier.fillMaxSize(),
text = stringResource(id = R.string.empty_search_result)
)
} else {
EmptyScreen(
modifier = Modifier.fillMaxSize(),
text = stringResource(id = R.string.empty_favorite_list)
)
}
}
else -> {
state.movies?.let { movies ->
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(
items = movies,
key = { movie -> movie.kinopoiskId }) { movie ->
FavoriteMoviesList(
movieId = movie.kinopoiskId,
movieName = movie.nameRu,
movieYear = movie.year,
movieGenre = movie.genres[0].genre,
moviePoster = movie.posterUrl,
onMovieClick = {
navController.navigate(Screen.MovieDetailsScreen.route + "/${movie.kinopoiskId}")
},
onMovieLongClick = {
viewModel.toggleFavourite(movie.kinopoiskId, movie)
},
)
}
}
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PopularMoviesScreen(
navController: NavController,
viewModel: MoviesViewModel = hiltViewModel()
) {
val selectedButton = viewModel.selectedButton
val state by viewModel.allMovies.collectAsState()
var isSearching by remember { mutableStateOf(false) }
val searchText by viewModel.searchText.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = {
TopAppBarMain(
text = stringResource(id = R.string.popular),
isSearching = isSearching,
searchText = searchText,
onSearchToggle = { isSearching = !isSearching },
onSearchTextChange = { query ->
viewModel.onSearchTextChange(query, false)
},
onClearSearch = {
isSearching = false
viewModel.onSearchTextChange("", false)
}
)
},
actions = {
if (!isSearching) {
IconButton(onClick = { isSearching = !isSearching }) {
Icon(
imageVector = if (isSearching) Icons.Default.Close else Icons.Filled.Search,
contentDescription = stringResource(id = R.string.search),
tint = colorResource(id = R.color.blue)
)
}
}
}
)
},
bottomBar = {
BottomAppBar(
actions = {
BottomAppBarMain(
modifier = Modifier.weight(1f),
onFavoriteClick = { navController.navigate(Screen.MovieFavoriteScreen.route) },
selectedButton = selectedButton,
onButtonSelected = { button ->
viewModel.selectedButton = button
},
)
},
containerColor = Color.Transparent
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when {
state.isLoading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
state.error.isNotEmpty() -> {
ShowError(navController = navController, error = state.error)
}
state.movies?.items?.isEmpty() == true -> {
if (searchText.isNotBlank()) {
EmptyScreen(
modifier = Modifier.fillMaxSize(),
text = stringResource(id = R.string.empty_search_result)
)
} else {
EmptyScreen(
modifier = Modifier.fillMaxSize(),
text = stringResource(id = R.string.empty_favorite_list)
)
}
}
else -> {
state.movies?.let { mov ->
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items = mov.items, key = { movie -> movie.kinopoiskId }) { movie ->
var isFav by remember(movie.kinopoiskId) {
mutableStateOf(
viewModel.isFavourite(
movie.kinopoiskId
)
)
}
MoviesListItem(
movieId = movie.kinopoiskId,
movieName = movie.nameRu,
movieYear = movie.year,
movieGenre = movie.genres[0].genre,
moviePoster = movie.posterUrlPreview,
isFavorite = isFav,
onMovieClick = {
navController.navigate(Screen.MovieDetailsScreen.route + "/${it}")
},
onMovieLongClick = {
isFav = !isFav
viewModel.toggleFavourite(it, movie.toMovieInfo())
}
)
}
}
}
}
}
}
}
}
If you do not force a screen refresh, the list is not updated, but also crashes with an error when switching to another screen.
I do not understand what exactly is the problem, in unnecessary recompositions or in receiving data from the room, maybe they do not have time to update or what else.
the database works correctly I observed in the app inspector that deletion and addition of items occur.
I want the aitems to be removed and the screen redrawn correctly.
alexxelo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.