I have a list of items from API. I'm using Paging3 with RemoteMedatior to maintain network with local caching in database using Room. My issue is when data is loading recyclerView looks messy and glitchy, items appear in the middle of the list and above visible ones, and recyclerView doesn't scroll to the top of the list. Looks like this.
I've tried with various values for pageSize, maxSize, and prefetchDistance for pageConfig but it doesn't help.
setHasFixedSize on recylerview also doesn't work.
Flow is collected with collectLatest in the fragment. I've tried also normal collect and changing flow into liveData. Same result. Any idea how to correct this?
load function in RemoteMediator
@ExperimentalPagingApi
class RecipesMediator(
private val recipesApi: RecipesApi,
private val appDatabase: RecipesDatabase,
private val queries: HashMap<String, String>,
) : RemoteMediator<Int, Recipe>() {
override suspend fun load(loadType: LoadType, state: PagingState<Int, Recipe>): MediatorResult {
val queryId = queries[QUERY_DIET] + queries[QUERY_MEAL]
val page = when (loadType) {
LoadType.REFRESH -> 0
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> appDatabase.remoteKeysDao().remoteKeysRecipeId(queryId).nextKey
}
queries[QUERY_OFFSET] = page.toString()
queries[QUERY_NUMBER] = "20"
try {
val response = recipesApi.getRecipes(queries)
appDatabase.withTransaction {
if (loadType == LoadType.REFRESH) {
appDatabase.remoteKeysDao().clearRemoteKeys()
appDatabase.recipesDao().clearRecipes()
}
val nextKey = page + 20
val keys = response.recipes.map {
RemoteKeys(query = queryId, nextKey = nextKey)
}
appDatabase.remoteKeysDao().insertAll(keys)
appDatabase.recipesDao().insertAllRecipes(response.recipes)
}
return MediatorResult.Success(endOfPaginationReached = response.recipes.isEmpty())
} catch (exception: IOException) {
return MediatorResult.Error(exception)
} catch (exception: HttpException) {
return MediatorResult.Error(exception)
}
}
I use string for remote key ids, because when i used item id from API = LoadType.Append was called many times and only first page was loaded. Its explained in this issue
nextPage is "page + 20" because this API (Spoonacular) doesn't provide pagination, although provides parameters offset (number of results to skip) and number(number of expected results), so i used them for my paging.
UI states in fragment
recipesAdapter.addLoadStateListener { loadState ->
binding.apply {
recipesProgressBar.isVisible = loadState.mediator?.refresh is LoadState.Loading
recipesProgressBar.isVisible = loadState.source.refresh is LoadState.Loading
recipesRecyclerView.isVisible =loadState.mediator?.refresh is LoadState.NotLoading
recipesRecyclerView.isVisible =loadState.source.refresh is LoadState.NotLoading
buttonRecipesRetry.isVisible =loadState.mediator?.refresh is LoadState.Error
buttonRecipesRetry.isVisible =loadState.source.refresh is LoadState.Error
textViewRecipesError.isVisible =loadState.mediator?.refresh is LoadState.Error
textViewRecipesError.isVisible =loadState.source.refresh is LoadState.Error
if (loadState.source.refresh is LoadState.NotLoading &&
loadState.mediator?.refresh is LoadState.NotLoading &&
loadState.append.endOfPaginationReached &&
recipesAdapter.itemCount < 1
) {
recipesRecyclerView.isVisible = false
textViewEmpty.isVisible = true
} else {
textViewEmpty.isVisible = false
}
}
}
Remote Keys
@Entity(tableName = "remoteKeys")
data class RemoteKeys
(@PrimaryKey val query: String, val nextKey: Int)
Remote Keys Dao
@Dao
interface RemoteKeysDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(remoteKey: List<RemoteKeys>)
@Query("SELECT * FROM remoteKeys WHERE `query` = :query")
suspend fun remoteKeysId(query: String): RemoteKeys
@Query("DELETE FROM remoteKeys")
suspend fun clearRemoteKeys()
}