References :
Step 1: Implementation of Library into Project:
First add paging 3 library in app gradle app.gradle file.
plugins { id ("kotlin-android") id ("kotlin-parcelize") id ("kotlin-kapt") } // ... implementation ("androidx.paging:paging-runtime-ktx:3.1.0") //optional to handle Serializable implementation ("com.squareup.retrofit2:converter-gson:2.5.0")
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<QuoteResult>, @SerializedName("totalCount") val totalCount: Int?, @SerializedName("totalPages") val totalPages: Int? )
data class QuoteResult( @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, QuoteResult>() { // logic to load data - how to load page override suspend fun load(params: LoadParams<Int>): LoadResult<Int, QuoteResult> { 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, QuoteResult>( 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, QuoteResult>): 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) } } }
Step 4 : Create RemoteMediator
@ExperimentalPagingApi class QuoteRemoteMediator @Inject constructor( private val quoteApi: QuoteApi, private val quoteDatabase: QuoteDatabase, ) : RemoteMediator<Int, QuoteResult>() { private val quoteDao = quoteDatabase.getQuoteDao() private val remoteKeyDao = quoteDatabase.getRemoteKeyDao() override suspend fun load( loadType: LoadType, state: PagingState<Int, QuoteResult>, ): MediatorResult { // step 1 fetch quotes from api // step 2 save these quotes + remoteKeys data into database // step 3 logic for state - REFRESH , PREPEND , APPEND return try { val currentPage = when (loadType) { // Refresh -> when first load or data invalidate LoadType.REFRESH -> { val remoteKeys = CoroutineScope(Dispatchers.IO).async { getRemoteKeyClosestToCurrentPosition(state) }.await() remoteKeys?.nextPage?.minus(1) ?: 1 } // Prepend -> when scroll up LoadType.PREPEND -> { val remoteKeys = CoroutineScope(Dispatchers.IO).async { getRemoteKeyForFirstItem(state) }.await() val prevPage = remoteKeys?.prevPage ?: return MediatorResult.Success( endOfPaginationReached = remoteKeys != null ) prevPage } // Append -> When Scroll Down LoadType.APPEND -> { val remoteKeys = CoroutineScope(Dispatchers.IO).async { getRemoteKeyForLastItem(state) }.await() val nextPage = remoteKeys?.nextPage ?: return MediatorResult.Success( endOfPaginationReached = remoteKeys != null ) nextPage } } // hit api val response = quoteApi.getQuotes(currentPage) // is last page == true val endOfPaginationReached = response.totalPages == currentPage val prevPage = if (currentPage == 1) null else currentPage - 1 val nextPage = if (endOfPaginationReached) null else currentPage + 1 quoteDatabase.withTransaction { // if refresh then first clear data from database to load new data if (loadType == LoadType.REFRESH) { quoteDao.deleteQuotes() remoteKeyDao.deleteRemoteKeys() } // store response in room database quoteDao.addQuotes(response.quoteResults) val keys = response.quoteResults.map { quote -> QuoteRemoteKey( id = quote.id, prevPage = prevPage, nextPage = nextPage ) } remoteKeyDao.insertRemoteKey(remoteKeys = keys) } MediatorResult.Success(endOfPaginationReached) } catch (e: Exception) { MediatorResult.Error(e) } } private suspend fun getRemoteKeyClosestToCurrentPosition( state: PagingState<Int, QuoteResult>, ): QuoteRemoteKey? { return state.anchorPosition?.let { position -> state.closestItemToPosition(position)?.id?.let { id -> remoteKeyDao.getRemoteKeys(id = id) } } } private suspend fun getRemoteKeyForFirstItem( state: PagingState<Int, QuoteResult>, ): QuoteRemoteKey? { return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull() ?.let { quote -> remoteKeyDao.getRemoteKeys(id = quote.id) } } private suspend fun getRemoteKeyForLastItem( state: PagingState<Int, QuoteResult>, ): QuoteRemoteKey? { return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull() ?.let { quote -> remoteKeyDao.getRemoteKeys(id = quote.id) } } }
Step 5 : Repository
The QuoteRepository class is used to create data sources from the network.
// Pager is second component for paging // here we will define Pager @ExperimentalPagingApi class QuoteRepository @Inject constructor( private val quoteApi: QuoteApi, private val quoteDatabase: QuoteDatabase, ) { // 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 ), // remote mediator object need to pass to get data for database remoteMediator = QuoteRemoteMediater(quoteApi, quoteDatabase), // here instead pulling data from api we are pulling from database pagingSourceFactory = { quoteDatabase.getQuoteDao().getQuotes() } ).liveData // to expose our data we have .livedata or .flow // so it will return data in form of Livedata or Flow }
Step 6 : ViewModel
@ExperimentalPagingApi @HiltViewModel class QuoteViewModel @Inject constructor( private val quoteRepository: QuoteRepository, ) : ViewModel() { // it will return live data that we will store here // in paging we need to cache our data for that it has one method. // it can cache our Livedata of PagingData inside cache memory using coroutine scope // benefit : performance val list = quoteRepository.getQuotes().cachedIn(viewModelScope) }
Step 7 : 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<QuoteResult, QuotePagingAdapter.QuoteViewHolder>(diffCallback = Comparator()) { class QuoteViewHolder(private val binding: ItemQuoteLayoutBinding) : RecyclerView.ViewHolder(binding.root) { fun onBind(item: QuoteResult) { 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 QuoteResult 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<QuoteResult>() { override fun areItemsTheSame(oldItem: QuoteResult, newItem: QuoteResult): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: QuoteResult, newItem: QuoteResult): Boolean { return oldItem == newItem } } }
Step 8 : Loading Adapter in fragment or Activity
Finally, we need to get the data from the
QuotePagingAdapter() and set the data into the recyclerview. @ExperimentalPagingApi @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) } } }