Caching and Pagination with Paging 3 in Android | Tech Zen

Posted on

On this article, we’ll implement caching and paging with Paging 3. We’ll be utilizing Jetpack Compose, however you can even observe alongside and be taught from this text, even should you will not be utilizing Jetpack Compose. Apart from the UI layer, most of it will likely be comparable.

Desk of Contents

We’ll be utilizing Room, Retrofit, and Hilt on this article, so that you higher know the way they work.

I am going to additionally assume you understand the fundamentals of how Paging 3 works. Should you do not, I like to recommend testing this text earlier than this one.

utility degree construct.gradle proceedings,

//Paging 3
def paging_version = "3.1.1"
implementation "androidx.paging:paging-runtime:$paging_version"
implementation "androidx.paging:paging-compose:1.0.0-alpha17"

def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

def hilt_version = "2.44"
implementation "$hilt_version"
kapt "$hilt_version"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0"

def room_version = "2.4.3"
implementation "$room_version"
kapt "$room_version"
implementation "$room_version"
implementation "$room_version"

implementation "io.coil-kt:coil-compose:2.2.2"

Do not forget so as to add web permission in AndroidManifest.xml,

<uses-permission android:title="android.permission.INTERNET" />

We’re going to use model 3 of TheMovieDB API. You’ll be able to register and get your API key from this hyperlink. We’ll use /film/well-liked remaining level

API key

response fashions,

Put them in several recordsdata. I’ve put them in a code block to make it simpler to learn.

