Paging 3 keeps doing api calls without reaching the end of recyclerview - android

Paging 3 keeps doing api calls without reaching the end of recyclerview
i tired to change page size to 15 but still the same
does using base paging source could lead to any problem?
this is the XML of the view
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/stl"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:descendantFocusability="blocksDescendants"
android:paddingStart="8dp"
android:paddingTop="8dp"
android:paddingEnd="8dp"
android:paddingBottom="80dp" />
base adapter
abstract class BasePagingAdapter<T : Any, VH : BasePagingAdapter.BaseViewHolder<T>>(diffCallback: DiffUtil.ItemCallback<T>) :
PagingDataAdapter<T, VH>(diffCallback) {
override fun onBindViewHolder(holder: VH, position: Int) {
getItem(position).let { data -> holder.bindData(data!!) }
}
abstract class BaseViewHolder<T>(view: View) : RecyclerView.ViewHolder(view) {
abstract fun bindData(data: T)
}
fun ViewGroup.inflateView(layoutRes: Int): View =
LayoutInflater.from(this.context).inflate(layoutRes, this, false)
}
my adapter
class OrdersPagingAdapter(val onCancelClick: (Order) -> Unit) :
BasePagingAdapter<Order, BasePagingAdapter.BaseViewHolder<Order>>(
OrdersPagingComparator
) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BaseViewHolder<Order> =
OrdersViewHolder(parent.inflateView(R.layout.item_order))
}
base paging source
open class BasePagingSource<T : Any>(
val call: suspend (Int) -> Response<BasePagingResponse<T>>
) :
PagingSource<Int, T>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, T> {
return try {
val nextPageNumber = params.key ?: 1
val response: Response<BasePagingResponse<T>> =
call(nextPageNumber)
if (response.code() == 200) {
LoadResult.Page(
data = response.body()?.data!!,
prevKey = null,
nextKey = if (response.body()?.currentPage == response.body()?.totalPages) null
else
response.body()?.currentPage!! + 1
)
} else {
LoadResult.Page(
data = emptyList(),
prevKey = null,
nextKey = null
)
}
} catch (e: IOException) {
return LoadResult.Error(e)
} catch (e: HttpException) {
return LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, T>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
my repository call I tries to chang initial page size but still not working
fun getOrders() = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false
), pagingSourceFactory = { OrdersPagingSource() }
).flow

Under your PagingSource class, change the logic to this. nextKey should be null if reach the end of the page.
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ProgressHistory> {
return try {
val position = params.key ?: 1
val response = apiService.getProgressHistory(jsonObject = payload, page = position)
val nextKey = if (response.code() != 200) {
null
} else {
position + 1
}
LoadResult.Page(data = response.body()!!.data!!,
prevKey = if (position == 1) null else position - 1,
nextKey = nextKey)
} catch (e: Exception) {
LoadResult.Error(e)
}
}

Related

How to write unit tests for paging library in android

