简体   繁体   English

当 viewModel 中的状态发生变化时,Jetpack compose Lazy Column 项目状态不会改变

[英]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.我已经调试了应用程序,当我尝试添加或删除项目时,我看到 UIState 中的数据发生了变化,尤其是 isAdded 字段。 However, even though the isAdded changes, the AddableItem does not recompose.但是,即使 isAdded 发生更改,AddableItem 也不会重构。 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.此外,当我尝试对项目进行排序或尝试编写不会发送任何 API 请求的查询时,只需更改 TEXTFIELD 中的字符串,UI 就会重新组合。 So UI reacts to changes in UIState.所以 UI 会对 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:最后这是 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.最后,对于具有相同 UI 结构的几乎相同的视图模型,我遇到了类似的问题。 The UI contains an Add All button and when everything is added, it turns to Remove All. UI 包含一个“全部添加”按钮,当所有内容都添加完毕后,它会变为“全部删除”。 I also hold the state of the button in UIState for that screen.我还在该屏幕的 UIState 中保存按钮的状态。 When I try to add all items or remove all items, the UI recomposes.当我尝试添加所有项目或删除所有项目时,UI 会重新组合。 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.我仍然不明白为什么当我尝试在两个屏幕上进行排序或尝试添加-删除所有内容时 UI 会重新组合,但在数据更改时不会重新组合,即使我更改了列表的指针地址也是如此。

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.value.copy(...) 更改为 _uiState.value = UIState(...copy and replace all with old value...) 为

_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.对于第二个屏幕,我只是通过直接发送数据而不复制来双重更改 isAdded 值。 As the api call changes the isAdded value again, and the read from room flow changes it again, the state were changed twice.随着 api 调用再次更改 isAdded 值,并且从房间流中读取再次更改它,状态更改了两次。

However, I still wonder why compose didn't recompose when I changed the memory location of data in UIState.但是,我还是想知道为什么我在 UIState 中更改数据的内存位置时,compose 没有重新组合。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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