knowledge class MovieResponse(
val web page: Int,
@SerializedName(worth = "outcomes")
val films: Listing<Film>,
val totalPages: Int,
val totalResults: Int

@Entity(tableName = "films")
knowledge class Film(
@PrimaryKey(autoGenerate = false)
val id: Int,
@ColumnInfo(title = "original_title")
val ogTitle: String,
@ColumnInfo(title = "overview")
val overview: String,
@ColumnInfo(title = "reputation")
val reputation: Double,
@ColumnInfo(title = "poster_path")
val posterPath: String?,
@ColumnInfo(title = "release_date")
val releaseDate: String,
@ColumnInfo(title = "title")
val title: String,
@ColumnInfo(title = "web page")
var web page: Int,

That is all for this half.

Let’s begin by creating and implementing Retrofit. The API service will likely be quite simple since we’re going to use only one endpoint.

interface MoviesApiService 
droop enjoyable getPopularMovies(
@Question("web page") web page: Int
): MovieResponse

The API service is prepared, we are going to create an replace occasion on the finish of this half after ending the Room deployment.

That is it for Retrofit, now we will implement Room. Earlier than we start, we’ll must create a brand new mannequin for caching.

@Entity(tableName = "remote_key")
knowledge class RemoteKeys(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(title = "movie_id")
val movieID: Int,
val prevKey: Int?,
val currentPage: Int,
val nextKey: Int?,
@ColumnInfo(title = "created_at")
val createdAt: Lengthy = System.currentTimeMillis()

WWhen distant keys should not immediately related to checklist gadgets, it’s higher to retailer them in a separate desk within the native database. Though this may be accomplished within the Film desk, creating a brand new desk for the subsequent and former distant keys related to a Film it permits us to have a greater separation of considerations.

This mannequin is required to maintain monitor of pagination. When we have now the final ingredient loaded from the PagingState, there isn’t any option to know the index of the web page it belonged to. To resolve this downside, we added one other desk that shops the subsequent, present, and former web page keys for every film. The keys are web page numbers. createdAt it’s wanted for the cache timeout. Should you needn’t examine after we final cached knowledge, you may delete it.

Now we will create Dao for each. Film Y RemoteKeys,

interface MoviesDao
@Insert(onConflict = OnConflictStrategy.REPLACE)
droop enjoyable insertAll(films: Listing<Film>)

@Question("Choose * From films Order By web page")
enjoyable getMovies(): PagingSource<Int, Film>

@Question("Delete From films")
droop enjoyable clearAllMovies()

interface RemoteKeysDao
@Insert(onConflict = OnConflictStrategy.REPLACE)
droop enjoyable insertAll(remoteKey: Listing<RemoteKeys>)

@Question("Choose * From remote_key The place movie_id = :id")
droop enjoyable getRemoteKeyByMovieID(id: Int): RemoteKeys?

@Question("Delete From remote_key")
droop enjoyable clearRemoteKeys()

@Question("Choose created_at From remote_key Order By created_at DESC LIMIT 1")
droop enjoyable getCreationTime(): Lengthy?

Lastly, we have to create the database class.

entities = [Movie::class, RemoteKeys::class],
model = 1,
summary class MoviesDatabase: RoomDatabase()
summary enjoyable getMoviesDao(): MoviesDao
summary enjoyable getRemoteKeysDao(): RemoteKeysDao

That is it. Now we’re going to create situations of Retrofit & Room.

class SingletonModule
enjoyable provideRetrofitInstance(): MoviesApiService =

enjoyable provideMovieDatabase(@ApplicationContext context: Context): MoviesDatabase =
.databaseBuilder(context,, "movies_database")

enjoyable provideMoviesDao(moviesDatabase: MoviesDatabase): MoviesDao = moviesDatabase.getMoviesDao()

enjoyable provideRemoteKeysDao(moviesDatabase: MoviesDatabase): RemoteKeysDao = moviesDatabase.getRemoteKeysDao()

Earlier than we begin implementing, let’s attempt to perceive what Distant Mediator is and why we’d like it.

Distant Mediator acts as a sign to the paging library when the appliance has run out of cached knowledge. You need to use this token to load extra knowledge from the community and retailer it within the native database, the place a PagingSource you may load it and supply it to the UI to show.

When extra knowledge is required, the paging library calls the load() methodology of the Distant Mediator implementation. This operate usually will get the brand new knowledge from a community supply and saves it to native storage.

A Distant Mediator implementation helps load paged knowledge from the community to the database, however doesn’t load knowledge on to the person interface. As an alternative, the appliance makes use of the database as a supply of knowledge. In different phrases, the app solely shows knowledge that has been cached within the database.

Now, we will begin implementing Distant Mediator. Let’s implement half by half. First, we are going to implement load methodology.

class MoviesRemoteMediator (
non-public val moviesApiService: MoviesApiService,
non-public val moviesDatabase: MoviesDatabase,
): RemoteMediator<Int, Film>() {

override droop enjoyable load(
loadType: LoadType,
state: PagingState<Int, Film>
): MediatorResult
val web page: Int = when (loadType)
LoadType.REFRESH ->

LoadType.PREPEND ->

LoadType.APPEND ->

val apiResponse = moviesApiService.getPopularMovies(web page = web page)

val films = apiResponse.films
val endOfPaginationReached = films.isEmpty()

if (loadType == LoadType.REFRESH)

val prevKey = if (web page > 1) web page - 1 else null
val nextKey = if (endOfPaginationReached) null else web page + 1
val remoteKeys =
RemoteKeys(movieID =, prevKey = prevKey, currentPage = web page, nextKey = nextKey)

moviesDatabase.getMoviesDao().insertAll(films.onEachIndexed _, film -> film.web page = web page )

return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
catch (error: IOException)
return MediatorResult.Error(error)
catch (error: HttpException)
return MediatorResult.Error(error)


state The parameter offers us details about the pages that had been loaded earlier than, probably the most just lately accessed index within the checklist, and the PagingConfig we outline when initializing the paging circulation.

loadType tells us if we have to load knowledge on the finish (LoadType.APPEND) or firstly of the information (LoadType.PREPEND) that we beforehand loaded,
or if it’s the first time we’re loading knowledge (LoadType.REFRESH).

we are going to implement web page attribute later, so let’s begin with the attempt/catch block. First, we make an API request and get films and set up endOfPaginationReach a films.isEmpty. If there are not any gadgets left to add, we assume it’s out of inventory.

Then we begin the database transaction. Inside it, we examine if loadType is REFRESH and clear caches. After that, we create RemoteKeys by mapping films and extract Lastly, we cache all the things retrieved. films Y remoteKeys.

Now, let’s examine how we retrieve the web page quantity with RemoteKeys,

class MoviesRemoteMediator (
non-public val moviesApiService: MoviesApiService,
non-public val moviesDatabase: MoviesDatabase,
): RemoteMediator<Int, Film>()
override droop enjoyable load(
loadType: LoadType,
state: PagingState<Int, Film>
): MediatorResult
val web page: Int = when (loadType)
LoadType.REFRESH ->
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: 1

LoadType.PREPEND ->
val remoteKeys = getRemoteKeyForFirstItem(state)
val prevKey = remoteKeys?.prevKey
prevKey ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)

LoadType.APPEND ->
val remoteKeys = getRemoteKeyForLastItem(state)
val nextKey = remoteKeys?.nextKey
nextKey ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)

//Beforehand carried out

non-public droop enjoyable getRemoteKeyClosestToCurrentPosition(state: PagingState<Int, Film>): RemoteKeys?
return state.anchorPosition?.let place ->
state.closestItemToPosition(place)?.id?.let id ->

non-public droop enjoyable getRemoteKeyForFirstItem(state: PagingState<Int, Film>): RemoteKeys?
return state.pages.firstOrNull
?.knowledge?.firstOrNull()?.let film ->

non-public droop enjoyable getRemoteKeyForLastItem(state: PagingState<Int, Film>): RemoteKeys?
return state.pages.lastOrNull
?.knowledge?.lastOrNull()?.let film ->

LoadType.REFRESH, receives a name when it’s the first time we load knowledge, or when refresh() is known as.

LoadType.ANTEPEND, when we have to load knowledge to the start of the at present loaded knowledge set, the load parameter is LoadType.PREPEND.

LoadType.APPEND, when we have to load knowledge on the finish of the at present loaded knowledge set, the load parameter is LoadType.APPEND.

getRemoteKeyClosestToCurrentPositionbased mostly on anchorPosition of the state, we will get nearer Film merchandise to that place by calling closestItemToPosition and get well RemoteKeys from the database Sure RemoteKeys is null, we return the primary web page quantity which is 1 in our instance.

getRemoteKeyForFirstItemwe get the primary Film merchandise loaded from database.

getRemoteKeyForLastItem, we get the final Film merchandise loaded from database.

Lastly, let’s implement the caching timeout,

class MoviesRemoteMediator (
non-public val moviesApiService: MoviesApiService,
non-public val moviesDatabase: MoviesDatabase,
): RemoteMediator<Int, Film>()

override droop enjoyable initialize(): InitializeAction
val cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)

return if (System.currentTimeMillis() - (moviesDatabase.getRemoteKeysDao().getCreationTime() ?: 0) < cacheTimeout)


initialize this methodology is to examine if the cached knowledge is outdated and resolve whether or not to set off a distant replace. This methodology is executed earlier than any add is completed, so you may manipulate the database (for instance, to delete previous knowledge) earlier than triggering any native or distant add.

In circumstances the place native knowledge must be totally up to date, initialize I ought to return LAUNCH_INITIAL_REFRESH. This causes the Distant Mediator to carry out a distant replace to totally reload the information.

In circumstances the place there isn’t any must replace native knowledge, initialize I ought to return SKIP_INITIAL_REFRESH. This causes the Distant Mediator to skip the distant replace and cargo the cached knowledge.

In our instance, we set the timeout to 1 hour and retrieve the cache time from RemoteKeys database.

That is it. you will discover the RemoteMediator code right here, you can even discover the complete code on the finish of this text.

That is going to be a easy one,

const val PAGE_SIZE = 20

class MoviesViewModel @Inject constructor(
non-public val moviesApiService: MoviesApiService,
non-public val moviesDatabase: MoviesDatabase,
): ViewModel()
enjoyable getPopularMovies(): Stream<PagingData<Film>> =
config = PagingConfig(
pageSize = PAGE_SIZE,
prefetchDistance = 10,
initialLoadSize = PAGE_SIZE,
pagingSourceFactory =
remoteMediator = MoviesRemoteMediator(

That is much like making a Pager from a easy community knowledge supply, however there are two issues it is advisable to do otherwise:

As an alternative of spending a PagingSource constructor immediately, it’s essential to present the question methodology that returns a PagingSource dao object.

You should present an occasion of your RemoteMediator implementation just like the remoteMediator parameter.

The pagingSourceFactory lambda ought to at all times return a brand new one PagingSource when invoked as PagingSource situations should not reusable.

Lastly, we will begin to implement the UI layer.

checklist configuration

The implementation of the checklist will likely be quite simple,

enjoyable MainScreen() {
val moviesViewModel = hiltViewModel<MoviesViewModel>()

val films = moviesViewModel.getPopularMovies().collectAsLazyPagingItems()

LazyColumn {
gadgets = films
) { film ->
film?.let {
horizontalArrangement = Association.Middle,
verticalAlignment = Alignment.CenterVertically,
if (film.posterPath != null)
var isImageLoading by bear in mind mutableStateOf(false)

val painter = rememberAsyncImagePainter(
mannequin = "" + film.posterPath,

isImageLoading = when(painter.state)
is AsyncImagePainter.State.Loading -> true
else -> false

Field (
contentAlignment = Alignment.Middle
modifier = Modifier
.padding(horizontal = 6.dp, vertical = 3.dp)
painter = painter,
contentDescription = "Poster Picture",
contentScale = ContentScale.FillBounds,

if (isImageLoading)
modifier = Modifier
.padding(horizontal = 6.dp, vertical = 3.dp),
coloration = MaterialTheme.colours.main,

Textual content(
modifier = Modifier
.padding(vertical = 18.dp, horizontal = 8.dp),
textual content = it.title


For an in depth rationalization of the checklist implementation, you may seek advice from this hyperlink.

checklist UI

Loading and error dealing with

enjoyable MainScreen() {
val moviesViewModel = hiltViewModel<MoviesViewModel>()

val films = moviesViewModel.getPopularMovies().collectAsLazyPagingItems()

LazyColumn {
//... Film gadgets

val loadState = films.loadState.mediator
merchandise loadState?.append is LoadState.Error)
val isPaginatingError = (loadState.append is LoadState.Error)


Since we’re utilizing Distant Mediator, we are going to use loadState.mediator. we’ll simply examine refresh Y append,

When refresh is LoadState.Loading we are going to present the loading display screen.

refresh Loading State

When append is LoadState.Loading we are going to present the pagination load.

add Loading

For errors, we examine if refresh both append is LoadState.Error. If we have now an error in refresh meaning we received an error within the preliminary search and can present an error display screen. If we have now an error in append meaning we received an error whereas paginating and we are going to present the error on the finish of the checklist.

Let’s have a look at the top end result.

That is it! I hope you may have been useful. 👋👋

full code

MrNtlu/JetpackCompose-PaginationCaching (


Caching and Pagination with Paging 3 in Android