I need to write unit tests for doing a paginated network request. First I implemented paging library logic to retrieve the data.
I created a data source class and a repository class to get data from the network request.
This is my data source class
class ListDataSource(
private val networkService: NetworkService,
private val searchKey: String) : PagingSource<Int, ListItem>() {
override fun getRefreshKey(state: PagingState<Int, ListItem>): Int? {
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, ListItem> {
val pageNumber: Int = params.key ?: 0
return try {
val response = networkService.getList(
searchTerm = searchKey,
page = pageNumber,
size = 30
)
val listItems = response.response?.list
val nextKey = listItems?.let { nonNullList ->
if (nonNullList.size < 30) {
null
} else {
pageNumber + 1
}
} ?: run {
null
}
LoadResult.Page(
data = listItems.orEmpty(),
prevKey = if (pageNumber == 0) null else pageNumber - 1,
nextKey = nextKey
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}}
This is my repository class
class ListPagingRepository(private val service: NetworkService) {
private lateinit var pager: Pager<Int, ListItem>
private lateinit var pagingSource: ListDataSource
fun getListPager(): Pager<Int, ListItem> {
return pager
}
fun isPagerInitialized(): Boolean = this::pager.isInitialized
fun createSource(searchTerm: String) {
pagingSource = ListDataSource(service, searchTerm)
}
fun createPager() {
pager = Pager(
config = PagingConfig(
initialLoadSize = 15,
pageSize = 15,
enablePlaceholders = false,
prefetchDistance = 2
),
pagingSourceFactory = { pagingSource }
)
}}
Inside my viewmodel I the function to do the network call is:
fun getPaginatedList(searchTerm: String): Flow<PagingData<ListItem>> {
listPagingRepository.createSource(searchTerm)
listPagingRepository.createPager()
return if (listPagingRepository.isPagerInitialized()) {
listPagingRepository
.getListPager()
.flow
.cachedIn(viewModelScope)
.map { pagingData -> pagingData.map { listMapper.map(it) } }
} else emptyFlow()
}
How can I test this network request?
Searched for 2 days but nothing that I found helped me.
You should test your implementation, with mocked network responses. An example of such a test with MockK as the mocking framework :
#Test
fun `verify error return from load() when a (types of responses you expect to potentially receive) response is returned` () = runTest {
val mockNetworkService = mockk<NetworkService>(relaxed = true, relaxedUnitFun = true)
val testParams = LoadParams(1, false)
every { mockNetworkService.getList(any(), any(), any()) } returns LoadResponse.Error(YourExpectedException)
val result = ListDataSource(mockNetworkService, "test search").load(testParams)
assertEquals(LoadResponse.Error(YourExpectedException), result)
}

Is it possible to display chat messages through the Android Paging Library?

I want to merge two message list(stored chat message, incoming chat message)
these two data is split based on the last check time.
I want stored chat message to be Paging and merge with incoming data
This is my code:
#Query("SELECT * FROM msgentity WHERE room_id =:roomId and time <= :lastReadTime ORDER BY time")
fun getPassedMsg(roomId: String, lastReadTime: Date) : PagingSource<Int, MsgEntity>
#Query("SELECT * FROM msgentity WHERE room_id=:roomId and time > :lastReadTime ORDER BY time")
suspend fun getNewMsg(roomId: String, lastReadTime: Date) : Flow<List<MsgEntity>>
Is it possible to merge these two items of data?
Or do I have to make my own paging object?
Yeah definitely, you can show messages using paging.
if your question is that how ?
So here I am using MVVM architecture , Kotlin coroutine
Step 1. you need to create a paging source for request and getting response
(here MessageList is api response model )
class MessagePaging(private val apiService: ApiService) : PagingSource<Int, MessageList.Data>() {
private var STARTING_PAGE_INDEX = 1
override fun getRefreshKey(state: PagingState<Int, MessageList.Data>): Int? {
return 1
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MessageList.Data> {
val page = params.key ?: STARTING_PAGE_INDEX
return try {
val response = apiService.getMessageList(page)
val data = response.body()?.data!!
if (!data.isNullOrEmpty()){
LoadResult.Page(
data = data,
prevKey = if (page == STARTING_PAGE_INDEX) null else page -1,
nextKey = if (data.isEmpty()) null else page + 1
)
}else{
return LoadResult.Error(NullPointerException("Data is Null or Empty"))
}
} catch (e : HttpException){
return LoadResult.Error(e)
} catch (e : IOException){
return LoadResult.Error(e)
} catch (e : Exception){
return LoadResult.Error(e)
}
}
}
Step 2 :- Create request in apiservice
#GET( GET_CONSULTATION_LIST )
suspend fun getMessageList(#Query("page") page : Int) : Response<MessageList>
Step 3:- in ApiHelperImpl interface
fun getConsultationList(): Flow<PagingData<ConsultationList.Data>> {
return Pager(
config = PagingConfig(
pageSize = 40,
enablePlaceholders = false,
prefetchDistance = 1
),
pagingSourceFactory = { ConsultantPaging(apiService) }
).flow
}
Step 4 :- In View model
fun getConsultationList() = apiHelper.getConsultationList().cachedIn(viewModelScope)
Step 5 :- In Fragment
private fun observeConsultations() {
lifecycleScope.launch {
viewModel.getConsultationList().collectLatest {
launch(Dispatchers.Main) {
adapter.loadStateFlow.collectLatest { loadStates ->
if (loadStates.refresh is LoadState.Loading) {
// loader.show()
} else {
// loader.dismiss()
if (loadStates.refresh is LoadState.Error) {
if (adapter.itemCount < 1) {
binding.clNoConsult.visibility = View.VISIBLE
} else {
binding.clNoConsult.visibility = View.GONE
}
}
}
}
}
adapter.submitData(it)
}
}
}
Step 6:- Create Paging Adapter
class ConsultationPagingAdapter (private val click: GetClicksOnItem ) : PagingDataAdapter<MessageList.Data, ConsultationPagingAdapter.ViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ListConsultationsBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val result = getItem(position)!!
holder.binding.apply {
tvName.text = result.name.capitalizeWords()
root.setOnClickListener {
click.action(result)
}
executePendingBindings()
}
}
class ViewHolder(val binding: ListConsultationsBinding) : RecyclerView.ViewHolder(binding.root)
private class DiffCallback : DiffUtil.ItemCallback<MessageList.Data>() {
override fun areItemsTheSame(
oldItem: MessageList.Data,
newItem: MessageList.Data
): Boolean = oldItem == newItem
override fun areContentsTheSame(
oldItem: MessageList.Data,
newItem: MessageList.Data
): Boolean = oldItem == newItem
}
interface GetClicksOnItem {
fun action(data: MessageList.Data)
}
}
If you getting understating then approve the answer

Paging3 with RemoteMediator stuck in page 3 and constantly calling APPEND even when idle

I am trying to use Paging 3 for my list. At first i use the PagingSource with only network data source and it is working properly. Then i tried to implement offline caching with RemoteMediator, but i couldnt get it work. There is 2 problem i experience with this:
It constantly calling LoadType.APPEND even when the list doesnt get
scrolled (idle)
It stucks on the page number 3 so i can not load other pages
other than the initial 3
Here is my RemoteMediator code:
#ExperimentalPagingApi
class ProductRemoteMediator(
private val remoteDataSource: ProductRemoteDataSource,
private val localDataSource: ProductLocalDataSource,
) : RemoteMediator<Int, ProductEntity>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, ProductEntity>
): MediatorResult {
val page = when (loadType) {
REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: STARTING_PAGE_INDEX
}
PREPEND -> {
val remoteKeys = getRemoteKeyForFirstItem(state)
val prevKey = remoteKeys?.prevKey
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
prevKey
}
APPEND -> {
val remoteKeys = getRemoteKeyForLastItem(state)
val nextKey = remoteKeys?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
nextKey
}
}
return try {
// i know from this log and interceptor that it calls constantly and infinitely
Timber.d("loadType=$loadType")
Timber.d("page=$page, size=${state.config.pageSize}")
val products = remoteDataSource.getProductsAsBuyer(
page = page,
size = state.config.pageSize,
).map { it.mapToEntityModel() }
val endOfPaginationReached = products.isEmpty()
Timber.d("endOfPaginationReached=$endOfPaginationReached")
localDataSource.cacheProductTransaction {
// clear all tables in the database
if (loadType == REFRESH) {
localDataSource.clearRemoteKeys()
localDataSource.clearCachedProducts()
}
val prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1
val keys = products.map {
RemoteKeys(productId = it.id, prevKey = prevKey, nextKey = nextKey)
}
localDataSource.insertRemoteKeys(keys)
localDataSource.cacheAllProducts(products)
}
MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (exception: IOException) {
MediatorResult.Error(exception)
} catch (exception: HttpException) {
MediatorResult.Error(exception)
} catch (exception: Exception) {
MediatorResult.Error(exception)
}
}
private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, ProductEntity>): RemoteKeys? {
return state.pages
.lastOrNull { it.data.isNotEmpty() }
?.data
?.lastOrNull()
?.let { repo ->
localDataSource.getRemoteKeysId(repo.id)
}
}
private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, ProductEntity>): RemoteKeys? {
return state.pages
.firstOrNull { it.data.isNotEmpty() }
?.data
?.firstOrNull()
?.let { repo ->
localDataSource.getRemoteKeysId(repo.id)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, ProductEntity>
): RemoteKeys? {
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.id?.let { repoId ->
localDataSource.getRemoteKeysId(repoId)
}
}
}
}
And here is my Pager in the repository
override fun getProductsAsBuyer(): Flow<PagingData<ProductEntity>> {
return Pager(
config = PagingConfig(pageSize = NETWORK_PAGE_SIZE),
remoteMediator = ProductRemoteMediator(
remoteDataSource = remoteDataSource,
localDataSource = localDataSource,
),
pagingSourceFactory = {
localDataSource.getCachedProducts()
}
).flow
}
Any help is appreciated.
Additional Info: i use Jetpack Compose and collectAsLazyPagingItems() to display it.

