pagination seems to work well, but when disconnect the internet and call adapter.retry everything works as expected, except that the recycler view flicks,
here's a video https://youtube.com/shorts/9Fw9VyEPGLE?feature=share
I followed android paging codelab to the detail, I just adapted somethings to match my api, nytimes api for movies.
when just scrolling the pagination works as expected, but if i for exemple the user loses connection while scrolling, when/if he reaches the end of the list and tries to reload then the recycler view flicks, but the new load of movies is added perfectly where it should be, so I don't know why the recycler view flickers.
My code;
RemoteMediator
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Movie>
): MediatorResult {
val page = when(loadType) {
LoadType.APPEND -> {
val remoteKeys = getRemoteKeyForLastItem(state)
val nextKey = remoteKeys?.nextKey
if (nextKey == null) {
return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
}
nextKey
}
LoadType.PREPEND -> {
val remoteKeys = getRemoteKeyForFirstItem(state)
val prevKey = remoteKeys?.prevKey
if (prevKey == null) {
return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
}
prevKey
}
LoadType.REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: STARTING_PAGE_INDEX
}
}
try {
val response = api.fetchMovieCatalog(offset).toMovieCatalog()
offset+=20
val movies = response.movieCatalog
val endOfPaginationReached = movies.isEmpty()
db.withTransaction {
if (loadType == LoadType.REFRESH) {
db.remoteKeysDao.clearRemoteKeys()
db.moviesDao.clearMovies()
}
val prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1
val keys = movies.map {
RemoteKeys(movie = it.title, prevKey = prevKey, nextKey = nextKey)
}
db.remoteKeysDao.insertAll(keys)
db.moviesDao.insertMovies(movies)
}
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (e: IOException) {
return MediatorResult.Error(e)
} catch (e: HttpException) {
return MediatorResult.Error(e)
}
}
private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, Movie>): 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 { movie ->
// Get the remote keys of the last item retrieved
Log.e("TAG", "Key - ${db.remoteKeysDao.remoteKeysMovieId(movie.title)}, last item")
db.remoteKeysDao.remoteKeysMovieId(movie.title)
}
}
private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, Movie>): 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 { movie ->
// GEt the remote keys of the first items retrieved
Log.e("TAG", "Key - ${db.remoteKeysDao.remoteKeysMovieId(movie.title)}, first item")
db.remoteKeysDao.remoteKeysMovieId(movie.title)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(state: PagingState<Int, Movie>): 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)?.title?.let { movie ->
Log.e("TAG", "Key - ${db.remoteKeysDao.remoteKeysMovieId(movie)}, refresh")
db.remoteKeysDao.remoteKeysMovieId(movie)
}
}
}
}
Fragment
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentMovieCatalogBinding.inflate(inflater, container, false)
binding.recyclerView.addItemDecoration(
DividerItemDecoration(
context,
DividerItemDecoration.VERTICAL
)
)
binding.recyclerView.itemAnimator = DefaultItemAnimator()
binding.bindState(
uiAction = viewModel.accept,
uiState = viewModel.state,
pagingData = viewModel.pagingDataFLow
)
return binding.root
}
private fun FragmentMovieCatalogBinding.bindState(
uiAction: (UiAction) -> Unit,
pagingData: Flow<PagingData<UiModel>>,
uiState: StateFlow<UiState>
) {
val adapter =
MovieCatalogAdapter(requireActivity())
val header = MoviesLoadStateAdapter {
adapter.retry()
}
val footer = MoviesLoadStateAdapter {
adapter.retry()
}
recyclerView.adapter = adapter.withLoadStateHeaderAndFooter(
header = header,
footer = footer
)
bindList(
adapter, pagingData, header
)
}
private fun FragmentMovieCatalogBinding.bindList(
adapter: MovieCatalogAdapter,
pagingData: Flow<PagingData<UiModel>>,
header: MoviesLoadStateAdapter
) {
swipeRefresh.setOnRefreshListener { adapter.refresh() }
lifecycleScope.launch {
pagingData.collectLatest(adapter::submitData)
}
lifecycleScope.launch {
adapter.loadStateFlow.collect { loadState ->
header.loadState = loadState.mediator
?.refresh
?.takeIf { it is LoadState.Error && adapter.itemCount > 0 }
?: loadState.prepend
// show empty list.
emptyList.isVisible =
loadState.refresh is LoadState.NotLoading && adapter.itemCount == 0
// Only show the list if refresh succeeds.
recyclerView.isVisible =
loadState.source.refresh is LoadState.NotLoading || loadState.mediator?.refresh is LoadState.NotLoading
// show progress bar during initial load or refresh.
swipeRefresh.isRefreshing = loadState.mediator?.refresh is LoadState.Loading
}
}
}