Since version beta01
of Paging 3, when refreshing the PagingData from a RemoteMediator
, it sometimes happens that the old APPEND
request from the previous generation is still executed after the refresh has completed. This seems to be expected behavior reading from this commit.
When this happens, the old APPEND
request calls the RemoteMediator
's load
method but with an outdated PagingState
. This outdated PagingState
can cause bugs and crashes if we are using the information in it in the load function (for example, the code snippet below uses lastItemOrNull
to find the RemoteKeys
for an item in the database). This breaking change (which also breaks the corresponding codelab) is not mentioned in the release notes at all. How are we supposed to handle this?
Here is an example of a RemoteMediator that breaks with beta01
. The getRemoteKeyForLastItem
method can return null
(because the old PagingState
is looking for a database entry that was deleted in the previous REFRESH
) causing the InvalidObjectException
to be thrown.
private const val GITHUB_STARTING_PAGE_INDEX = 1
@OptIn(ExperimentalPagingApi::class)
class GithubRemoteMediator(
private val query: String,
private val service: GithubService,
private val repoDatabase: RepoDatabase
) : RemoteMediator<Int, Repo>() {
override suspend fun load(loadType: LoadType, state: PagingState<Int, Repo>): MediatorResult {
val page = when (loadType) {
LoadType.REFRESH -> GITHUB_STARTING_PAGE_INDEX
LoadType.PREPEND -> return MediatorResult.Success(true)
LoadType.APPEND -> {
// this can run with an outdated PagingState from the previous RemoteMediator instance, causing the Exception to be thrown
val remoteKeys = getRemoteKeyForLastItem(state)
if (remoteKeys == null || remoteKeys.nextKey == null) {
throw InvalidObjectException("Remote key should not be null for $loadType")
}
remoteKeys.nextKey
}
}
val apiQuery = query + IN_QUALIFIER
try {
val apiResponse = service.searchRepos(apiQuery, page, state.config.pageSize)
val repos = apiResponse.items
val endOfPaginationReached = repos.isEmpty()
repoDatabase.withTransaction {
if (loadType == LoadType.REFRESH) {
repoDatabase.remoteKeysDao().clearRemoteKeys()
repoDatabase.reposDao().clearRepos()
}
val prevKey = if (page == GITHUB_STARTING_PAGE_INDEX) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1
val keys = repos.map {
RemoteKeys(repoId = it.id, prevKey = prevKey, nextKey = nextKey)
}
repoDatabase.remoteKeysDao().insertAll(keys)
repoDatabase.reposDao().insertAll(repos)
}
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (exception: IOException) {
return MediatorResult.Error(exception)
} catch (exception: HttpException) {
return MediatorResult.Error(exception)
}
}
private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, Repo>): RemoteKeys? {
return state.pages.lastOrNull() { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { repo ->
repoDatabase.remoteKeysDao().remoteKeysRepoId(repo.id)
}
}
}