0

I am implementing Paging with cache using Room database by following this codelab tutorial : - https://codelabs.developers.google.com/codelabs/android-paging/#13

However while implementing the caching part, I have come across some strange behaviour while appending the next page in recyclerview.

The RemoteMediator only fetches the 2nd page data in a continuous infinite loop. Rather than fetching data for the next page it continuously fetches data for the 2nd page.

I have found out that this is happening because of @PrimaryKey of table. I have Int as @PrimaryKey in the table and it is 5 characters long.

The same is implemented in the codelab tutorial, where they also have Int as @PrimaryKey. But they have a minimum 7 character long randomized primary key. And it works perfectly fine. To verify this, I have made my @PrimaryKey around 15 character long by adding

id = id + System.currentTimeMillis()

and after doing this it works perfectly. But in real scenario I can't modify the id.

So, is there any limitations in @PrimayKey of Room table(i.e. like the primary key must have minimum 7 character)? Or is @PrimaryKey have to be randomize in Room? Or I am doing something wrong here?

Here is the code and JSON.

JSON FORMAT

[
    { 
        "id": 24087,
        "date": "2020-07-15T11:20:00",
        "link": "https://www.somesite.com/24534-eu-covid-19-stimulus-negotiations-and-its-historic-backdrop-2020-07-21/",
        "title": {
            "rendered": "EU Covid-19 Stimulus Negotiations And Its Historic Backdrop"
        },
        "excerpt": {
            "rendered": "After a long debate between the opposing factions, the European Union agreed on a recovery fund to aid the economy amidst the Covid-19 pandemic..."
        }
    },
    {..},
    {..}
]

Story.kt

@Entity(tableName = "stories")
data class Story(
    @PrimaryKey @field:SerializedName("id") var storyId: Long,
    @field:SerializedName("date") val date: String,
    @field:SerializedName("link") val link: String,
    @Embedded(prefix = "title_") @field:SerializedName("title") val title: Title,
    @Embedded(prefix = "excerpt_") @field:SerializedName("excerpt") val excerpt: Excerpt    
)

data class Title(
    val rendered: String
)

data class Excerpt(
    val rendered: String
)

RemoteMediator.kt

@OptIn(ExperimentalPagingApi::class)
class StoryRemoteMediator(
    private val category: Int,
    private val ocapiDatabase: OCAPIDatabase
) : RemoteMediator<Int, Story>() {

    private val storyService = StoryService.create()

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, Story>
    ): MediatorResult {
        val page = when (loadType) {
            LoadType.REFRESH -> {
                val remoteKeys = getRemoteKeysClosestToCurrentPosition(state)
                remoteKeys?.nextKey?.minus(1) ?: STORY_STARTING_PAGE_INDEX
            }
            LoadType.PREPEND -> {
                val remoteKeys = getRemoteKeysForFirstItem(state) ?: 
                    throw InvalidObjectException("Remote key and the prevKey should not be null")

                remoteKeys.prevKey ?: return MediatorResult.Success(endOfPaginationReached = true)
            }
            LoadType.APPEND -> {
                val remoteKeys = getRemoteKeysForLastItem(state) ?:
                    throw InvalidObjectException("Remote key should not be null for $loadType")

                remoteKeys.nextKey ?: return MediatorResult.Success(endOfPaginationReached = true)
            }
        }

        try {
            val stories = storyService.getStories(page = page, category = category)

            val endOfPaginationReached = stories.isEmpty()

            ocapiDatabase.withTransaction {
                if (loadType == LoadType.REFRESH && stories.isNotEmpty()) {
                    ocapiDatabase.storyDao().deleteAll()
                    ocapiDatabase.storyRemoteKeysDao().deleteAll()
                }
 
                val prevKey = if (page == STORY_STARTING_PAGE_INDEX) null else page - 1
                val nextKey = if (endOfPaginationReached) null else page + 1

                val keys = stories.map {
                    StoryRemoteKeys(
                        storyId = it.storyId,
                        prevKey = prevKey,
                        nextKey = nextKey
                    )
                }
                ocapiDatabase.storyDao().insertAll(stories)
                ocapiDatabase.storyRemoteKeysDao().insertAll(keys)
            }

            return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
        } catch (exception: IOException) {
            return MediatorResult.Error(exception)
        } catch (exception: HttpException) {
            return MediatorResult.Error(exception)
        }
    }


    private suspend fun getRemoteKeysClosestToCurrentPosition(state: PagingState<Int, Story>): StoryRemoteKeys? {
        return state.anchorPosition?.let { position ->
            state.closestItemToPosition(position)?.storyId?.let { storyId ->
                ocapiDatabase.storyRemoteKeysDao().getRemoteKeysById(storyId)
            }
        }
    }

    private suspend fun getRemoteKeysForFirstItem(state: PagingState<Int, Story>): StoryRemoteKeys? {
        return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
            ?.let { story ->
                ocapiDatabase.storyRemoteKeysDao().getRemoteKeysById(story.storyId)
            }
    }

    private suspend fun getRemoteKeysForLastItem(state: PagingState<Int, Story>): StoryRemoteKeys? {
        return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()
            ?.let { story ->
                ocapiDatabase.storyRemoteKeysDao().getRemoteKeysById(story.storyId)
            }
        }
    }
}

