Skip to main content

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 PagingData stream.
  • PagingConfig controls how many items to load and when to show placeholders.
  • PagingSourceFactory provides a new instance of PagingSource for 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 LoadState to 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.Error from load()
  • User can configure retry strategy using PagingConfig or custom RemoteMediator

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) }
    }
}

Key Concepts

  • PagingSource provides data to the Pager
  • PagingData is a flow that emits pages as they load
  • LazyPagingItems is used in Compose to display data
  • Pager uses PagingConfig to control page size and prefetching

Was this article helpful?