In today’s dynamic mobile development ecosystem, optimizing the user experience with smooth data loading is critical. Android’s Paging 3 library — part of Android Jetpack — empowers developers to efficiently load paginated data from various sources. Whether you're pulling data from a local database, a REST API, or a hybrid source, Paging 3 ensures better memory usage and seamless scrolling.
In this tutorial, we will walk you through each step required to implement Paging 3 in your Android application. This guide is suitable for intermediate developers, and by the end, you’ll have a fully functional paginated list with headers, footers, and reactive programming support through RxJava.
Let’s dive into the ultimate Paging 3 Android tutorial.
Introduction to Paging 3 Library
The Paging 3 library is designed to handle large datasets efficiently by loading them incrementally, reducing memory consumption, and offering a smoother UI experience. Unlike previous versions, Paging 3 is built on Kotlin coroutines and Flow but also supports RxJava and LiveData.
Its robust architecture ensures easy integration with Room, Retrofit, and remote mediators, offering out-of-the-box support for complex pagination needs.
Understanding and Implementing Paging 3 Library
Let's break down how you can implement the Paging 3 library from scratch.
Step 01. Add Dependencies
To get started, add the following dependencies in your app-level build.gradle
file:
dependencies {
implementation "androidx.paging:paging-runtime:3.2.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0"
}
For RxJava support, include:
implementation "androidx.paging:paging-rxjava3:3.2.1"
Sync your project to proceed.
Step 02. Create Data Models
Assume you’re fetching user data from an API. Here's a sample data model:
data class User(
val id: Int,
val name: String,
val email: String
)
Also, define the API response wrapper if required.
Step 03. Create a PagingSource
PagingSource is responsible for loading pages of data. Create a class that extends PagingSource
:
class UserPagingSource(
private val apiService: ApiService
) : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
val page = params.key ?: 1
return try {
val response = apiService.getUsers(page)
LoadResult.Page(
data = response.users,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.users.isEmpty()) null else page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
return state.anchorPosition?.let { anchor ->
state.closestPageToPosition(anchor)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchor)?.nextKey?.minus(1)
}
}
}
Step 04. Create a Pager and Repository
Create a repository that provides PagingData using a Pager object.
class UserRepository(private val apiService: ApiService) {
fun getUserStream(): Flow<PagingData<User>> {
return Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = { UserPagingSource(apiService) }
).flow
}
}
Step 05. Set up ViewModel with PagingData
Integrate the repository into your ViewModel
.
class UserViewModel(private val repository: UserRepository) : ViewModel() {
val users: Flow<PagingData<User>> = repository.getUserStream()
.cachedIn(viewModelScope)
}
cachedIn
ensures the paging data survives configuration changes like screen rotation.
Step 06. Implement Adapter and ViewHolder
Create a PagingDataAdapter and corresponding ViewHolder.
class UserAdapter : PagingDataAdapter<User, UserAdapter.UserViewHolder>(DIFF_CALLBACK) {
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val user = getItem(position)
holder.bind(user)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_user, parent, false)
return UserViewHolder(view)
}
class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(user: User?) {
itemView.findViewById<TextView>(R.id.userName).text = user?.name
}
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: User, newItem: User) = oldItem == newItem
}
}
}
Step 07. Connect Everything in the UI
Now, bind your adapter in the Activity
or Fragment
.
lifecycleScope.launch {
viewModel.users.collectLatest {
adapter.submitData(it)
}
}
Getting the States of the Data
adapter.addLoadStateListener { loadState ->
if (loadState.refresh is LoadState.Loading) {
// Show loading spinner
} else if (loadState.refresh is LoadState.Error) {
// Show error message
} else {
// Hide spinner
}
}
This enables you to provide feedback to users and handle empty states gracefully.
Adding the Header and Footer View
Paging 3 allows you to add headers and footers using LoadStateAdapter
.
val adapter = UserAdapter()
.withLoadStateHeaderAndFooter(
header = LoadingStateAdapter { adapter.retry() },
footer = LoadingStateAdapter { adapter.retry() }
)
This is ideal for showing a retry button on failure or a loading indicator when fetching more data.
Using it with RxJava
If your architecture is RxJava-centric, Paging 3 offers full support for RxJava3.
Step 01. Add RxJava Support
Ensure the following dependency is included:
implementation "androidx.paging:paging-rxjava3:3.2.1"
Step 02. Create RxPagingSource
Instead of Flow, return a Flowable<PagingData<T>>
class RxUserRepository(private val apiService: ApiService) {
fun getUserStream(): Flowable<PagingData<User>> {
return RxPager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = { UserPagingSource(apiService) }
).flowable
}
}
Step 03. Setup Rx Pager Flowable
Integrate it into your ViewModel
:
val userStream: Flowable<PagingData<User>> = repository.getUserStream()
.cachedIn(viewModelScope)
Step 04. Bind Data in UI
Subscribe to the RxJava stream in the Activity
:
userViewModel.userStream
.observeOn(AndroidSchedulers.mainThread())
.subscribe { pagingData ->
adapter.submitData(lifecycle, pagingData)
}
Make sure to manage the disposables properly to avoid memory leaks.
Conclusion
Paging 3 significantly improves data pagination in Android apps by offering coroutine, Flow, LiveData, and RxJava support. It ensures optimal performance even when dealing with large or frequently updating datasets. From creating a PagingSource to binding data in the UI, this tutorial covered everything you need to implement Paging 3 effectively.
If your team is planning to scale your Android development efforts or implement robust, enterprise-grade features like Paging 3, it might be time to hire Android developers who bring advanced knowledge and architectural discipline to your projects.
Unlocking high performance in your apps begins with the right tools — and the right team behind them.