Android: Definitive guide to Paging 3
Tags: android, jetpack, mobile, paging
Published: 2021-04-05 11:59:48

# Overview

Paging 3 is a library from the Android Jetpack that helps you load and display pages of data from a larger amount of data from local or remote data source. This approach allows your app to use efficiently the bandwidth and system resources once the user may not see all the data loaded at once.

## Advantages

- In-memory cache.
- Makes your RecyclerView's Adapter to automatically request new data when the user scrolls toward the end.
- Flow and LiveData support.
- Error handling, refresh and retry capabilities.

## Architecture

The Paging library integrates directly into the recommended Android app architecture. The library's components operate in three layers of the app:

- Repository
- ViewModel
- View

![https://developer.android.com/topic/libraries/architecture/images/paging3-library-architecture.svg](https://developer.android.com/topic/libraries/architecture/images/paging3-library-architecture.svg)

### Repository

In the Repository layer there is the `PagingSource`. The PagingSource defines a source of data and how to retrieve data from it. Also, can load from remote or local data sources.

In addiction, in the Repository layer there is the `RemoteMediator`. The RemoteMediator handles paging from a layered data source.

### ViewModel

The `Pager` component provides a public API for constructing instances of `PagingData` that are exposed to the View, based on a `PagingSource` and a `PagingConfig` configuration.

### View

In the View layer there is the `PagingDataAdapter`, a RecyclerView adapter that handle the paginated data.

# Implementation

For our implementation of the Paging 3, we are going to use the Github API [https://api.github.com/users/google/repos](https://api.github.com/users/google/repos) which can be used the parameters `page` and `per_page` to handle pagination like this: [https://api.github.com/users/google/repos?page=1&per_page=20](https://api.github.com/users/google/repos?page=1&per_page=20).

This will be the final result:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ahsk8uhcqccpy2opq8f3.png)

To keep it simples we'll model our `Repo` class as simple as possible to keep all the focus on the Paging library. This will be our `Repo` class:

```kotlin
data class Repo(
    val fullName: String
)
```

The `GithubApi` that will be used to retrieve data is:

```kotlin
interface GithubApi {

    @GET("users/{username}/repos")
    suspend fun fetchRepos(
        @Path("username") username: String,
        @Query("page") page: Int,
        @Query("per_page") size: Int
    ): List<Repo>
}
```

## Setup

Add the following to your app's build.gradle.

```gradle
dependencies {
	def paging_version = "3.0.0-beta03"

	implementation "androidx.paging:paging-runtime-ktx:$paging_version"
}
```

## Data Source

Let's start building our `PagingSource` which will load more and more data once the scroll toward the end.

```kotlin
private const val INITIAL_PAGE = 1

class GithubRepoPagingSource(
    private val api: GithubApi,
		private val username: String
) : PagingSource<Int, Repo>() {
    override fun getRefreshKey(state: PagingState<Int, Repo>): Int? {
        TODO("Not yet implemented")
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
        TODO("Not yet implemented")
    }
}
```

The `Key` and `Value` parameters from `PagingSource` are the type of key which define what data to load (Int to represent a page) and the type of data loaded by this PagingSource, respectively.

This `GithubRepoPagingSource` receives a `GithubAPI` where the data is retrieved and a `username` to fetch the repos.

The `load()` function will be called by the Paging to fetch more data to be displayed to the user and can have this implementation:

```kotlin
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
        return try {
            val page = params.key ?: INITIAL_PAGE
            val response = api.fetchRepos(username, page, params.loadSize)
            LoadResult.Page(
                data = response,
                prevKey = if (page == INITIAL_PAGE) null else page - 1,
                nextKey = if (response.isEmpty()) null else page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
```

The refresh key is used for subsequent refresh calls to `PagingSource.load()` (the first call is initial load which uses `initialKey` provided by `Pager`). A refresh happens whenever the Paging library wants to load new data to replace the current list, e.g., on swipe to refresh or on invalidation due to database updates, config changes, process death, etc. Typically, subsequent refresh calls will want to restart loading data centered around `PagingState.anchorPosition` which represents the most recently accessed index.

```kotlin
override fun getRefreshKey(state: PagingState<Int, Repo>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }
```

## Repository

Now that we already implemented our data source, let's build the Repository.

```kotlin
class GithubRepository(
    private val api: GithubApi
) {
    fun searchRepos(username: String) = Pager(
        pagingSourceFactory = { GithubRepoPagingSource(api, username) },
        config = PagingConfig(
            pageSize = 20
        )
    ).flow
}
```

Here we have a `searchRepos` method that returns a `Flow<PagingData<Repo>>`. In the `Pager` object we define the `GithubRepoPagingSource` created previously and a `PagingConfig` with the `pageSize` parameter.

## ViewModel

In the ViewModel we are going to expose the `Flow<PagingData<Repo>>`. Here we are avoiding to request the same username if it was previously requested. Also, we are caching the content of the `Flow<PagingData>>`.

```kotlin
class GithubViewModel(
    private val repository: GithubRepository
) : ViewModel() {

    private var currentUsernameValue: String? = null

    private var currentSearchResult: Flow<PagingData<Repo>>? = null

    fun searchRepos(username: String): Flow<PagingData<Repo>> {
        val lastResult = currentSearchResult
        if (username == currentUsernameValue && lastResult != null) {
            return lastResult
        }
        currentUsernameValue = username
        val newResult = repository.searchRepos(username)
            .cachedIn(viewModelScope)
        currentSearchResult = newResult
        return newResult
    }
}
```

## Adapter

Now we are in the View component, let's start with the Adapter and make it works with Paging.

```kotlin
class ReposAdapter : PagingDataAdapter<Repo, ReposAdapter.ViewHolder>(COMPARATOR) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
        ItemReposBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    )

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        getItem(position)?.let { holder.bind(it) }
    }

    class ViewHolder(
        private val binding: ItemReposBinding
    ) : RecyclerView.ViewHolder(binding.root) {
        fun bind(repo: Repo) = with(binding) {
            tvItemRepos.text = repo.fullName
        }
    }

    companion object {
        private val COMPARATOR = object : DiffUtil.ItemCallback<Repo>() {
            override fun areItemsTheSame(oldItem: Repo, newItem: Repo): Boolean =
                oldItem.fullName == newItem.fullName

            override fun areContentsTheSame(oldItem: Repo, newItem: Repo): Boolean =
                oldItem == newItem
        }
    }
}
```

The implementation of the Adapter is pretty much the same as a usual `RecyclerView.Adater`, however now we need to extend from `PagingDataAdapter` and pass a `COMPARATOR` to its constructor.

## Activity/Fragment

We need to launch a new coroutine to search for the repos. We'll do that in the `lifecyclerScope`.

We also want to ensure that whenever the user searches for a new username, the previous query is cancelled. To do this, our Activity/Fragment can hold a reference to a new `Job` that will be cancelled every time we search for a new username.

```kotlin
private var searchJob: Job? = null

private fun search(username: String) {
   // Make sure we cancel the previous job before creating a new one
   searchJob?.cancel()
   searchJob = lifecycleScope.launch {
       viewModel.searchRepos(username).collect { adapter.submitData(it) }
   }
}
```

At this point your app is ready and working as expected, however there are more customization that we could do, let's continue and see them!

## Display the loading and error state in the footer

For a better experience, we can display a loading state when the list is fetching new data or display an error when something wrong happens.

For this purpose we need to create a new xml file, a new Adapter and ViewHolder, let's see them below:

```kotlin
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="8dp">

    <TextView
        android:id="@+id/error_msg"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:textAlignment="center"
        android:textSize="22sp"
        tools:text="Timeout" />

    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />

    <Button
        android:id="@+id/retry_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="@string/retry_button" />
</LinearLayout>
```

```kotlin
class ReposLoadStateAdapter(
    private val retry: () -> Unit
) : LoadStateAdapter<ReposLoadStateAdapter.ViewHolder>() {

    override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) = holder.bind(loadState)

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState) = ViewHolder(
        ItemReposLoadStateFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false),
        retry
    )

    class ViewHolder(
		    private val binding: ItemReposLoadStateFooterBinding,
        retry: () -> Unit
    ) : RecyclerView.ViewHolder(binding.root) {

        init {
            binding.retryButton.setOnClickListener { retry() }
        }

        fun bind(loadState: LoadState) = with(binding) {
		        if (loadState is LoadState.Error) {
                errorMsg.text = loadState.error.localizedMessage
            }
            progressBar.isVisible = loadState is LoadState.Loading
            retryButton.isVisible = loadState is LoadState.Error
            errorMsg.isVisible = loadState is LoadState.Error
        }
    }
}
```

Now that we have our load state adapter done, we need to link to our list.

```kotlin
binding.recyclerView.adapter = adapter.withLoadStateHeaderAndFooter(
		header = ReposLoadStateAdapter { adapter.retry() },
		footer = ReposLoadStateAdapter { adapter.retry() }
)
```

## Empty state

What if we try to load some data but there is no data (list is 0)?

For this purpose we need to be notified when the load state is changed, we'll use tee `addLoadStateListener`.

```kotlin
adapter.addLoadStateListener { loadState ->
		val isEmptyList = loadState.refresh is LoadState.NotLoading && adapter.itemCount == 0
		showEmptyList(isEmptyList)
}
```

## Load and error state for the Activity/Fragment

For the first request, until now there is no feedback what is happening in the screen, also if happens some problem we won't know because there is no error handling.

Update your Activity/Fragment and add a progress bar and a retry button. Don't forget to set the retry click:

```kotlin
binding.retryButton.setOnClickListener { adapter.retry() }
```

Let's update the previous `addLoadStateListener` and update the visibility of the progress bar and retry button. 

```kotlin
adapter.addLoadStateListener { loadState ->
		val isEmptyList = loadState.refresh is LoadState.NotLoading && adapter.itemCount == 0
		showEmptyList(isEmptyList)

		// Only show the list if refresh succeeds
		binding.recyclerView.isVisible = loadState.source.refresh is LoadState.NotLoading
		// Show loading spinner during initial load or refresh
		binding.progressBar.isVisible = loadState.source.refresh is LoadState.Loading
		// Show the retry state if initial load or refresh fails
		binding.retryButton.isVisible = loadState.source.refresh is LoadState.Error
}
```

---

This is everything we need to start using Paging 3 in our app!

## Conclusion

Paging 3 is a pagination library for Android that is build into the recommended Android app architecture. It uses Flow or LiveData for the communication between the layers.

Also, it handles the load, error and empty state in a simple way, giving us more flexibility for that.