Load next data when on click of button using the Paging 3 library for Compose

750 views Asked by At

Currently Paging library handles when to load the data automatically when a user scrolls down. But what if you want to give the user full authority for when they want the next page of data to be loaded i.e when button is clicked show next page of movies. How can you handle this in Paging library? See below how I've implemented the paging to load data as a user scrolls down

Here below this how I implemented the Paging to load next page when user scrolls down


class MoviesPagingDataSource(
    private val repo: MoviesRepository,
 ) : PagingSource<Int, Movies>() {

    override fun getRefreshKey(state: PagingState<Int, Movies>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            val anchorPage = state.closestPageToPosition(anchorPosition)
            anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movies> {
        return try {
            val nextPageNumber = params.key ?: 0
            val response = repo.getMovies(page = nextPageNumber, size = 10)
            LoadResult.Page(
                data = response.content,
                prevKey = null,
                nextKey = if (response.content.isNotEmpty()) response.number + 1 else null
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
 }

This is how I emit the state in ViewModel for the UI to observe


@HiltViewModel
 class MoviesViewModel @Inject constructor(
   private val moviesRepository: MoviesRepository
): ViewModel {
   ....
   //emitting the data to the UI to observe

        val moviesPagingDataSource = Pager(PagingConfig(pageSize = 10)) {
        MoviesPagingDataSource(moviesRepository)
       }.flow.cachedIn(viewModelScope)
}

How I'm observing it in the UI


@Composable
fun MoviesList(viewModel: MoviesViewModel) {

    val moviesList = viewModel.moviesPagingDataSource.collectAsLazyPagingItems()

    LazyColumn {
        items(moviesList) { item ->
            item?.let { MoviesCard(movie = it) }
        }

        when (moviesList.loadState.append) {
            is LoadState.NotLoading -> Unit
            LoadState.Loading -> {
                item {
                    LoadingItem()
                }
            }
            is LoadState.Error -> {
                item {
                    ErrorItem(message = "Some error occurred")
                }
            }
        }

        when (moviesList.loadState.refresh) {
            is LoadState.NotLoading -> Unit
            LoadState.Loading -> {
                item {
                    Box(
                        modifier = Modifier.fillMaxSize(),
                        contentAlignment = Center
                    ) {
                        CircularProgressIndicator()
                    }
                }
            }
            is LoadState.Error -> TODO()
        }
    }
}

So currently I'm adding 1 to the previous page every time a user clicks the button to load more movies then saving this movies to the list state. Also making sure the current page is greater than or equal to total pages before loading more data and adding to the list state of previous loaded movies

2

There are 2 answers

1
Jack On

you can use Channel to block LoadResult returning from load, when user clicks the button, send an element to the Channel. here is the simple: https://github.com/sunchao0108/ComposePagingDemo

0
vodailuong On

Here is my solution:

Instead of rendering the item list by

        items(moviesList) { item ->
            item?.let { MoviesCard(movie = it) }
        }

we should manually render it by index and use moviesList.itemSnapshotList[index] to get the item object to ignore sending the item view event to Paging. This will help not to notify the Paging to load the next page.

        items(
            count = moviesList.itemCount,
            key = moviesList.itemKey { it.id },
        ) { index ->
            // Get item from itemSnapshotList instead of moviesList[index]
            // to ignore sending item view event to Paging
            val item = moviesList.itemSnapshotList[index]
            if (item != null) {
                MoviesCard(movie = item)
            }
        }

When clicking on the "Load More" button, force calling LazyPagingItems.get(index) > PagingDataDiffer.get(index) > hintReceiver?.accessHint(presenter.accessHintForPresenterIndex(index)) to trigger load more manually.

        onClick = {
            moviesList[moviesList.itemCount - 1]
        },