1

After making a simple app with Paging 3 (based on this Google GitHub codelab) my app crashes. When I scroll down, at some point (probably when the new GET function is called). Logcat looks like this:

D/NewsRemoteMediator: APPEND
I/okhttp.OkHttpClient: --> GET https://webit-news-search.p.rapidapi.com/search?language=en&number=5&offset=10&q=SearchText
I/okhttp.OkHttpClient: <-- 200 OK https://webit-news-search.p.rapidapi.com/search?language=en&number=5&offset=10&q=SearchText(182ms, 2853-byte body)

2020-11-16 16:39:30.991 10559-10559/com.example.testapp E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.testapp, PID: 10559
    java.lang.IllegalArgumentException: Additional prepend event after prepend state is done
        at androidx.paging.SeparatorState.onInsert(Separators.kt:221)
        at androidx.paging.SeparatorState.onEvent(Separators.kt:173)
        at androidx.paging.SeparatorsKt$insertEventSeparators$$inlined$map$1$2.emit(Collect.kt:135)
        at androidx.paging.PagingDataKt$map$$inlined$transform$1$2.emit(Collect.kt:136)
        at kotlinx.coroutines.flow.FlowKt__ChannelsKt.emitAllImpl$FlowKt__ChannelsKt(Channels.kt:61)
        at kotlinx.coroutines.flow.FlowKt__ChannelsKt$emitAllImpl$1.invokeSuspend(Unknown Source:11)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.EventLoop.processUnconfinedEvent(EventLoop.common.kt:69)
        at kotlinx.coroutines.DispatchedTaskKt.resumeUnconfined(DispatchedTask.kt:236)
        at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:161)
        at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:362)
        at kotlinx.coroutines.CancellableContinuationImpl.completeResume(CancellableContinuationImpl.kt:479)
        at kotlinx.coroutines.channels.AbstractChannel$ReceiveElement.completeResumeReceive(AbstractChannel.kt:899)
        at kotlinx.coroutines.channels.ArrayChannel.offerInternal(ArrayChannel.kt:84)
        at kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:135)
        at androidx.paging.PageFetcherSnapshot.doLoad(PageFetcherSnapshot.kt:487)
        at androidx.paging.PageFetcherSnapshot$doLoad$1.invokeSuspend(Unknown Source:12)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at android.os.Handler.handleCallback(Handler.java:900)
        at android.os.Handler.dispatchMessage(Handler.java:103)
        at android.os.Looper.loop(Looper.java:219)
        at android.app.ActivityThread.main(ActivityThread.java:8347)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)
2020-11-16 16:39:31.036 10559-10559/com.example.testapp I/Process: Sending signal. PID: 10559 SIG: 9

The same error happens in the Google sample app from GitHub but in their case, it happens after scrolling down and then scrolling up. Is there any possibility to see where and what exactly causes this error? I tried to read this logcat error but it says nothing to me. Below is RemoteMediator and Repository class, I can add other classes if it can help.

@OptIn(ExperimentalPagingApi::class)
class NewsRemoteMediator @Inject constructor(
    private val newsRetrofit: NewsRetrofit,
    private val database: AppDatabase,
    private val networkToEntityMapper: NetworkToEntityMapper
) : RemoteMediator<Int, News>()
{

    var searchKey: String? = null

    override suspend fun load(loadType: LoadType, state: PagingState<Int, News>): MediatorResult
    {
        val page: Int = when (loadType)
        {
            LoadType.REFRESH ->
            {
                Timber.d("REFRESH")
                val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
                remoteKeys?.nextKey?.minus(1) ?: NEWS_STARTING_PAGE_INDEX
            }
            LoadType.PREPEND ->
            {
                Timber.d("PREPEND")
                val remoteKeys = getRemoteKeyForFirstItem(state)
                    ?: throw InvalidObjectException("Remote key and the prevKey should not be null")
                remoteKeys.prevKey
                    ?: return MediatorResult.Success(endOfPaginationReached = true)
                remoteKeys.prevKey
            }
            LoadType.APPEND ->
            {
                Timber.d("APPEND")
                val remoteKeys = getRemoteKeyForLastItem(state)
                if (remoteKeys?.nextKey == null)
                {
                    throw InvalidObjectException("Remote key should not be null for $loadType")
                }
                remoteKeys.nextKey
            }
        }

        try
        {
            val apiResponse =
                if (searchKey.isNullOrEmpty())
                    newsRetrofit.getTrending(
                        state.config.pageSize,
                        page * state.config.pageSize
                    )
                else
                    newsRetrofit.getSearched(
                        state.config.pageSize,
                        page * state.config.pageSize,
                        searchKey!!
                    )


            val news = apiResponse.data.results
            val endOfPaginationReached = news.isEmpty()

            database.withTransaction {
                // clear all tables in the database
                if (loadType == LoadType.REFRESH)
                {
                    Timber.d("Clear db")
                    database.remoteKeysDao.clearRemoteKeys()
                    database.newsDao.clearNews()
                }

                val prevKey = if (page == NEWS_STARTING_PAGE_INDEX) null else page - 1
                val nextKey = if (endOfPaginationReached) null else page + 1
                val keys = news.map {
                    RemoteKeys(
                        newsId = it.url,
                        prevKey = prevKey,
                        nextKey = nextKey
                    )
                }

                database.remoteKeysDao.insertAll(keys)
                database.newsDao.insertAll(networkToEntityMapper.mapToNewModelList(news))
            }
            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, News>): RemoteKeys?
    {
        return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()
            ?.let { news ->
                database.remoteKeysDao.remoteKeysRepoId(news.url)
            }
    }

    private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, News>): RemoteKeys?
    {
        return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
            ?.let { news ->
                database.remoteKeysDao.remoteKeysRepoId(news.url)
            }
    }

    private suspend fun getRemoteKeyClosestToCurrentPosition(
        state: PagingState<Int, News>
    ): RemoteKeys?
    {
        return state.anchorPosition?.let { position ->
            state.closestItemToPosition(position)?.url?.let { newsUrl ->
                database.remoteKeysDao.remoteKeysRepoId(newsUrl)
            }
        }
    }
}

RepositoryClass:

@Singleton
class NewsRepository @Inject constructor(
    private val database: AppDatabase,
    private val newsRemoteMediator: NewsRemoteMediator
)
{
     fun getSearchResultStream(searchKey: String?): Flow<PagingData<News>>
    {
        val pagingSourceFactory = { database.newsDao.getNews() }

        return Pager(
            config = PagingConfig(
                pageSize = NETWORK_PAGE_SIZE,
                enablePlaceholders = false,
                initialLoadSize = INITIAL_LOAD_SIZE
            ),
            remoteMediator = newsRemoteMediator.apply { this.searchKey = searchKey },
            pagingSourceFactory = pagingSourceFactory
        ).flow
    }
}
iknow
  • 8,358
  • 12
  • 41
  • 68

1 Answers1

0

I am not sure but I think it was just a bug in the library. I was using 3.0.0-alpha08 but after updating to 3.0.0-alpha09 I don't get any errors. I tested it in my app and in Google sample project after updating the version and it is working.

Fix for IllegalArgumentException being throw when using separators with RemoteMediator and an invalidate is triggered while a remote load that would return endOfPagination is still running (I3a260)

iknow
  • 8,358
  • 12
  • 41
  • 68