To make it more clear, here is the log which prints the the current page number to fetch from network.

2020-07-22 18:21:35.767 3510-3510/com.milan.ocapi I/STORY_PAGE: 2
2020-07-22 18:21:36.599 3510-3510/com.milan.ocapi I/STORY_PAGE: 2
2020-07-22 18:21:37.517 3510-3510/com.milan.ocapi I/STORY_PAGE: 2
2020-07-22 18:21:38.456 3510-3510/com.milan.ocapi I/STORY_PAGE: 2
2020-07-22 18:21:39.723 3510-3510/com.milan.ocapi I/STORY_PAGE: 2
2020-07-22 18:21:40.459 3510-3510/com.milan.ocapi I/STORY_PAGE: 2
2020-07-22 18:21:41.275 3510-3510/com.milan.ocapi I/STORY_PAGE: 2
2020-07-22 18:21:42.191 3510-3510/com.milan.ocapi I/STORY_PAGE: 2
2020-07-22 18:21:43.095 3510-3510/com.milan.ocapi I/STORY_PAGE: 2
2020-07-22 18:21:44.010 3510-3510/com.milan.ocapi I/STORY_PAGE: 2
2020-07-22 18:21:44.903 3510-3510/com.milan.ocapi I/STORY_PAGE: 2

As you can see it does not fetch next(i.e. 3, 4, 5) page. It goes into loop. For testing, I have also changed @PrimaryKey to link(which is String type value), and it is working in that condition also.

So I am pretty sure that there is something wrong with @PrimaryKey length.

Milan Thakor
  • 103
  • 3
  • 7

1 Answers1

0

To answer your question directly - there is no such limitation for character length of keys in Paging.

The reason that you are loading the same page over and over again in RemoteMediator is due to Paging being unable to tell you've hit the last page through endOfPaginationReached, but is receiving invalidations telling it the data has updates, and so it should try to load again.

Check that stories.isEmpty() is true after loading the second page and if so, try returning immediately with return MediatorResult.Success(endOfPaginationReached = true)?

Also, check to see if PagingSource is being invalidated somehow although you're fetching the same page. Does writing the same items somehow trigger invalidation or update a column even though it's technically keyed on the same page, perhaps the date field is changing each time you make the request?

dlam
  • 3,547
  • 17
  • 20
  • Ok. I'll try to check `stories.isEmpty()` is `true` or `not`. But can you explain me, why it's working perfectly with `link`(type of string) as Primary Key?. The data is same in both conditions. – Milan Thakor Jul 24 '20 at 02:55