Android Paging 3 leading to duplicate rows

I was trying out the Paging 3.0.1 version. The API calls are happening right when I printed the log. But the data shown is duplicate. Could someone tell me where I went wrong?
Page data source class
class MyPageDataSource(private val api: RetrofitInstance) :
PagingSource<Int, APIDataResponse>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, APIDataResponse> {
return try {
val nextPageNumber = params.key ?: FIRST_PAGE_NUMBER
val response = api.getData(nextPageNumber, PAGE_SIZE)
LoadResult.Page(
data = response.APIS!!,
prevKey = if (nextPageNumber > FIRST_PAGE_NUMBER) nextPageNumber - 1 else null,
nextKey = if (nextPageNumber * PAGE_SIZE < response.total!!) nextPageNumber + 1 else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, APIDataResponse>): Int? {
return state.anchorPosition
}
companion object {
const val FIRST_PAGE_NUMBER = 1
const val PAGE_SIZE = 20
}
}
Adapter:
class MyListingAdapter() : PagingDataAdapter<APIDataResponse, MyListingAdapter.MyViewHolder>(MyComparator) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
FragmentItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(item = getItem(position))
}
inner class MyViewHolder(binding: FragmentItemBinding) :
RecyclerView.ViewHolder(binding.root) {
private val title: TextView = binding.title
fun bind(item: APIDataResponse?) {
if(item != null) {
title.text = item.title
}
}
}
object MyComparator : DiffUtil.ItemCallback<APIDataResponse>() {
override fun areItemsTheSame(
oldItem: APIDataResponse,
newItem: APIDataResponse
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(
oldItem: APIDataResponse,
newItem: APIDataResponse
): Boolean {
return oldItem == newItem
}
}
}
View Model:
class PagingViewModel : ViewModel() {
fun getData() : Flow<PagingData<APIDataResponse>> {
return Pager(
PagingConfig(
pageSize = 20,
enablePlaceholders = false,
maxSize = 40,
initialLoadSize = 20,
prefetchDistance = 10
)
) {
MyPageDataSource(RetrofitInstance())
}.flow.cachedIn(viewModelScope)
}
}
Recycler view setting up in the fragment:
val myAdapter = MyListingAdapter(myActivity)
//Setup the recyclerview
binding.myList.apply {
layoutManager = when {
columnCount <= 1 -> LinearLayoutManager(context)
else -> GridLayoutManager(context, columnCount)
}
myAdapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
val decoration =
DividerItemDecoration(myActivity, DividerItemDecoration.VERTICAL)
addItemDecoration(decoration)
setHasFixedSize(true)
adapter = myAdapter
}
lifecycleScope.launch {
viewModel.getData().distinctUntilChanged().collectLatest { pagedData ->
myAdapter.submitData(pagedData)
}
}
I had the same problem.
It solved by setting the correct pageSize.
This problem can also happen by setting the wrong maxSize, initialLoadSize or other attributes.
You have set setHasFixedSize(true) it means data won't change because of a change in the adapter content. For example, the RecyclerView size can change because of a size change on its parent. Maybe that's why you are getting the same records. try to remove it and then check it works or not.

