Paging 3 With Offline Support ( Room )

Paging 3 With Offline Support ( Room )

 

References :

Paging 3 Remote Mediator Concept Tutorial | Offline Support - CheezyCode | Hindi
Android Paging 3 Tutorial in Hindi - Learn Paging 3 in Android with simple example. In this video, we will learn how to implement Remote Mediator in Paging 3. Learn how remote mediator will work, learn how to define remote keys table to manage keys inside room database. In this video, we have implemented required setup of Room Database for Paging. Learn how paging 3 manages pages using Room. Learn how to handle different states - PREPEND, APPEND, REFRESH. We have implemented it using HILT and MVVM Architecture Pattern. Learn by creating a simple example in Hindi. Topics covered - 1. Paging 3 Offline Support 2. Paging 3 with Room DB Support 3. Remote Mediator Concept 4. MVVM + HILT + Paging 3 Jetpack Component. Complete Dependency Injection Playlist Link - https://www.youtube.com/playlist?list=PLRKyZvuMYSIPwjYw1bt_7u7nEwe6vATQd Complete Android Architecture Components Playlist - https://www.youtube.com/playlist?list=PLRKyZvuMYSIO0jLgj8g6sADnD0IBaWaw2 Beginner series in Android Playlist (Hindi) - https://www.youtube.com/playlist?list=PLRKyZvuMYSIN9sVZTfDm4CTdTAzDQyLJU Kotlin Beginners Tutorial Series - https://www.youtube.com/playlist?list=PLRKyZvuMYSIMW3-rSOGCkPlO1z_IYJy3G For more info - visit the below link http://www.cheezycode.com API Used - https://quotable.io/ Source Code - https://github.com/CheezyCode/Android-Paging-3-Demo We are social. Follow us at - Facebook - http://www.facebook.com/cheezycode Twitter - http://www.twitter.com/cheezycode Instagram - https://www.instagram.com/cheezycode/
Paging 3 Remote Mediator Concept Tutorial | Offline Support - CheezyCode | Hindi
 
Paging 3 Remote Mediator Implementation Tutorial | Offline Support - CheezyCode | Hindi
Android Paging 3 Tutorial in Hindi - Learn Paging 3 in Android with simple example. In this video, we will learn how to implement Remote Mediator in Paging 3. Learn how remote mediator will work, learn how to define remote keys table to manage keys inside room database. In this video, we have implemented required setup of Room Database for Paging. Learn how paging 3 manages pages using Room. Learn how to handle different states - PREPEND, APPEND, REFRESH. Learn how to calculate the current page based on the loadType. Using paging state you can calculate the page number. For append, you can get the last page's last record to get the next key. For prepend, you can use first page in paging state for previous key. Refresh and First Time Load is similar - it uses Anchor Position to evaluate the current page number. We have implemented it using HILT and MVVM Architecture Pattern. Learn by creating a simple example in Hindi. Topics covered - 1. Paging 3 Offline Support 2. Paging 3 with Room DB Support 3. Remote Mediator Concept 4. Remote Mediator Implementation 5. MVVM + HILT + Paging 3 Jetpack Component. Jetpack Paging Tutorial - https://www.youtube.com/playlist?list=PLRKyZvuMYSIPci119n2gt_kq1GU-PAYRk Complete Dependency Injection Playlist Link - https://www.youtube.com/playlist?list=PLRKyZvuMYSIPwjYw1bt_7u7nEwe6vATQd Complete Android Architecture Components Playlist - https://www.youtube.com/playlist?list=PLRKyZvuMYSIO0jLgj8g6sADnD0IBaWaw2 Beginner series in Android Playlist (Hindi) - https://www.youtube.com/playlist?list=PLRKyZvuMYSIN9sVZTfDm4CTdTAzDQyLJU Kotlin Beginners Tutorial Series - https://www.youtube.com/playlist?list=PLRKyZvuMYSIMW3-rSOGCkPlO1z_IYJy3G For more info - visit the below link http://www.cheezycode.com API Used - https://quotable.io/ Codelab Link - https://developer.android.com/codelabs/android-paging?hl=en#0 Source Code - https://github.com/CheezyCode/PagingWithRemoteMediator We are social. Follow us at - Facebook - http://www.facebook.com/cheezycode Twitter - http://www.twitter.com/cheezycode Instagram - https://www.instagram.com/cheezycode/
Paging 3 Remote Mediator Implementation Tutorial | Offline Support - CheezyCode | Hindi

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