Better Programming

Advice for programmers.

Follow publication

Jetpack Compose Pagination

Implementing pagination using the Paging 3 library in Jetpack Compose

Igor Stevanovic
Better Programming
Published in
5 min readOct 14, 2022

--

The Jetpack Compose logo used in this image is the official logo created by Google

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 next PagingSource due to the invalidation of this PagingSource. The key is provided to the load via LoadParams.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 the PagingData 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 the getRefreshKey function). The result of this function is a sealed class LoadResult<Key, Value>.

LoadParams is a sealed class, and it has three child classes:

  • Refresh — which represents the initial load request
  • Append — load request which will be appended to the end of the list
  • Prepend — 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 result
  • Invalid — which represents the invalid result
  • Page — 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 the Paging. Takes in one mandatory and a couple of optional parameters. A mandatory parameter is pageSize: Int which is a number of items to be loaded at once from PagingSource. Some of the optional parameters that are interesting are enablePlaceholders: Boolean and jumpThreshold: Int
  • initialKey: Key — initial key of the PagingSource
  • pagingSourceFactory: () -> PagingSource<Key, Value> — lambda factory that should create and return PagingSource 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: Booleanand 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
LinkedIn
Twitter

Portfolio website

If you want to learn more about Jetpack Compose, take a look at these articles:

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

--

--

Igor Stevanovic
Igor Stevanovic

Written by Igor Stevanovic

Android Engineer, Freelancer and Writer

No responses yet

Write a response