Working with Paging Library
1. Setting Up Paging Dependencies
| Dependency | Group ID | Artifact ID | Version |
|---|---|---|---|
| Paging Runtime | androidx.paging |
paging-runtime |
3.3.0 |
| Paging Common | androidx.paging |
paging-common |
3.3.0 |
| Paging Compose | androidx.paging |
paging-compose |
3.3.0 |
Example: Gradle Dependencies
implementation "androidx.paging:paging-runtime:3.3.0"
implementation "androidx.paging:paging-common:3.3.0"
implementation "androidx.paging:paging-compose:3.3.0"
Note: Ensure you are using the latest stable version of AndroidX Paging library.
2. Creating PagingSource
A PagingSource is responsible for loading data in pages from a data source such as a database or API. It defines how to load data for a specific page and how to calculate the next or previous page.
| Method | Description | Return Type |
|---|---|---|
| load() | Loads a page of data based on the provided parameters. | LoadResult |
| invalidate() | Invalidates the current page and triggers a reload. | Unit |
| getRefreshKey() | Provides a key to refresh the page at a specific position. | Int? |
Example: Basic PagingSource Implementation
class UserPagingSource(
private val api: UserService
) : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
return try {
val position = params.key ?: 0
val response = api.getUsers(position, params.loadSize)
LoadResult.Page(
data = response.users,
prevKey = if (position > 0) position - params.loadSize else null,
nextKey = if (response.hasMore) position + params.loadSize else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
return state.anchorPosition?.let { position ->
(position / state.config.pageSize) * state.config.pageSize
}
}
}
Note: The
loadSize parameter specifies how many items to load per page. It is determined by the PagingConfig and can be adjusted based on performance needs.
Warning: Ensure that
prevKey and nextKey are correctly calculated to avoid infinite loading or missing data.
3. Building Pager
Example: Basic Pager Setup
val pager = Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = true
),
pagingSourceFactory = { MyPagingSource() }
)
| Property | Description | Default Value |
|---|---|---|
| pageSize | Number of items to load per page | 20 |
| enablePlaceholders | Whether to show placeholders while loading | true |
| initialLoadSize | Initial number of items to load | pageSize * 2 |
Note:
pageSize determines how many items are fetched per network request or database query.Warning: Setting
enablePlaceholders to false can lead to a janky UI experience if not handled carefully.Example: Custom PagingConfig
val pagingConfig = PagingConfig(
pageSize = 10,
initialLoadSize = 15,
prefetchDistance = 5,
enablePlaceholders = false
)
Key Points
- Pager is the main entry point for creating a
PagingDatastream. - PagingConfig controls how many items to load and when to show placeholders.
- PagingSourceFactory provides a new instance of
PagingSourcefor each page.
4. Observing PagingData
| Property | Description | Usage |
|---|---|---|
| PagingData | Represents a stream of paged data | PagingData<T> is used to observe paged data in a stream |
| Flow<PagingData<T>> | Flow of PagingData objects | Used to observe changes in paged data over time |
Example: Observing PagingData in ViewModel
class MyViewModel : ViewModel() {
val pagingData: Flow<PagingData<User>> = repository.getUsers()
}
Example: Using PagingData in Composable
@Composable
fun UserListScreen(viewModel: MyViewModel) {
val pagingData = viewModel.pagingData.collectAsState().value
LazyColumn {
items(pagingData) { user ->
Text(text = user.name)
}
}
}
Note:
PagingData is designed to be observed using Flow or LiveData in a ViewModel.
5. Using PagingDataAdapter
The PagingDataAdapter is a powerful utility in Android's Paging Library that simplifies displaying paged data in RecyclerViews. It handles the complexity of loading and displaying pages efficiently.
| Property | Description | Values |
|---|---|---|
| PagingDataAdapter | Base class for adapting paged data to RecyclerView | PagingDataAdapter<T, VH extends RecyclerView.ViewHolder> |
| submitData() | Submits a new page of data to the adapter | suspend fun submitData(page: PagingData<T>) |
| getItemCount() | Returns the total number of items in the list | override fun getItemCount(): Int |
| onBindViewHolder() | Binds the view holder with data from a specific position | override fun onBindViewHolder(holder: VH, position: Int) |
Example: Initializing PagingDataAdapter
class UserAdapter : PagingDataAdapter<User, UserViewHolder>(UserDiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.user_item, parent, false)
return UserViewHolder(view)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
Example: Using PagingDataAdapter with RecyclerView
val adapter = UserAdapter()
recyclerView.adapter = adapter
lifecycleScope.launch {
val pagingDataFlow = userPagingSourceFactory
.asPagingSourceFactory { it }
.toFlow()
adapter.submitData(pagingDataFlow)
}
Note: The
PagingDataAdapter requires a DiffUtil.ItemCallback to efficiently update the UI when data changes.
Warning: Ensure that
PagingDataAdapter is used with a proper DiffUtil.ItemCallback to avoid UI flickering or incorrect updates.
Key Features of PagingDataAdapter
- Handles asynchronous data loading and display
- Automatically manages page transitions
- Supports efficient diffing of data changes
- Works seamlessly with
RecyclerView
6. Handling Loading States
Example: Basic Loading State with Paging 3
val pagingData = pager.flow.cachedIn(viewModelScope)
val loadState = pagingData.loadState
.map { it.refresh }
LaunchedEffect(loadState) {
loadState.collect { state ->
when (state) {
is LoadState.Loading -> {
// Show loading indicator
showLoading(true)
}
is LoadState.NotLoading -> {
// Hide loading indicator
showLoading(false)
}
is LoadState.Error -> {
// Handle error case
handleError(state.error)
}
}
}
}
| LoadState Type | Description | Usage |
|---|---|---|
| LoadState.Loading | Indicates data is being loaded or refreshed | Show progress indicator or skeleton UI |
| LoadState.NotLoading | Data loading is complete (success or error) | Hide loading indicators, show data |
| LoadState.Error | Represents an error during data loading | Display error message or retry button |
Note: The
LoadState object contains both refresh and append states, allowing you to distinguish between initial load and pagination loading.
Example: Handling Append State
val loadState = pagingData.loadState
val appendState = loadState.map { it.append }
LaunchedEffect(appendState) {
appendState.collect { state ->
when (state) {
is LoadState.Loading -> {
// Handle pagination loading
showPaginationLoading(true)
}
is LoadState.NotLoading -> {
// Pagination complete
showPaginationLoading(false)
}
is LoadState.Error -> {
// Handle pagination error
handleError(state.error)
}
}
}
}
Key Points for Loading State Management
- Use
LoadStateto track loading and error states - Separate refresh vs append states for better UX control
- Implement loading indicators to improve perceived performance
- Handle errors gracefully with user-friendly messages or retry logic
7. Implementing Retry Logic
Example: Retry Logic with PagingSource
class MyPagingSource : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
return try {
val data = api.fetchUsers(
page = params.key ?: 0,
size = params.loadSize
)
LoadResult.Page(
data = data.users,
prevKey = if (params.key == 0) null else params.key - 1,
nextKey = if (data.hasMore) params.key + 1 else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
| Method | Description | Notes |
|---|---|---|
| load() | Fetches data for a specific page | Return LoadResult.Error on failure to trigger retry logic |
| getRefreshKey() | Determines where to restart loading after a retry | Used by Paging Library to resume from the correct position |
Note: Retry logic is handled automatically by the Paging Library when
LoadResult.Error is returned. The library will attempt to reload the failed page.
Retry Behavior
- Paging Library retries failed pages automatically
- Retry attempts are based on
LoadResult.Errorfromload() - User can configure retry strategy using
PagingConfigor customRemoteMediator
8. Using RemoteMediator
The RemoteMediator class in Android's Paging Library is used to handle remote data sources, particularly when you need to fetch data from a network or database that doesn't support pagination natively. It allows you to integrate with a remote data source and manage the loading of data in a paginated manner.
| Property | Description | Values |
|---|---|---|
| load | Method to load data from the remote source. | (loadType: LoadType, pageKey: Int?): List<Item> |
| initialize | Method to initialize the mediator before loading. | () |
Example: Implementing RemoteMediator
class MyRemoteMediator(
private val api: ApiService,
private val database: AppDatabase
) : RemoteMediator<Int, MyItem>() {
override suspend fun initialize(): InitializeAction {
return InitializeAction.SKIP_INITIAL_LOAD
}
override suspend fun load(
loadType: LoadType,
currentPageKey: Int?
): MediatorResult {
return try {
val page = when (loadType) {
LoadType.REFRESH -> 1
LoadType.APPEND -> currentPageKey?.plus(1) ?: return MediatorResult.Success(false)
LoadType.PREPEND -> return MediatorResult.Success(false)
}
val response = api.fetchItems(page, 20)
val items = response.items
database.itemDao().insertAll(items)
MediatorResult.Success(endOfPaginationReached = response.items.isEmpty())
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
}
Note: The
RemoteMediator is particularly useful when integrating with APIs that don't support cursor-based pagination or when you need to handle complex data loading logic, such as caching or offline support.
9. Displaying Load State
Example: Basic Load State Handling
class UserPagingAdapter : PagingDataAdapter<User, UserViewHolder>(USER_COMPARATOR) {
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val user = getItem(position)
if (user != null) {
holder.bind(user)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
return UserViewHolder.create(parent)
}
}
class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(user: User) {
// Bind user data
itemView.nameTextView.text = user.name
}
}
| State | Description | Usage |
|---|---|---|
| LoadState.NotLoading | Represents a state where no loading is in progress | Used to display loaded data or handle successful loads |
| LoadState.Loading | Represents a state where data is being fetched | Used to show loading indicators or placeholders |
| LoadState.Error | Represents a state where an error occurred during loading | Used to display error messages or retry options |
Example: Handling LoadState in UI
class UserListFragment : Fragment() {
private val adapter = UserPagingAdapter()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.adapter = adapter
adapter.addLoadStateListener { loadState ->
when (loadState.refresh) {
is LoadState.Loading -> {
// Show loading indicator
showLoading()
}
is LoadState.Error -> {
// Handle error
showError(loadState.refresh as LoadState.Error)
}
is LoadState.NotLoading -> {
// Hide loading indicator
hideLoading()
}
}
}
}
private fun showLoading() {
// Implement loading UI
}
private fun showError(error: LoadState.Error) {
// Implement error UI with message
}
private fun hideLoading() {
// Hide loading indicator
}
}
Note: The
LoadState object contains information about the current loading state, including refresh and append states. You can use addLoadStateListener to observe changes in the load state and update your UI accordingly.10. Using Paging with Compose
Example: Basic Paging with Jetpack Compose
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
@Composable
fun UserList() {
val pagingData: PagingData<User> = Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = { UserPagingSource() }
).flow.collectAsLazyPagingItems()
LazyColumn {
items(pagingData) { user ->
UserItem(user)
}
}
}
| Property | Description | Values |
|---|---|---|
| Pager | Creates a flow of PagingData |
Pager(config, pagingSourceFactory) |
| PagingConfig | Configuration for page size and prefetch distance | pageSize = 20, prefetchDistance = 4 |
| PagingData | Represents a paginated data set | PagingData<T> |
| collectAsLazyPagingItems() | Converts PagingData to a lazy list |
LazyPagingItems<T> |
Note:
PagingData is a flow that emits new pages as they are loaded.Example: Custom Paging Source
class UserPagingSource : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
val position = params.key ?: 0
val response = api.fetchUsers(position, params.loadSize)
return LoadResult.Page(
data = response.users,
prevKey = if (position == 0) null else position - params.loadSize,
nextKey = if (response.users.isEmpty()) null else position + params.loadSize
)
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
return state.anchorPosition?.let { position - (position % 20) }
}
}