简体   繁体   中英

Jetpack compose Lazy Column item state does not change when state in viewModel changes

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.

I could not believe that the answer can be so simple but here are the solutions:

For the posted screen, I just changed _uiState.value = _uiState.value.copy(...) to _uiState.value = UIState(...copy and replace everything with old value...) as

_uiState.value = UIState(
                            data = data,
                            isLoading = false,
                            error = "",
                            searchQuery = _uiState.value.searchQuery,
                            sortType = _uiState.value.sortType
                        )

For the second screen, I was just double changing the isAdded value by sending the data directly without copying. As the api call changes the isAdded value again, and the read from room flow changes it again, the state were changed twice.

However, I still wonder why compose didn't recompose when I changed the memory location of data in UIState.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM