Hey there! Welcome to my Blog.

I’m Raj and I will help you implement lazy list paging in android with jetpack compose.
We are going to make use of Tmdb Api and show movies.

Alternative you can refer this code from the GitHub repository which I will be using as a reference. And there is preview below on how it would look like.

gif preview paging compose android

Dependency

By the time I wrote this article, 1.0.0-alpha18 was the latest one. Replace your dependency version if new one is available.

implementation "androidx.paging:paging-compose:1.0.0-alpha18"

1. Prepare your API to return paged response

The Url which we are using to fetch list of movies will return below Json response.

fun getNowPlayingMoviesUrl(
    page: Int
): URL {
    return URL("/movie/now_playing?$api_key=API_KEY&page=$page")
}

Makes sure your API supports paginated response before trying implement paging in you app.

{ } JSON
    page: 1        
    [] results
       {} 0
       {} 1
       ....
       {} 18
       {} 19
       total_pages: 73
       total_results: 1458

2. Fetch data from API

Create below data class, which contains properties helps us build paging with information.

data class PagedResponse<T>(
    val data: List<T>,
    val total: Int,
    val page: Int,
)
  • data – Contains list of movie objects.
  • total – Total number of items each page combined as total in response.
  • page – Number for first page or current page.

Below I’m getting the Json response into PagedResponse of type Movie objects.

fun fetchNowPlayingMoviesJsonData(
    response: String
): PagedResponse<Movie> {
    
    val movieList: MutableList<Movie> = ArrayList()
    val baseJsonArray = JSONObject(response)
    val moviesJsonArray = baseJsonArray.getJSONArray("results")
    val totalResults = baseJsonArray.getInt("total_results")
    val page = baseJsonArray.getInt("page")
    ...

    return PagedResponse(
        data = movieList,
        total = totalResults,
        page = page
    )
}

At this point, API is ready to return paged response of list of movie objects.

3. Create paging source to manage refresh and load

If you are following repository pattern for all network data information you are receiving in one place, contain the new information in the repository class.

The function should certainly return PagedResponse movie objects and take page as parameter.

class MovieRepository(
    private val networkDataSource: NetworkDataSource
) {
    suspend fun getNowPlayingMoviesData(page: Int): PagedResponse<Movie> {
        return networkDataSource.getNowPlayingMoviesData(page)
    }
}

Create a class which extends type PagingSource<Int, Movie> and override functions which handles refreshing and loading paging information.

Pass your repository class into this new class since you need to access function which returns response data.

class MoviesPagingSource(
    private val movieRepository: MovieRepository
) : PagingSource<Int, Movie>() {

    override fun getRefreshKey(state: PagingState<Int, Movie>): Int? { }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> { }
}
  • getRefreshKey – Provides a key which is of type Int to load given page number in response.
  • load – Will load data asynchronously from network with given information from response.

Simply return the anchor position for function to return the page number to load.
If the position is null, return the closest page position.
If the closest position is found, increment or decrement the position by 1 to return correct position.

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

Now for load function.

Get the recent key for loading the page, if that value is null, safely return 1.
I’m using 1 here since the default start index in the API is 1.

val currentPageNumber = params.key ?: 1

Pass the accessed page number into the function which returns the response by page number.

val movies: PagedResponse<Movie> = movieRepository.getNowPlayingMoviesData(
    page = currentPageNumber
)

We need to handle the logic until when we need to keep refreshing and load new information.
We will sum up all the items previously loaded and compare those to total available items available.

If there are more items, simply increment the page number by 1.
When there are no items to load, we will return null.

val nextKey = when {
    (params.loadSize * (currentPageNumber + 1)) < movies.total -> currentPageNumber + 1
    else -> null
}

With available key information to handle refresh, return the result and data with LoadResult.Page

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> {
    val currentPageNumber = params.key ?: 1

    val movies: PagedResponse<Movie> = movieRepository.getNowPlayingMoviesData(
        page = currentPageNumber
    )

    val nextKey = when {
        (params.loadSize * (currentPageNumber + 1)) < movies.total -> currentPageNumber + 1
        else -> null
    }

    return LoadResult.Page(
        prevKey = null,
        nextKey = nextKey,
        data = movies.data
    )
}

4. Paging configuration with Pager

A Pager is paging configuration which requires information on

  1. How much the size of each page size should be.
  2. Initial page to load (which is a key).
  3. Data source factory which is the network response we received in repository.
val moviesData: Flow<PagingData<Movie>> = Pager(
    config = PagingConfig(pageSize = 20),
    initialKey = 1,
    pagingSourceFactory = { MoviesPagingSource(movieRepository) }
).flow.cachedIn(viewModelScope)

Convert to Flow

.flow

Pager takes care of returning the data as of type Flow, if we assign .flow call to the Pager.
You can manipulate the type of data being returned by Pager with other extension types instead of flow.

Caching into ViewModel scope

.cachedIn(viewModelScope)

The flow is kept active as long as the given scope is active, in our case until the ViewModel is destroyed.

ViewModel

class HomeViewModel @Inject constructor(
    private val movieRepository: MovieRepository
) : ViewModel() {

    val moviesData: Flow<PagingData<Movie>> = Pager(
        config = PagingConfig(pageSize = 20),
        initialKey = 1,
        pagingSourceFactory = { MoviesPagingSource(movieRepository) }
    ).flow.cachedIn(viewModelScope)
}

5. Display lazy list items into Composable

Since your ViewModel contains movie information of type Flow of PagingData, we will collect values from it and show it inside the composable with LazyPagingItems using collectAsLazyPagingItems().

import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items

@Composable
fun Content(viewMode: YourViewModel) {
    val movies: LazyPagingItems<Movie> = viewModel.moviesData.collectAsLazyPagingItems()

    LazyColumn {
        items(items = movies) { movieItem: Movie? ->
            Image(
                painter = rememberImagePainter(imageUrl),
                contentDescription = "Movie poster",
            )
        }
    }
}

If you want to keep the Pager configuration into separate place or in domain layer, you can create a class and contain the information this way.

class GetPagedMovies(
    private val movieRepository: MovieRepository
) {
    operator fun invoke(
        viewModelScope: CoroutineScope
    ): Flow<PagingData<Movie>> {
        return Pager(
            config = PagingConfig(pageSize = PAGE_SIZE),
            initialKey = INITIAL_PAGE_KEY,
            pagingSourceFactory = {
                MoviesPagingSource(movieRepository)
            }
        ).flow.cachedIn(viewModelScope)
    }

    companion object {
        private const val INITIAL_PAGE_KEY = 1
        private const val PAGE_SIZE = 20
    }
}

And your ViewModel can use this information by keeping the Pager logic in other place.
This will help you have things separated and easy to test those little piece of information.

class YourViewModel(
    private val getPagedMovies: GetPagedMovies
) : ViewModel() {

    val moviesData: Flow<PagingData<Movie>> = getPagedMovies(viewModelScope)
}

6. Show lazy list in LazyVerticalGrid scope

As of now, there is no extension type support for showing the lazy items list when the scope is LazyVerticalGrid. But dependency offers for both LazyRow and LazyColumn.

LazyVerticalGrid(
    columns = GridCells.Fixed(3)
) {
    itemsPaging(items = movies) { movieItem: Movie? ->
        Image(
            painter = rememberImagePainter(imageUrl),
            contentDescription = "Movie poster",
        )
    }
}

To make this work, copy below functions and use the extension function and access it inside the LazyGridScope.

fun <T : Any> LazyGridScope.itemsPaging(
    items: LazyPagingItems<T>,
    key: ((item: T) -> Any)? = null,
    itemContent: @Composable LazyGridItemScope.(value: T?) -> Unit
) {
    items(
        count = items.itemCount,
        key = if (key == null) null else { index ->
            val item = items.peek(index)
            if (item == null) {
                PagingPlaceholderKey(index)
            } else {
                key(item)
            }
        }
    ) { index ->
        itemContent(items[index])
    }
}

@SuppressLint("BanParcelableUsage")
private data class PagingPlaceholderKey(private val index: Int) : Parcelable {

    override fun writeToParcel(parcel: Parcel, flags: Int) = parcel.writeInt(index)

    override fun describeContents(): Int = 0

    companion object {
        @Suppress("unused")
        @JvmField
        val CREATOR: Parcelable.Creator<PagingPlaceholderKey> =
            object : Parcelable.Creator<PagingPlaceholderKey> {
                override fun createFromParcel(parcel: Parcel) =
                    PagingPlaceholderKey(parcel.readInt())
                override fun newArray(size: Int) = arrayOfNulls<PagingPlaceholderKey?>(size)
            }
    }
}

Please keep in mind, since this is not made available and could surely have bad effects or unstable.

That’s it!

Take a look at GitHub repository which I have linked below which has complete code example with preview above.

Read all the detailed guidelines from Android documentation, it covers everything in much depth.

Project code and resources :

Rajasekhar K E


Hi ! I’m Rajasekhar a Programmer who does Android Development, Creative & Technical writing, Kotlin enthusiast and Engineering graduate. I learn from Open Source and always happy to assist others with my work. I spend most of time Training, Assisting & Mentoring students who are absolute Beginners in android development. I’m also running my startup named Developers Breach which mostly works on contributing to open source.

Here We Go Again : (

if (article == helpful) {
    println("Like and subscribe to blog newsletter.")
} else {
    println("Let me know what i should blog on.")
}

This site uses Akismet to reduce spam. Learn how your comment data is processed.