Paging 3 with retrofit

Paging 3 with retrofit

 
 
 
  • Paging 3 helps when working with RecyclerViews in our Android project to display a list with a lot of items in it. Sometimes we have use-case like fetching a huge list of songs list. Loading a lot of data at once in a list is not a very efficient task to do. To get over this, we have paging in Android.

What is Paging?

Paging a technique to use huge no of data to display as per user requirement of whenever needed which is efficient rather than fetching this many items at once and create view components for it. It will requires too much network bandwidth (if data is coming remotely) and it will also use too much system resources to display that huge data set at once.
  • Example : if most API can send thousands of data in but in form of pages or limit the the response because website or app can use page section from it. - It similar to next page in website at bottom even it is same page but data will be changed but in app new data will be fetched as old will be out of user screen.
 
  • Benefit : user are not going to see all data at once from application so don’t need to fetch all data unnecessary by using peging we will fetch small amount first then as user need it will fetch more data from server and display it to user as needed. also old data will be discarded from memory and new data will be assigned so memory will hold only limited amount data in it.
 

Paging Library Components

To implement paging functionality in our android app, we will use the following Paging library components:
This demographic workflow will be about on Paging 3 Library in Android
notion image

Benefits of using the Paging 3 library

  • Provides in-memory caching of the paged data that assures the systematic use of device resources.
  • Paging 3 library support coroutine flow
  • List separators (Decoration with Item )
  • Prevents duplication of the API request, ensuring that your app uses network bandwidth and system resources efficiently.
  • Configurable RecyclerView adapters automatically request data as the user scrolls toward the end of the loaded data.
  • Finest support for Kotlin coroutines and Flow, as well as LiveData and RxJava.
  • Built-in functionality to add loading state headers, footers, and list separators.
  • Built-in support for error handling, including refresh and retry capabilities.

Implement Paging 3 Library in Android Kotlin

The below process will guide you that how to implement Paging 3 in your
application.
notion image

Step 1: Implementation of Library into Project:

First add paging 3 library in app gradle app.gradle file.
implementation ("androidx.paging:paging-runtime-ktx:3.1.0") //optional to handle Serializable implementation ("com.squareup.retrofit2:converter-gson:2.5.0") plugins { id ("kotlin-android") id ("kotlin-parcelize") id ("kotlin-kapt") } // for other dependency check main page

Step 2: Generating Data Models

Now, we have to create the data class for the api response to handle and display output.
data class Quotelist( @SerializedName("count") val count: Int?, @SerializedName("lastItemIndex") val lastItemIndex: Int?, @SerializedName("page") val page: Int?, @SerializedName("results") val results: List<Result>, @SerializedName("totalCount") val totalCount: Int?, @SerializedName("totalPages") val totalPages: Int? )
data class Result( @SerializedName("author") val author: String?, @SerializedName("authorSlug") val authorSlug: String?, @SerializedName("content") val content: String?, @SerializedName("dateAdded") val dateAdded: String?, @SerializedName("dateModified") val dateModified: String?, @SerializedName("_id") val id: String?, @SerializedName("length") val length: Int?, @SerializedName("tags") val tags: List<String?>? )

Step 3: Integrate Source File

To get the data from backend API, we need a PagingSource.
package com.example.paging3withroomdemo.paging import androidx.paging.PagingSource import androidx.paging.PagingState import com.example.paging3withroomdemo.data.model.Result import com.example.paging3withroomdemo.data.retrofit.QuoteApi // Int refers to page number // Result is model class for response // we are loading data from api so we provide reference for it. class QuotePagingSource(private val quoteApi: QuoteApi) : PagingSource<Int, Result>() { // logic to load data - how to load page override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Result> { return try { // params.key will store current page number // if current key null then refer to 1 val position = params.key ?: 1 val response = quoteApi.getQuotes(position) LoadResult.Page<Int, Result>( data = response.results, // List<Result> // if position on first page then no prev key prevKey = if (position == 1) null else position - 1, // if position on last page then no next key nextKey = if (position == response.totalPages) null else position + 1 ) } catch (e: Exception) { // if something went wrong then we pass error LoadResult.Error(e) } } // if pagingSource lost current page key it will help to find next key to load data from override fun getRefreshKey(state: PagingState<Int, Result>): Int? { // anchorPosition hold value for last page visited but it can be null so we use null check if not null then we proceed return state.anchorPosition?.let { anchorPosition -> // it will find closest page from last page encountered val anchorPage = state.closestPageToPosition(anchorPosition) anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) } } }
  • Another PagingSource Example
