Search code examples
kotlinpaginationandroid-jetpack-composeinfinite-scrolllazycolumn

Jetpack compose - load more data or pagination, LazyVerticalStaggeredGrid/LazyColumn disappears


I am implementing a pagination or load more data, whenever LazyVerticalStaggeredGrid/LazyColumn reaches the end/last item. Everything works as expected however, when the data load more function, the LazyVerticalStaggeredGrid goes off the screen and shows only the loader and once more data is loaded LazyVerticalStaggeredGrid shows up again. I'm not sure what I'm doing wrong here. I've attached the code below for better understanding.

 Column {
            val dataFlow = viewModel.uiStateFlow.collectAsStateWithLifecycle()

            if (dataFlow.value.error is Throwable) {
                Log.d(TAG, "View: Error")
                Toast.makeText(
                    LocalContext.current,
                    dataFlow.value.error.toString(),
                    Toast.LENGTH_LONG
                ).show()
            } else {
                Log.d(TAG, "View: listview")
                PhotosGrid(
                    modifier = Modifier
                        .fillMaxWidth()
                        .fillMaxHeight(),
                    list = dataFlow.value.list,
                    loading = dataFlow.value.loading
                ) {
                    viewModel.getPhotos()
                }
            }
        }

@Composable
fun PhotosGrid(
    modifier: Modifier,
    lazyListState: LazyStaggeredGridState = rememberLazyStaggeredGridState(),
    list: List<PhotosModel>,
    loading: Boolean = false,
    loadMore: () -> Unit
) {
    val reachedBottom: Boolean by remember { derivedStateOf { lazyListState.reachedBottom() } }

    if (reachedBottom && !loading) {
        Log.d(TAG, "View: reached bottom")
        loadMore()
    }

    LazyVerticalStaggeredGrid(
        modifier = modifier,
        columns = StaggeredGridCells.Fixed(2),
        horizontalArrangement = Arrangement.spacedBy(4.dp),
        verticalItemSpacing = 4.dp,
        contentPadding = PaddingValues(all = 4.dp),
        state = lazyListState
    ) {
        items(list) {
            PhotosItem(photosModel = it)
        }
        if (loading) {
            item {
                CircularProgressIndicator(
                    modifier = Modifier.width(64.dp),
                    color = MaterialTheme.colorScheme.secondary
                )
            }
        }
    }
}
@Composable
fun PhotosItem(photosModel: PhotosModel) {
    val imgUrl = photosModel.urls?.regular
    Box {
        AsyncImage(
            model = ImageRequest.Builder(LocalContext.current)
                .data(imgUrl)
                .crossfade(true)
                .build(),
            error = painterResource(R.drawable.broken_image),
            placeholder = painterResource(R.drawable.ic_loader),
            contentDescription = photosModel.description,
            contentScale = ContentScale.Crop,
            modifier = Modifier
                .fillMaxWidth()
                .fillMaxHeight()
                .clip(RectangleShape)
        )
    }
}
internal fun LazyStaggeredGridState.reachedBottom(buffer: Int = 1): Boolean {
    val lastVisibleItem = this.layoutInfo.visibleItemsInfo.lastOrNull()
    return lastVisibleItem?.index != 0 && lastVisibleItem?.index == this.layoutInfo.totalItemsCount - buffer
}

Logic in ViewModel

 var pageNo = 0


    private val _uiStateFlow = MutableStateFlow(UiState(false, emptyList(), null))
    val uiStateFlow: StateFlow<UiState> = _uiStateFlow

    init {
        viewModelScope.launch {
            photosRepository.getAllPhotos().collectLatest { list ->
                if (list.isEmpty()) {
                    getPhotos()
                } else {
                    _uiStateFlow.update { UiState(list = list) }
                }
            }
        }
    }

    fun getPhotos() = viewModelScope.launch {
        photosRepository.getPhotos(accessKey = ConstantUrls.ACCESS_KEY, pageNo = pageNo++) { event ->
            when(event) {
                is ResponseEvent.Failure -> {
                    _uiStateFlow.update { UiState(error = event.error) }
                }
                ResponseEvent.Loading -> {
                    _uiStateFlow.update { UiState(loading = true) }
                }
            }
        }
    }
data class UiState(
    val loading: Boolean = false,
    val list: List<PhotosModel> = emptyList(),
    val error: Throwable? = null
)

Solution

  • If you are sending the loading UIState like this

    _uiStateFlow.update { UiState(loading = true) }
    

    without passing on a value for the list property, it will use an emptyList by default as you specified in the UIState class:

    val list: List<PhotosModel> = emptyList()
    

    As a result, while loading, the LazyVerticalStaggeredGrid became empty and would only display the single loading item.
    To fix this, pass the current list alongside the UIState while loading like this:

    _uiStateFlow.update { UiState(loading = true, list = uiStateFlow.value) }