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.