Hello! I have a problem with Paging v3 library on ...
# android
n
Hello! I have a problem with Paging v3 library on Android, here's what happens: When the app is opened, the list is shown correctly and everything is great, but if I call
invalidate()
method of PagingSource, then it starts spamming
load
method of PagingSource (and doesn't even wait until the job is finished, cancels it and starts anew). This is the stacktrace:
Copy code
2021-07-14 02:23:46.189 15215-15254/com.xlebnick.kitties.debug D/OkHttp: --> GET <https://myapi.com/v1/images/search?page=0&limit=24&order=ASC>
2021-07-14 02:23:46.189 15215-15254/com.xlebnick.kitties.debug D/OkHttp: x-api-key: 9b7e282d-2a67-4c7b-a9fd-3f3e4056e949
2021-07-14 02:23:46.189 15215-15254/com.xlebnick.kitties.debug D/OkHttp: --> END GET
2021-07-14 02:23:46.189 15215-15254/com.xlebnick.kitties.debug D/OkHttp: <-- HTTP FAILED: java.io.IOException: Canceled
2021-07-14 02:23:46.189 15215-15215/com.xlebnick.kitties.debug W/System.err: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@6c3004
And this is my implementation:
Copy code
class KittiesPagingSource(
    private val repository: Repository,
    var breedFilter: Breed? = null,
    var likedKitties: List<Like> = listOf()
) : PagingSource<Int, Kitty>() {


    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Kitty> {
        return try {
            // Start refresh at page 1 if undefined.
            val nextPage = params.key ?: 1

            val filters = if (breedFilter != null) listOf(breedFilter!!) else null
            var kitties: List<KittyRemoteModel> = listOf()
            try {
                kitties = repository.fetchKitties(nextPage, KITTIES_PAGE_SIZE, filters)
            } catch (e: Throwable) {
                e.printStackTrace()
            }

            val newKitties = kitties.map { kitty ->
                kitty.asKitty(likedKitties.any { it.imageId == kitty.id })
            }

            LoadResult.Page(
                data = newKitties,
                prevKey = if (nextPage == 1) null else nextPage - 1,
                nextKey = if (newKitties.isEmpty()) null else nextPage + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, Kitty>): Int? {
        return state.anchorPosition?.let {
            if (it < state.config.initialLoadSize) {
                // if anchor position is less than initial loading count then download from the beginning
                0
            } else {
                // otherwise load a page around anchorPosition using initialLoadSize
                (it - state.config.initialLoadSize / 2).coerceAtLeast(0)
            }
        }
    }
}
Copy code
private val kittiesPagingFactory = InvalidatingPagingSourceFactory {
        KittiesPagingSource(repository, breedFilter, likedKitties.value ?: listOf())
    }
    private val kittiesPager =
        Pager(PagingConfig(pageSize = 6), pagingSourceFactory = kittiesPagingFactory)
    val kitties: Flow<PagingData<Kitty>> = kittiesPager
        .flow
        .cachedIn(viewModelScope)
What am I doing wrong?
d
So cancellation seems to be WAI as invalidation cancels the previous generation and creates a new one to reload data.
Do you mean that you are invalidating in a loop over and over again?
How / when are you calling .invalidate() ?
n
No, the invalidation is only called once
I invalidate when the second batch of data comes which changes the representation (I take data A and data B and display AB, but until B came, I show only A)
I put a breakpoint on
kittiesPagingFactory.invalidate()
, and it's been only fired once
What does WAI mean?
e
working as intended
d
What is the loadType / subclass of LoadParams passed to .load()? is it looping between prepend and append
n
image.png
is it looping between prepend and append
If I understand the question correctly, then yes, because it keeps updating the flow, and therefore, the RecyclerView
The type of LoadParams that is sent to
load
is always
androidx.paging.PagingSource$LoadParams$Refresh
So no prepend/append
d
So this implies an invalidation loop, I can take a look if you have a sample you can share
To be clear, cancellation is expected, but spamming load is not 🙂
n
Sure, I will create a minimum reproducible code in a bit
Please let me know if something is missing
d
I think this is a bug with InvalidatingPagingSourceFactory
To confirm, can you try something like
Copy code
public class InvalidatingPagingSourceFactory<Key : Any, Value : Any>(
    private val pagingSourceFactory: () -> PagingSource<Key, Value>
) : () -> PagingSource<Key, Value> {
    private val pagingSources = mutableListOf<PagingSource<Key, Value>>()


    override fun invoke(): PagingSource<Key, Value> {
        return pagingSourceFactory().also { pagingSources.add(it) }
    }

    public fun invalidate() {
        for (pagingSource in pagingSources.toList()) {
            if (!pagingSource.invalid) {
                pagingSource.invalidate()
            }
        }

        pagingSources.removeAll { it.invalid }
    }
}
Fix won't make it for next release, but should for the one after (3.1.0-alpha04)
n
Works like a charm, thanks a lot!
661 Views