Paging Library for Android With Kotlin: Creating Infinite Lists
In this tutorial, you’ll build a simple Reddit clone that loads pages of information gradually into an infinite list using Paging 3.0 and Room. By Harun Wangereka.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Paging Library for Android With Kotlin: Creating Infinite Lists
30 mins
- Getting Started
- Defining a PagingSource
- Fetching Reddit Posts From the Network
- Fetching Posts From the PagingSource
- Configuring Your ViewModel to Fetch Data
- Using a PagingData Adapter
- Displaying Data in Your UI
- Enabling Key Reuse
- Displaying the Loading State
- Breaking Free of Network Dependency
- Creating a RemoteMediator
- Adding RemoteMediator to Your Repository
- Fetching Previous and Next Pages With RemoteMediator
- Paging 3.0 Remarks
- Where to Go From Here?
Breaking Free of Network Dependency
At this point, the app is firing on all cylinders. But there’s one problem — it’s dependent on the network to run. If you have no internet access, the app is doomed!
Next up, you are going to use the Room database library for persisting/caching the list so it is functional even when offline.
Navigate to database/dao package and create a new interface name RedditPostsDao.kt with the following code:
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy.REPLACE
import androidx.room.Query
import com.raywenderlich.android.redditclone.models.RedditPost
@Dao
interface RedditPostsDao {
@Insert(onConflict = REPLACE)
suspend fun savePosts(redditPosts: List<RedditPost>)
@Query("SELECT * FROM redditPosts")
fun getPosts(): PagingSource<Int, RedditPost>
}
For the most part, this is a standard Dao object in Room. But there’s one unique thing here: The return type for getPosts is a PagingSource. That’s right, Room can actually create the PagingSource for you! Room will generate one that uses an Int key to pull RedditPost objects from the database as you scroll.
Now, navigate to database/RedditDatabase.kt. Add the following code to the class:
abstract fun redditPostsDao(): RedditPostsDao
Here, you’re adding an abstract method to return RedditPostsDao.
At this point, you have methods for inserting and fetching posts. Now it’s time to add the logic to fetch posts from Reddit API and insert them into your Room database.
Creating a RemoteMediator
A RemoteMediator is like a PagingSource class. But RemoteMediator does not display data to a RecyclerView. Instead, it uses the database as a single source of truth. You fetch data from the network and save it to the database.
Navigate to repositories/RedditRemoteMediator.kt. You’ll see the following code:
@OptIn(ExperimentalPagingApi::class)
class RedditRemoteMediator(
private val redditService: RedditService,
private val redditDatabase: RedditDatabase
) : RemoteMediator<Int, RedditPost>() {
override suspend fun load(
// 1
loadType: LoadType,
// 2
state: PagingState<Int, RedditPost>
): MediatorResult {
TODO("not implemented")
}
}
This class extends RemoteMediator and overrides the load(). You’re going to add the logic to fetch data from the Reddit API and save it to a database. The arguments for this method are different from those in PagingSource:
-
LoadType, is anenumthat represents the loading type. It can have any of these values:- REFRESH indicates it’s a new fetch.
-
PREPEND indicates that content is being added at the start of the
PagingData. -
APPEND indicates that content is being added at the end of the
PagingData.
-
PagingState. This takes a key-value pair, where the key has typeIntand the value has typeRedditPost.
Replace the TODO() with the following code:
return try {
// 1
val response = redditService.fetchPosts(loadSize = state.config.pageSize)
// 2
val listing = response.body()?.data
val redditPosts = listing?.children?.map { it.data }
// 3
if (redditPosts != null) {
redditDatabase.redditPostsDao().savePosts(redditPosts)
}
// 4
MediatorResult.Success(endOfPaginationReached = listing?.after == null)
// 5
} catch (exception: IOException) {
MediatorResult.Error(exception)
} catch (exception: HttpException) {
MediatorResult.Error(exception)
}
Here is what is happening in above code block:
- This is a call to the Reddit API where you use the
pageSizefrom the state parameter to fetch the data from the network. - Get the list of posts from the response body
- If there are posts returned from the API, you save the list in the database.
- If the network response is successful, you set the return type of the method to
MediatorResult.Success. You also passendOfPaginationReached, a Boolean variable that indicates when you are at the end of the list. In this case, the list ends whenafteris null. Notice thatRemoteMediatoruses the sealed classMediatorResultto represent the state of the data fetch. - Finally, you handle any exceptions that may occur during the loading operation and pass them to the Paging library.
Notice that RemoteMediator uses the sealed class MediatorResult to represent the state of the data fetch.
Next, you’ll modify your repository class to use RedditRemoteMediator in order to start using Room for persisting the list of posts.
Adding RemoteMediator to Your Repository
Navigate to repositories/RedditRepo.kt. Add the following code to the class body:
private val redditDatabase = RedditDatabase.create(context)
This creates an instance of the Room database using the create(). You’ll use it with RemoteMediator.
Next, replace the complete fetchPosts() with the following code:
@OptIn(ExperimentalPagingApi::class)
fun fetchPosts(): Flow<PagingData<RedditPost>> {
return Pager(
PagingConfig(
pageSize = 40,
enablePlaceholders = false,
// 1
prefetchDistance = 3),
// 2
remoteMediator = RedditRemoteMediator(redditService, redditDatabase),
// 3
pagingSourceFactory = { redditDatabase.redditPostsDao().getPosts() }
).flow
}
Here’s a breakdown of code above:
- As part of your paging configuration, you add
prefetchDistancetoPagingConfig. This parameter defines when to trigger the load of the next items within the loaded list. - You set
RedditRemoteMediator, which you created earlier.RedditRemoteMediatorfetches the data from the network and saves it to the database. - Finally, you set
pagingSourceFactory, in which you call the Dao to get your posts. Now your database serves as a single source of truth for the posts you display, whether or not you have a network connection.
You don’t have to modify the ViewModel or the activity layer, since nothing has changed there! That’s the benefit of choosing a good architecture. You can swap the data source implementations without modifying other layers in your app.
RemoteMediator API is currently experimental and needs to be marked as OptIn via the @OptIn(ExperimentalPagingApi::class) annotation in the classes using it.
Now, build and run. You'll see the following screen:
As you scroll, you'll notice that your app only fetches one page. Why went wrong? Well, no worries! In the next section, you're going to fix it.
Fetching Previous and Next Pages With RemoteMediator
In PagingSource, you passed the before and after keys to LoadResult. But you're not doing this in RedditRemoteMediator. That's why the app currently fetches only one page.
To scroll continuously, you have to tell Room how to fetch the next and previous pages from the network when it reaches to the start or end of the current page. But how do you do this, when you're not passing the keys to MediatorResult?
To achieve this, you're going to save the keys in the Room database after every network fetch. Navigate to database/dao and create a new interface named RedditKeysDao.kt with below code:
@Dao
interface RedditKeysDao {
@Insert(onConflict = REPLACE)
suspend fun saveRedditKeys(redditKey: RedditKeys)
@Query("SELECT * FROM redditKeys ORDER BY id DESC")
suspend fun getRedditKeys(): List<RedditKeys>
}
This is a standard Dao object in Room with two methods for saving and retrieving the keys.
Next, go back to database/RedditDatabase.kt. In your RedditDatabase class, append the below line of code:
abstract fun redditKeysDao(): RedditKeysDao
redditKeysDao() is used to get access to the RedditKeysDao so that you can access the keys in the database easily.
Now navigate to repositories/RedditRemoteMediator.kt. Replace the body of load() with the following code:
return try {
// 1
val loadKey = when (loadType) {
LoadType.REFRESH -> null
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
state.lastItemOrNull()
?: return MediatorResult.Success(endOfPaginationReached = true)
getRedditKeys()
}
}
// 2
val response = redditService.fetchPosts(
loadSize = state.config.pageSize,
after = loadKey?.after,
before = loadKey?.before
)
val listing = response.body()?.data
val redditPosts = listing?.children?.map { it.data }
if (redditPosts != null) {
// 3
redditDatabase.withTransaction {
redditDatabase.redditKeysDao()
.saveRedditKeys(RedditKeys(0, listing.after, listing.before))
redditDatabase.redditPostsDao().savePosts(redditPosts)
}
}
MediatorResult.Success(endOfPaginationReached = listing?.after == null)
} catch (exception: IOException) {
MediatorResult.Error(exception)
} catch (exception: HttpException) {
MediatorResult.Error(exception)
}
Here is what is happening in above code block:
- You fetch the Reddit keys from the database when the
LoadTypeis APPEND. - You set the
afterandbeforekeys infetchPosts. - Finally, you create a database transaction to save the keys and the posts you retrieved is there response returned a lits of posts
Next, add a new getRedditKeys() to the RedditRemoteMediator.kt class body:
private suspend fun getRedditKeys(): RedditKeys? {
return redditDatabase.redditKeysDao().getRedditKeys().firstOrNull()
}
This is a suspend method that fetches RedditKeys from the database. Notice that you're using firstOrNull(). This will return the first items in the list. If there are no items in the database, it returns null.
Build and run, and voila! Your endless scrolling list is back.
You now have an endlessly scrolling app that works whether or not you have a network connection!

