Pagination with Paging3

01 / May / 2023 by Reyansh Jatoliya 0 comments

During the development journey, we may require to show unlimited data as no of users of a particular mobile application grows, so does the data associated with that application.

Consider the image application, the user scrolls to the bottom of the screen and wants to be able to fetch more data from the server to display it, but what if the user scrolls up the screen at the same time? As a result, we will have to deal with a large number of cases and error handling.

We require an approach to do this task that loads all data efficiently from the local database or network, allows caching, reduces resource utilization, and also saves development time.

Paging does this work for you. Paging is a Jetpack library that manages and load data efficiently from different data source; it is compatible with Kotlin and works with threading solutions i.e. Flow, Coroutine, etc. It is designed to follow the Android app architecture. Also, it supports RxJava and LiveData.

Paging3 & Application architecture:

Paging3 uses the Android app architecture basic layer like repository->View Model -> Ui component.

Paging Source: Paging source is a generic abstract class that takes two type page keys Type and Response Type.

Pager: This Api consumes a Paging source or remote mediator data source and returns a stream of paged data; it can be flow, Observable, or Live Data.

PagingDataAdapter: This is a UI component that is responsible for presenting paged data in the recycler view; it has some additional methods to manage the header and footer at runtime.

Paging config: Here, you can define how much data it load for each page.

Let us code and achieve it

Add the required dependency in app level gradle:

implementation ‘androidx.paging:paging-runtime:3.1.1’

Image Repository:

class ImageRepository(
private val apiService: ApiService = RemoteInjector.injectApiService(),
) {
companion object {
const val DEFAULT_PAGE_INDEX = 1
const val DEFAULT_PAGE_SIZE = 20
fun getInstance() = ImageRepository()

fun letImagesFlow(pagingConfig: PagingConfig = getDefaultPageConfig()): Flow<PagingData<ImageModel>> {
return Pager(
config = pagingConfig,
pagingSourceFactory = { ImagePagingSource(apiService) }

private fun getDefaultPageConfig(): PagingConfig {
return PagingConfig(pageSize = DEFAULT_PAGE_SIZE, enablePlaceholders = false)

Image Paging Source:

class ImagePagingSource(var apiService: ApiService) : PagingSource<Int, ImageModel>() {

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ImageModel> {
val page = params.key ?: DEFAULT_PAGE_INDEX
return try {
val response = apiService.getImages(page, params.loadSize)

response, prevKey = if (page == DEFAULT_PAGE_INDEX) null else page - 1,
nextKey = if (response.isEmpty()) null else page + 1

} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)

override fun getRefreshKey(state: PagingState<Int, ImageModel>): Int? {


Images View Model:

class ImagesViewModel(private val repository: ImageRepository = ImageRepository.getInstance()) :
ViewModel() {
fun fetchImages(): Flow<PagingData<String>> {
return repository.letImagesFlow()
.map { it -> { it.url } }

Images Adapter:
class ImagesAdapter :
PagingDataAdapter<String, UserViewHolder>(object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem

override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): UserViewHolder {
val inflater = LayoutInflater.from(parent.context)

return UserViewHolder(inflater.inflate(R.layout.item_image_view, parent,false))

override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val item = getItem(position)


class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(url: String?) {
val image = itemView.findViewById<ImageView>(

Images Fragment:

class ImagesFragment : Fragment() {

private lateinit var viewModel: ImagesViewModel
private var _binding: FragmentImagesBinding? = null
private lateinit var adapter: ImagesAdapter
private lateinit var loaderAdapter: LoaderStateAdapter

private val binding get() = _binding!!

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentImagesBinding.inflate(inflater, container, false)
return binding.root


override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
viewModel.fetchImages().distinctUntilChanged().collectLatest {

override fun onDestroyView() {
_binding = null

private fun init() {
viewModel = defaultViewModelProviderFactory.create(
adapter = ImagesAdapter()
loaderAdapter = LoaderStateAdapter()

private fun setupUi() {
val manager = GridLayoutManager(context, 2)

binding.rvImages.layoutManager = manager
binding.rvImages.adapter = adapter.withLoadStateFooter(loaderAdapter)


If you run the above code, it will produce output like below:

If you are facing some use cases, feel free to comment and clap if you enjoyed.

Find the complete code on Github.

Thank you for reading, and Happy Coding!



Leave a Reply

Your email address will not be published. Required fields are marked *