pagination not working when reaches to end of page in paging 3 android?

VideoStatusDataSource.kt
class VideoStatusDataSource(
private val categoryKey: String,
private val videosStatusApi: VideoStatusApiService
) : PagingSource<Int, VideoStatus>() {
companion object {
private const val VIDEO_STARTING_PAGE_INDEX = 0
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, VideoStatus> {
return try {
val pageIndex = params.key ?: VIDEO_STARTING_PAGE_INDEX
logger(params.key)
logger(pageIndex)
val response =
videosStatusApi.getVideoStatusByPageNumberAndCategoryName(pageIndex, categoryKey)
val jsonCategoryResponse = response.getAsJsonArray(DATA_KEY)
val videoStatusList: List<VideoStatus> = Gson().fromJson(jsonCategoryResponse)
LoadResult.Page(
data = videoStatusList.orEmpty(),
prevKey = if (pageIndex == VIDEO_STARTING_PAGE_INDEX) null else pageIndex - 1,
nextKey = if (videoStatusList.isEmpty()) null else pageIndex.plus(1)
)
} catch (exception: IOException) {
LoadResult.Error(exception)
} catch (exception: HttpException) {
LoadResult.Error(exception)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, VideoStatus>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
video-API service is providing 10 results on each page but with this data source class it loads all data at once
I want only the first 10 items to load initially and then use scroll first 10 items it needs to load the next 10 items
here is my paging data repository
MainRepopsitory.kt
fun getVideoStatusPagingData(categoryKey: String): Pager<Int, VideoStatus> =
Pager(
config = PagingConfig(
pageSize = 10
),
pagingSourceFactory = { VideoStatusDataSource(categoryKey, videosStatusApi) }
)
ViewModel
#HiltViewModel
class PagingViewModel #Inject constructor(
private val mainRepository: MainRepository,
#IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : ViewModel() {
fun getCurrentCategoryVideoStatus(categoryKey: String): Flow<PagingData<VideoStatus>> =
mainRepository
.getVideoStatusPagingData(categoryKey)
.flow.cachedIn(viewModelScope)
.flowOn(ioDispatcher)
}
This is how I'm using load function in my paging sources you can get help from this I have five to six paging sources in my app and all have same implementations like this
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Message> {
return try {
val nextPage = params.key ?: 0
chatWithSellerRequest.offset = nextPage.times(PAGE_SIZE_LIMIT)
val response = apiService.getSellerChatResponse(chatWithSellerRequest)
_chatWithSellerResultResponse.value = response.chatWithSellerResult
LoadResult.Page(
data = response.chatWithSellerResult?.messages!!,
prevKey = null,
nextKey = if (response.chatWithSellerResult.messages.isEmpty()) null else nextPage + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}

Categories

Resources