class PassengerItemDataSource (private val dataRepository: DataRepository):PagingSource<Int, Passenger>() { companion object { private const val STARTING_PAGE_INDEX = 1 } override fun getRefreshKey(state: PagingState<Int, Passenger>): Int? { // We need to get the previous key (or next key if previous is null) of the page // that was closest to the most recently accessed index. // Anchor position is the most recently accessed index return state.anchorPosition?.let { anchorPosition-> state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) } } override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Passenger> { val position = params.key ?: STARTING_PAGE_INDEX return when(val result = dataRepository.getPassengerListForPage(position, params.loadSize)){ is Resource.Failure -> LoadResult.Error(Exception(result.toString())) is Resource.Loading -> LoadResult.Error(Exception()) is Resource.Success -> { LoadResult.Page(data = result.data.passengerList, prevKey = if(position == STARTING_PAGE_INDEX) null else -1, nextKey = if (result.data.passengerList.isEmpty()) null else position + 1) } } } }
Now used the seal class to manage the response and other error and loading case.
sealed class Resource { val data:PassengerListResponse = PassengerListResponse(100, 50, listOf<Passenger>()) class Failure : Resource() class Loading : Resource() class Success : Resource() }

Step 4: Repository

The QuoteRepository class is used to create data sources from the network.
// Pager is second component for paging // here we will define Pager class QuoteRepository @Inject constructor(val quoteApi: QuoteApi) { // we need repository to provide data // we will get data from paging Source // pager will interact with paging source fun getQuotes() = Pager( // one page has 20 records and it will hold maximum 100 records in memory then it will drop old config = PagingConfig(pageSize = 20, maxSize = 100), pagingSourceFactory = { QuotePagingSource(quoteApi) } ).liveData // to expose our data we have .livedata or .flow // so it will return data in form of Livedata or Flow }

Step 5: Create List Adapter Class

Normally, RecyclerView uses RecyclerView.Adapter or ListAdapter but for Paging 3 we use PagingDataAdapter but the behaviour is like a normal adapter.
PagingDataAdapter takes a DiffUtil callback, as a parameter to its primary constructor which helps the PagingDataAdapter to update the items if they are changed or updated. And DiffUtil callback is used because they are more performant.
// we will create adapter like normal RecyclerView Adapter but here it will be PagingDataAdapter<>() // our adapter will hold Comparator using constructor // we will define type of data objects and ViewHolder inside this class type class QuotePagingAdapter : PagingDataAdapter<Result, QuotePagingAdapter.QuoteViewHolder>(diffCallback = Comparator()) { class QuoteViewHolder(private val binding: ItemQuoteLayoutBinding) : RecyclerView.ViewHolder(binding.root) { fun onBind(item: Result) { binding.apply { quoteTv.text = item.content } } } override fun onBindViewHolder( holder: QuoteViewHolder, position: Int, payloads: MutableList<Any>, ) { if (payloads.isNullOrEmpty()) { super.onBindViewHolder(holder, position, payloads) } else { val newItem = payloads[0] as Result holder.onBind(newItem) } } override fun onBindViewHolder(holder: QuoteViewHolder, position: Int) { // getItem() will provide current data object based on position holder.onBind(getItem(position)!!) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuoteViewHolder { return QuoteViewHolder( ItemQuoteLayoutBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } class Comparator : DiffUtil.ItemCallback<Result>() { override fun areItemsTheSame(oldItem: Result, newItem: Result): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: Result, newItem: Result): Boolean { return oldItem == newItem } } }

Step 6: Loading Adapter in fragment or Activity

Finally, we need to get the data from the PagingDataAdapter and set the data into the recyclerview. We will collect the data from the getMovieList() function inside the lifecycleScope and update the data we fetched from the API and display it in the UI.
@AndroidEntryPoint class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val myViewModel: QuoteViewModel by viewModels() private lateinit var quoteAdapter: QuotePagingAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) quoteAdapter = QuotePagingAdapter() binding.apply { quotesRv.apply { layoutManager = LinearLayoutManager(this@MainActivity, LinearLayoutManager.VERTICAL, false) adapter = quoteAdapter setHasFixedSize(true) } } myViewModel.list.observe(this) { // here we need to pass lifecycle quoteAdapter.submitData(lifecycle = lifecycle, pagingData = it) } } }