androidandroid-paging-3

Paging 3 refresh in the middle


I'm having a problem with refreshing the paged data and I'm not sure how I need to set the refresh key so it works correctly. The docs aren't clear at all. I have this base class for offset paging, so it goes 0-40, 40-60, 60-80, and so on. And that works, but when I'm in the middle of the collection and I want to refresh the data, either with invalidate() or adapter.refresh(), it crashes with following message:

java.lang.IllegalStateException: The same value, 40, was passed as the prevKey in two sequential Pages loaded from a PagingSource. Re-using load keys in PagingSource is often an error, and must be explicitly enabled by overriding PagingSource.keyReuseSupported.

When it is enabled, it doesn't crash, but its behavior is weird because it starts paging from the middle and I can't go back to the beginning of the collection, as it is constantly paging the same items.

Example:

prevKey null, nextKey: 40 -> prevKey 40, nextKey: 60 -> prevKey 60, nextKey: 80

Invalidate()

prevKey 40, nextKey: 80 -> prevKey 40, nextKey: 60 // This is the case when it crashes without the keyReuseSupported flag.

And as I want to go back to the beginning it is stuck on prevKey 40, nextKey: 60

abstract class OffsetPagingSource<Value : Any>(
    private val coroutineScope: CoroutineScope
) : PagingSource<Int, Value>() {

    abstract suspend fun queryFactory(
        size: Int,
        after: Int,
        itemCount: Boolean
    ): PagingResult<Value>?

    abstract suspend fun subscriptionFactory(): Flow<Any>?

    init {
        initSubscription()
    }

    private fun initSubscription() {
        coroutineScope.launch {
            try {
                subscriptionFactory()?.collect {
                    invalidate()
                }
            } catch (e: Throwable) {}
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Value> {
        val size = params.loadSize
        val after = params.key ?: 0

        return try {
            val result = queryFactory(size, after, true) ?: return LoadResult.Page(emptyList(), null, null)

            val totalCount = result.pageInfo.itemCount

            val nextCount = after + size

            val prevKey = if (after == 0) null else after
            val nextKey = if (nextCount < totalCount) nextCount else null

            LoadResult.Page(result.edges.mapNotNull { it.node }, prevKey = prevKey, nextKey)
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, Value>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            val anchorPage = state.closestPageToPosition(anchorPosition)
            anchorPage?.prevKey
        }
    }
}

Does anyone know how to fix this? I'm not sure if this is a bug or not. Paging 3 kinds of forces a way the paging works and not enabling a different approach.


Solution

  • The exception being thrown is warning you that you are using the same key to load different data / pages. In your example after invalidate:

    { prevKey 40, nextKey: 80}, { prevKey 40, nextKey: 60 }

    Implies that you would want to use params.key = 40 for the page before the first page and also to reload the first page. I.e., you have:

    page0: not loaded yet
    page1: { prevKey 40, nextKey: 80 }
    page2: { prevKey 40, nextKey: 60 }
    

    Implies you want to use params.key to load both page0 and page1. This might be what you intend, but is generally a mistake which is what that exception is warning you about.

    The reason you are probably reloading the same page over and over again, is that you interpret prepending and appending with the same logic that loads from key...key+pageSize and always passing prevKey = key. You probably need to either check for LoadParams is LoadParams.Prepend and offset the key in your load logic, or offset the key before you pass it back to Paging as prevKey.