Jetpack Compose Pagination
Implementing pagination using the Paging 3 library in Jetpack Compose

Fetching a long list of data from the server is a very costly operation. It takes too long and too much memory. In those situations, the backend will split the list into pages. That is called pagination.
We are all familiar with androidx.paging
libraries. The most recent version is Paging 3
. I am gonna show you how to use the Paging 3
library in Jetpack Compose.
“The Paging library helps you load and display pages of data from a larger dataset from local storage or over network. This approach allows your app to use both network bandwidth and system resources more efficiently.
The components of the Paging library are designed to fit into the recommended Android app architecture, integrate cleanly with other Jetpack components, and provide first-class Kotlin support.” According to the Paging official documentation
Let’s start by adding the dependency:
implementation "androidx.paging:paging-compose:1.0.0-alpha16"
Note: Check if there is a newer version of this dependency.
We will need some data to showcase pagination. For that, we will use Tasty APIs. At this link, you can find the API that we are gonna use.
Except for the paging library, I am using Hilt, Retrofit, OkHttp, Moshi, Coil, and Accompanist’s Placeholder. Make sure to add all those libraries. The link to my GitHub repo will be at the end of the article.
The first thing that we need to implement here is a component that will be our data source. Depending on where are we loading data, there are two types of data sources that we can extend here:
RemoteMediator<Key : Any, Value : Any>
— it is used to incrementally load data from a remote source into a local source.PagingSource<Key: Any, Value: Any>
— it defines a source of data and how to retrieve data from that source. It can load data from any single source, including network and local databases.
Basically for fetching data that you want to show in UI you will use PagingSource
and if you want to incrementally load data from the network to the local database you will use RemoteMediator
.
In this article, we will use PagingSource
. If you want to learn more about RemoteMediator
, take a look at this. Here is our implementation of PagingSource
:
We extend PagingSource
by passing Int
as a type of the key
and RecipeModel
as a type of the value
. RecipeModel
is what we load from this source.
There are two functions we needed to implement, getRefreshKey
and load
. Here is why they are needed:
getRefreshKey
— provide a key used for the initial load for the nextPagingSource
due to the invalidation of thisPagingSource
. The key is provided to the load viaLoadParams.key
. The parameter of the function,state: PagingState<Key, Value>
, is the current state of the fetched data, it includes loaded pages of data(pages: List<Page<Key, Value>>
), most recently accessed index in the list(anchorPosition: Int?
) and confines that were given when initializing thePagingData
stream(config: PagingConfig
, we will talk more about this later).load
— trigger the async load of the data, from DB or the network. The parameter of the function,params: LoadParams<Key>
, are params for the load request and it contains the requested number of items to load(loadSize: Int
), if the placeholders are enabled(placeholdersEnabled: Boolean
), and the key for the page to be loaded(key: Int
, explained in thegetRefreshKey
function). The result of this function is a sealed classLoadResult<Key, Value>
.
LoadParams
is a sealed class, and it has three child classes:
Refresh
— which represents the initial load requestAppend
— load request which will be appended to the end of the listPrepend
— load request which will be prepended to the start of the list
By checking what is the actual type of the params we can determine what is the type of the request.
LoadResult
has three child classes too. Here’s what they are:
Error
— which represents the error resultInvalid
— which represents the invalid resultPage
— which represents the successful result
Note how we implemented these functions. In getRefreshKey
, we just returned the most recently accessed index in the list. In load
, we call the repository function to fetch the data from the API and return the result depending if the placeholders are enabled or not.
When placeholders are enabled, we will have the itemsAfter
count of null elements after the loaded data and theitemsBefore
count of null elements before the loaded data. Later, we can use those elements to show placeholders. If it is not enabled, we won’t have any null elements.
Here we will have a maximum loadedSize
of placeholders after a minimum number of items are left to load. There’s a total number of elements from the API, so for some different APIs, this logic may vary.
Next, we implement our MainViewModel
. Here’s what the code looks like:
In our ViewModel
, we create a flow of PagingData
. Pager
is a constructor for a reactive stream of PagingData
. The constructor takes in three parameters:
config: PagingConfig
— configuration of thePaging
. Takes in one mandatory and a couple of optional parameters. A mandatory parameter ispageSize: Int
which is a number of items to be loaded at once fromPagingSource
. Some of the optional parameters that are interesting areenablePlaceholders: Boolean
andjumpThreshold: Int
initialKey: Key
— initial key of thePagingSource
pagingSourceFactory: () -> PagingSource<Key, Value>
— lambda factory that should create and returnPagingSource
instance.
Pager
has one more constructor which has four parameters. The first three are the same, and the fourth parameter is remoteMediator: RemoteMediator<Key, Value>?
, which we explained earlier.
With .flow
, we create a flow of PagingData
, and with .cachedIn
, we cache the PagingData
such that any downstream collection from this flow will share the same PagingData
.
Next is to create a MainScreen
. Here’s the code:
First, we use collectAsLazyPagingItems
to collect values from the flow of PagingData
and create an instance of LazyPagingItems
. LazyPagingItems
is responsible for accessing the data from a flow of PagingData
. With this instance, we can access the load state, trigger a refresh, get item count, retry failed load requests that would result in a LoadState.Error
, and so on.
In the LazyPagingItems
instance, we have access to loadState: CombinedLoadStates
, which represents the load states of refresh
, prepend
and append
. LoadState
is a state of a PagedList
load; it can be NotLoading
, Loading
and Error
. NotLoading
has a field endOfPaginationReached: Boolean
and Error
has error: Throwable
.
Checking these states, we can choose whether should we show a loading spinner, an error message, or do nothing.
To the items
composable, we pass LazyPagingItems
and for each row, we call RecipesRow
composable.
RecipesRow
is a card that contains an image, name, and rating. If the current recipeModel
is null, that means it is a placeholder and will be the check we pass to the placeholder
modifier. If you are not familiar with Accompanist’s placeholder
, or you want to remind yourself, take a look at one of my previous articles because I won’t explain that here.
That’s it for this article. I hope you learned something new and that it was interesting. Feel free to ask questions if you have any doubts.
The source code can be found in my GitHub repo.
Want to Connect?GitHub
Portfolio website
If you want to learn more about Jetpack Compose, take a look at these articles:
- Implement Horizontal and Vertical ViewPager in Jetpack Compose
- Build a Camera Android App in Jetpack Compose Using CameraX
- Jetpack Compose Swipe To Refresh
Resources
https://developer.android.com/topic/libraries/architecture/paging/v3-overview
https://developer.android.com/reference/kotlin/androidx/paging/Pager
https://developer.android.com/reference/kotlin/androidx/paging/PagingSource
https://developer.android.com/reference/kotlin/androidx/paging/RemoteMediator