Paging 3 - How to handle old APPEND RemoteMediator requests that come trough since beta01?

Viewed 1349

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)
                }
    }
}
1 Answers

I talked to Dustin Lam and Yigit Boyar and apparently, the best way to handle this is to make prepend an append not depend on the PagingState. This means we should store our remote keys in a table related to a query rather than on the item level.

Exmaple:

@Entity(tableName = "search_query_remote_keys")
data class SearchQueryRemoteKey(
    @PrimaryKey val searchQuery: String,
    val nextPageKey: Int
)
Related