Reputation: 882
I have swipeToRefreshLayout outside of a recyclerview. I added a refresh Listener on this layout and called paging adapter refresh method.
binding.swipe.setOnRefreshListener {
feedsAdapter?.refresh()
binding.swipe.isRefreshing= false
}
But my app crashes with the error
java.lang.IllegalStateException: The same value, 2, was passed as the prevKey in two sequential Pages loaded from a PagingSource.
Error suggest me to override keyReuseSupported which when I do get wrong list Of feeds and out-of-sync too. My PagingSource code of both overrided method with STARTING_KEY = 0
override fun getRefreshKey(state: PagingState<Int, FeedsModel>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition = anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, FeedsModel> {
val pageIndex = params.key ?: STARTING_KEY
return try {
val response = restApi.getAllFeeds(pageIndex, NETWORK_PAGE_SIZE)
val feedsDetails = response.response?.toDomainFeed() ?: emptyList()
val nextKey = if (feedsDetails.isEmpty()) {
null
} else {
// pageIndex + (params.loadSize/NETWORK_PAGE_SIZE)
pageIndex + NETWORK_PAGE_SIZE
}
LoadResult.Page(
data = feedsDetails,
prevKey = if (pageIndex == STARTING_KEY) null else pageIndex,
nextKey = nextKey
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
}
and the code for the mediator's Load method
val page = when(loadType){
LoadType.APPEND ->{
val remoteKeys = getRemoteKeyForLastItem(state)
val nextKey = remoteKeys?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys!=null)
nextKey
}
LoadType.PREPEND ->{
val remoteKeys = getRemoteKeyForFirstTime(state)
val prevKey = remoteKeys?.prevKey
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys!=null)
prevKey
}
LoadType.REFRESH ->{
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: STARTING_KEY
}
}
try {
val apiResponse = restApi.getAllFeeds(page,state.config.pageSize)
val feeds = apiResponse.response ?: emptyList()
val paginationEndReached = feeds.isEmpty()
db.withTransaction {
if(loadType == LoadType.REFRESH){
db.remoteKeysDao().clearRemoteKeys()
db.feedsDaoInterface().clearDB()
}
val prevKey = if(page == STARTING_KEY) null else page -1
val nextKey = if(paginationEndReached) null else page +1
val keys = feeds.map {
RemoteKeys(feedsId = it.feedId,prevKey, nextKey)
}
db.remoteKeysDao().insertAll(keys)
db.feedsDaoInterface().insertAllFeeds(*feeds.toDatabasesModel())
}
return MediatorResult.Success(paginationEndReached)
private suspend fun getRemoteKeyForLastItem(state : PagingState<Int,FeedsModel>) : RemoteKeys?{
// Get the last page that was retrieved, that contained items.
// From that last page, get the last item
return state.pages.lastOrNull() { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { repo ->
// Get the remote keys of the last item retrieved
db.remoteKeysDao().remoteKeysRepoId(repo.feedId)
}
}
private suspend fun getRemoteKeyForFirstTime(state : PagingState<Int,FeedsModel>) : RemoteKeys?{
// Get the first page that was retrieved, that contained items.
// From that first page, get the first item
return state.pages.firstOrNull(){
it.data.isNotEmpty()
}?.data?.firstOrNull()
?.let {
db.remoteKeysDao().remoteKeysRepoId(it.feedId)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, FeedsModel>
): RemoteKeys? {
//by just returning null here My code works for the first time but second time it crashes...
return null
/*return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.feedId?.let { id ->
db.remoteKeysDao().remoteKeysRepoId(id)
}
}*/
}
ViewModel code
@OptIn(ExperimentalPagingApi::class)
fun getFeeds() = Pager(
config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false) ,
remoteMediator = FeedsMediator(restApi,myDatabase)
,pagingSourceFactory = {
repository.feedsPagingSource()
}
)
.flow
.cachedIn(viewModelScope)
When user swipe to refresh feeds, I want to load new feeds thats has been added to the List in the backend by other users (in the backend ,feeds are added at start of the list according to the created time so Latest feeds will always be at start of the list ) but my crashes. User can also update the feeds in the recyclerview. So in that scenario also, i will call adapter's refresh method and i suspect that it will crash at that time too. I have never used pager ever before. I followed google's code lab and adjusted code to my needs.
Upvotes: 0
Views: 348
Reputation: 882
I found the problem, I was mixing two concepts PagingSource and Mediator into one. Paging works on single source of truth mechanism and while configuring the pager, I provided both my own implementation of PagingSource and Mediator. The data which I was getting from the is from PagingSource and I was adjusting the code in Mediator to fix the error. Once I changed my pagingSource 's code of overridden method getRefreshKey to below code.
override fun getRefreshKey(state: PagingState<Int, FeedsModel>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition = anchorPosition)?.prevKey?.plus(state.config.pageSize)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(state.config.pageSize)
}
}
The error was gone. So I got to know that My RemoteMediator codes can work too. I removed the my own implementation of pagingSource and used pagingSource provided by the paging3 api in the dao as
@Query("SELECT * FROM ${DatabaseFeedsModel.FEEDS_TABLE}")
fun getAllFeeds() : PagingSource<Int,DatabaseFeedsModel>
I adjusted Mediator's code
LoadType.REFRESH ->{
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(**state.config.pageSize**) ?: STARTING_KEY
}
and
val prevKey = if(page == STARTING_KEY) null else page - state.config.pageSize
val nextKey = if(paginationEndReached) null else page + state.config.pageSize
also instead of just returning null in the method get getRemoteKeyClosestToCurrentPosition
private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, DatabaseFeedsModel>
): RemoteKeys? {
// The paging library is trying to load data after the anchor position
// Get the item closest to the anchor position
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.feedId?.let { id ->
db.remoteKeysDao().remoteKeysRepoId(id)
}
}
}
I finally managed to run the app without crash as well as working swipe refresh.
Upvotes: 0