I have debugged the app and I saw that the data in UIState changes when I try to add or remove the item, especially the isAdded field. However, even though the isAdded changes, the AddableItem does not recompose. Additionally, when I try to sort items, or try to write a query THAT WILL NOT SEND ANY API REQUEST, JUST CHANGES THE STRING IN TEXTFIELD, the UI recomposes. So UI reacts to changes in UIState. I have searched for similar issues but cannot find anything. I believe that the framework must recompose when the pointer of the filed changes, however, it does not. Any idea why this happens or solve that?
This is the viewModel:
@HiltViewModel class AddableItemScreenViewModel@Inject constructor( val getAddableItemsUseCase: GetItems, val getItemsFromRoomUseCase: GetRoomItems, val updateItemCase: UpdateItem, savedStateHandle: SavedStateHandle) : ViewModel() { private val _uiState = mutableStateOf(UIState()) val uiState: State<UIState> = _uiState private val _title = mutableStateOf("") val title: State<String> = _title private var getItemsJob: Job? = null init { savedStateHandle.get<String>(NavigationConstants.TITLE)?.let { title -> _title.value = title } savedStateHandle.get<Int>(NavigationConstants.ID)?.let { id -> getItems(id = id.toString()) } } fun onEvent(event: ItemEvent) { when(event) { is ItemEvent.UpdateEvent -> { val modelToUpdate = UpdateModel( id = event.source.id, isAdded = event.source.isAdded, name = event.source.name, index = event.source.index ) updateUseCase(modelToUpdate).launchIn(viewModelScope) } is ItemEvent.QueryChangeEvent -> { _uiState.value = _uiState.value.copy( searchQuery = event.newQuery ) } is ItemEvent.SortEvent -> { val curSortType = _uiState.value.sortType _uiState.value = _uiState.value.copy( sortType = if(curSortType == SortType.AS_IT_IS) SortType.ALPHA_NUMERIC else SortType.AS_IT_IS ) } } } private fun getItems(id: String) { getItemsJob?.cancel() getItemsJob = getItemsUseCase(id) .combine( getItemsFromRoomUseCase() ){ itemsApiResult, roomData -> when (itemsApiResult) { is Resource.Success -> { val data = itemsApiResult.data.toMutableList() // Look the api result, if the item is added on room, make it added, else make it not added. This ensures API call is done once and every state change happens because of room. for(i in data.indices) { val source = data[i] val itemInRoomData = roomData.find { it.id == source.id } data[i] = data[i].copy( isAdded = itemInRoomData != null ) } _uiState.value = _uiState.value.copy( data = data, isLoading = false, error = "", ) } is Resource.Error -> { _uiState.value = UIState( data = emptyList(), isLoading = false, error = itemsApiResult.message, ) } is Resource.Loading -> { _uiState.value = UIState( data = emptyList(), isLoading = true, error = "", ) } } }.launchIn(viewModelScope) } } This it the composable:
@OptIn(ExperimentalComposeUiApi::class) @Composable fun AddableItemsScreen( itemsViewModel: AddableItemScreenViewModel = hiltViewModel() ) { val state = itemsViewModel.uiState.value val controller = LocalNavigationManager.current val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current val mainScrollState = rememberLazyListState() val focusRequester = remember { FocusRequester() } // Screen UI Box( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colors.BackgroundColor) .clickable( indication = null, interactionSource = remember { MutableInteractionSource() } ) { focusManager.clearFocus() }, ) { LazyColumn( modifier = Modifier .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, state = mainScrollState, ) { item { WhiteSpacer( whiteSpacePx = 200, direction = SpacerDirections.VERTICAL ) } if (state.isLoading) { item { ProgressIndicator() } } if (state.error.isNotEmpty() && state.error.isNotBlank()) { item { ErrorText() } } if (state.data.isNotEmpty()) { val data = if (state.sortType == SortType.ALPHA_NUMERIC) state.data.sortedBy { it.name } else state.data data.forEach { source -> if((state.searchQuery.isEmpty() && state.searchQuery.isBlank()) || (source.name != null && source.name.contains(state.searchQuery, ignoreCase = true))) { item { AddableItem( modifier = Modifier .padding( vertical = dimManager.heightPxToDp(20) ), text = source.name ?: "", isAdded = source.isAdded ?: false, onItemPressed = { controller.navigate( Screens.ItemPreviewScreen.route + "?title=${source.name}" + "&id=${source.categoryId}" + "&isAdded=${source.isAdded}" ) }, onAddPressed = { itemsViewModel.onEvent(ItemEvent.UpdateEvent(source)) } ) } } } } } Column( modifier = Modifier .align(Alignment.TopStart) .background( MaterialTheme.colors.BackgroundColor ), ) { ItemsScreenAppBar( title = itemsViewModel.title.value, onSortPressed = { itemsViewModel.onEvent(ItemEvent.SortEvent) } ) { controller.popBackStack() } SearchBar( query = state.searchQuery, focusRequester = focusRequester, placeholder = itemsViewModel.title.value, onDeletePressed = { itemsViewModel.onEvent(ItemEvent.QueryChangeEvent("")) }, onValueChanged = { itemsViewModel.onEvent(ItemEvent.QueryChangeEvent(it)) }, onSearch = { keyboardController!!.hide() } ) WhiteSpacer( whiteSpacePx = 4, direction = SpacerDirections.VERTICAL ) } } } And finally this is the UIState:
data class UIState( val data: List<ItemModel> = emptyList(), val isLoading: Boolean = false, val error: String = "", val searchQuery: String = "", val sortType: SortType = SortType.AS_IT_IS, ) @Parcelize data class ItemModel ( val id: Int? = null, var isAdded: Boolean? = null, val name: String? = null, val index: Int? = null, @SerializedName("someSerializedNameForApi") var id: Int? = null ): Parcelable Finally, I have a similar issue with almost the same viewModel with the same UI structure. The UI contains an Add All button and when everything is added, it turns to Remove All. I also hold the state of the button in UIState for that screen. When I try to add all items or remove all items, the UI recomposes. But when I try to add or remove a single item, the recomposition does not happen as same as the published code above. Additionally, when I remove one item when everything is added on that screen, the state of the button does change but stops to react when I try to add more. I can also share that code if you people want. I still do not understand why the UI recomposes when I try to sort or try to add-remove all on both screens but does not recompose when the data changes, even though I change the pointer address of the list.
Thanks for any help.