Skip to content

Commit adba8cd

Browse files
Making the RemoteMediator key conversion work
1 parent 83bc78c commit adba8cd

File tree

9 files changed

+133
-50
lines changed

9 files changed

+133
-50
lines changed

app/src/main/java/com/example/android/codelabs/paging/Injection.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.example.android.codelabs.paging.api.GithubService
2222
import com.example.android.codelabs.paging.data.GithubRepository
2323
import com.example.android.codelabs.paging.db.RepoDatabase
2424
import com.example.android.codelabs.paging.ui.ViewModelFactory
25+
import kotlinx.coroutines.Dispatchers
2526

2627
/**
2728
* Class that handles object creation.
@@ -35,7 +36,7 @@ object Injection {
3536
* [GithubLocalCache]
3637
*/
3738
private fun provideGithubRepository(context: Context): GithubRepository {
38-
return GithubRepository(GithubService.create(), provideDatabase(context))
39+
return GithubRepository(GithubService.create(), provideDatabase(context), Dispatchers.IO)
3940
}
4041

4142
private fun provideDatabase(context: Context): RepoDatabase {

app/src/main/java/com/example/android/codelabs/paging/data/GithubRemoteMediator.kt

Lines changed: 97 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import com.example.android.codelabs.paging.db.RepoDatabase
2828
import com.example.android.codelabs.paging.model.Repo
2929
import retrofit2.HttpException
3030
import java.io.IOException
31+
import java.io.InvalidObjectException
3132

3233
// GitHub page API is 1 based: https://developer.github.com/v3/#pagination
3334
const val GITHUB_STARTING_PAGE_INDEX = 1
@@ -38,50 +39,74 @@ class GithubRemoteMediator(
3839
private val repoDatabase: RepoDatabase
3940
) : RemoteMediator<Int, Repo>() {
4041

42+
/**
43+
* The paging library will call this method when there is no more data in the database to
44+
* load from.
45+
*
46+
* For every item we retrieve we store the previous and next keys, so we know how to request
47+
* more data, based on that specific item.
48+
*
49+
* state.pages holds the pages that were loaded until now and saved in the database.
50+
*
51+
* If the load type is REFRESH it means that we are in one of the following situations:
52+
* * it's the first time when the query was triggered and there was nothing saved yet in the
53+
* database
54+
* * a swipe to refresh was called
55+
* If the load type is START, we need to rely on the first item available in the pages
56+
* retrieved so far to compute they key for the previous page.
57+
* If the load type is END, we need to rely on the last item available in the pages
58+
* retrieved so far to compute they key for the next page.
59+
*/
4160
override suspend fun load(loadType: LoadType, state: PagingState<Int, Repo>): MediatorResult {
42-
Log.d("GithubRemoteMediator", "load type: $loadType ")
43-
44-
if (loadType == LoadType.REFRESH) {
45-
repoDatabase.withTransaction {
46-
repoDatabase.reposDao().clearRepos()
47-
repoDatabase.remoteKeysDao().clearRemoteKeys()
48-
}
49-
}
50-
val remoteKeys = repoDatabase.remoteKeysDao().remoteKeys()
51-
?: RemoteKeys(null, GITHUB_STARTING_PAGE_INDEX)
52-
5361
val page = when (loadType) {
54-
LoadType.REFRESH -> remoteKeys.nextKey
55-
LoadType.START -> remoteKeys.prevKey
56-
LoadType.END -> remoteKeys.nextKey
57-
}
58-
59-
if (page == null) {
60-
return MediatorResult.Success(canRequestMoreData = false)
62+
LoadType.REFRESH -> {
63+
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
64+
remoteKeys?.nextKey?.minus(1) ?: GITHUB_STARTING_PAGE_INDEX
65+
}
66+
LoadType.START -> {
67+
val remoteKeys = getRemoteKeyForFirstItem(state)
68+
if (remoteKeys == null) {
69+
// The LoadType is START so some data was loaded before,
70+
// so we should have been able to get remote keys
71+
// If the remoteKeys are null, then we're an invalid state and we have a bug
72+
throw InvalidObjectException("Remote key and the prevKey should not be null")
73+
}
74+
// If the previous key is null, then we can't request more data
75+
val prevKey = remoteKeys.prevKey
76+
if (prevKey == null) {
77+
return MediatorResult.Success(canRequestMoreData = false)
78+
}
79+
remoteKeys.prevKey
80+
}
81+
LoadType.END -> {
82+
val remoteKeys = getRemoteKeyForLastItem(state)
83+
if (remoteKeys?.nextKey == null) {
84+
// The LoadType is END, so some data was loaded before,
85+
// so we should have been able to get remote keys
86+
// If the remoteKeys are null, then we're an invalid state and we have a bug
87+
throw InvalidObjectException("Remote key should not be null for $loadType")
88+
}
89+
remoteKeys.nextKey
90+
}
6191
}
6292

63-
Log.d("GithubRemoteMediator", "page: $page")
64-
6593
val apiQuery = query + IN_QUALIFIER
94+
6695
try {
6796
val apiResponse = service.searchRepos(apiQuery, page, state.config.pageSize)
6897

6998
return if (apiResponse.isSuccessful) {
7099
val repos = apiResponse.body()?.items ?: emptyList()
71100
val canRequestMoreData = repos.isNotEmpty()
72-
val newRemoteKeys = RemoteKeys(
73-
prevKey = remoteKeys.nextKey,
74-
nextKey = if (canRequestMoreData) {
75-
(remoteKeys.nextKey ?: GITHUB_STARTING_PAGE_INDEX) + 1
76-
} else {
77-
null
78-
}
79-
)
80101
repoDatabase.withTransaction {
81-
repoDatabase.remoteKeysDao().replace(newRemoteKeys)
102+
val prevKey = if (page == GITHUB_STARTING_PAGE_INDEX) null else page - 1
103+
val nextKey = if (canRequestMoreData) page + 1 else null
104+
val keys = repos.map {
105+
RemoteKeys(repoId = it.id, prevKey = prevKey, nextKey = nextKey)
106+
}
107+
repoDatabase.remoteKeysDao().insertAll(keys)
82108
repoDatabase.reposDao().insert(repos)
83109
}
84-
Log.d("GithubRemoteMediator", "data: ${repos.size}")
85110
MediatorResult.Success(canRequestMoreData = canRequestMoreData)
86111
} else {
87112
MediatorResult.Error(IOException(apiResponse.message()))
@@ -92,4 +117,46 @@ class GithubRemoteMediator(
92117
return MediatorResult.Error(exception)
93118
}
94119
}
120+
121+
/**
122+
* Get the remote keys of on the last item retrieved
123+
*/
124+
private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, Repo>): RemoteKeys? {
125+
// Get the last page that was retrieved, that contained items.
126+
// From that last page, get the last item
127+
return state.pages.lastOrNull() { it.data.isNotEmpty() }?.data?.lastOrNull()
128+
?.let { repo ->
129+
// Get the remote keys of the last item retrieved
130+
repoDatabase.remoteKeysDao().remoteKeysRepoId(repo.id)
131+
}
132+
}
133+
134+
/**
135+
* Get the remote keys of the first item retrieved
136+
*/
137+
private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, Repo>): RemoteKeys? {
138+
// Get the first page that was retrieved, that contained items.
139+
// From that first page, get the first item
140+
return state.pages.firstOrNull() { it.data.isNotEmpty() }?.data?.firstOrNull()
141+
?.let { repo ->
142+
// Get the remote keys of the first items retrieved
143+
repoDatabase.remoteKeysDao().remoteKeysRepoId(repo.id)
144+
}
145+
}
146+
147+
/**
148+
* Get the remote keys of the closest item to the anchor position
149+
*/
150+
private suspend fun getRemoteKeyClosestToCurrentPosition(
151+
state: PagingState<Int, Repo>
152+
): RemoteKeys? {
153+
// The paging library is trying to load data after the anchor position
154+
// Get the item closest to the anchor position
155+
return state.anchorPosition?.let { position ->
156+
state.closestItemToPosition(position)?.id?.let { repoId ->
157+
// Get the remote keys of the item
158+
repoDatabase.remoteKeysDao().remoteKeysRepoId(repoId)
159+
}
160+
}
161+
}
95162
}

app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,49 @@ import android.util.Log
2020
import androidx.paging.PagingConfig
2121
import androidx.paging.PagingData
2222
import androidx.paging.PagingDataFlow
23+
import androidx.room.withTransaction
2324
import com.example.android.codelabs.paging.api.GithubService
2425
import com.example.android.codelabs.paging.db.RepoDatabase
2526
import com.example.android.codelabs.paging.model.Repo
27+
import kotlinx.coroutines.CoroutineDispatcher
28+
import kotlinx.coroutines.CoroutineScope
2629
import kotlinx.coroutines.ExperimentalCoroutinesApi
30+
import kotlinx.coroutines.SupervisorJob
2731
import kotlinx.coroutines.flow.Flow
32+
import kotlinx.coroutines.launch
2833

2934
/**
3035
* Repository class that works with local and remote data sources.
3136
*/
3237
@ExperimentalCoroutinesApi
3338
class GithubRepository(
3439
private val service: GithubService,
35-
private val database: RepoDatabase
40+
private val database: RepoDatabase,
41+
private val dispatcher: CoroutineDispatcher
3642
) {
3743

44+
private val parentJob = SupervisorJob()
45+
private val scope = CoroutineScope(dispatcher + parentJob)
46+
3847
/**
3948
* Search repositories whose names match the query, exposed as a stream of data that will emit
4049
* every time we get more data from the network.
4150
*/
4251
fun getSearchResultStream(query: String): Flow<PagingData<Repo>> {
52+
// New query, so clear the database
53+
scope.launch {
54+
database.withTransaction {
55+
database.remoteKeysDao().clearRemoteKeys()
56+
database.reposDao().clearRepos()
57+
}
58+
}
59+
4360
Log.d("GithubRepository", "New query: $query")
4461

45-
val pagingSourceFactory = database.reposDao().reposByName().asPagingSourceFactory()
62+
// appending '%' so we can allow other characters to be before and after the query string
63+
val dbQuery = "%${query.replace(' ', '%')}%"
64+
val pagingSourceFactory = database.reposDao().reposByName(dbQuery).asPagingSourceFactory()
65+
4666
return PagingDataFlow(
4767
config = PagingConfig(pageSize = NETWORK_PAGE_SIZE),
4868
remoteMediator = GithubRemoteMediator(

app/src/main/java/com/example/android/codelabs/paging/db/RemoteKeys.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import androidx.room.PrimaryKey
2121

2222
@Entity(tableName = "remote_keys")
2323
data class RemoteKeys(
24-
@PrimaryKey(autoGenerate = true)
24+
@PrimaryKey
25+
val repoId: Long,
2526
val prevKey: Int?,
2627
val nextKey: Int?
2728
)

app/src/main/java/com/example/android/codelabs/paging/db/RemoteKeysDao.kt

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package com.example.android.codelabs.paging.db
1919
import androidx.room.Dao
2020
import androidx.room.Insert
2121
import androidx.room.Query
22-
import androidx.room.Transaction
2322

2423
/**
2524
* Room data access object for accessing the [RemoteKeys] table
@@ -28,16 +27,10 @@ import androidx.room.Transaction
2827
interface RemoteKeysDao {
2928

3029
@Insert
31-
suspend fun insert(remoteKey: RemoteKeys)
30+
suspend fun insertAll(remoteKey: List<RemoteKeys>)
3231

33-
@Query("SELECT * FROM remote_keys LIMIT 1")
34-
suspend fun remoteKeys(): RemoteKeys?
35-
36-
@Transaction
37-
suspend fun replace(remoteKey: RemoteKeys) {
38-
clearRemoteKeys()
39-
insert(remoteKey)
40-
}
32+
@Query("SELECT * FROM remote_keys WHERE repoId = :repoId")
33+
suspend fun remoteKeysRepoId(repoId: Long): RemoteKeys?
4134

4235
@Query("DELETE FROM remote_keys")
4336
suspend fun clearRemoteKeys()

app/src/main/java/com/example/android/codelabs/paging/db/RepoDao.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@ import com.example.android.codelabs.paging.model.Repo
3030
interface RepoDao {
3131

3232
@Insert(onConflict = OnConflictStrategy.REPLACE)
33-
suspend fun insert(posts: List<Repo>)
33+
suspend fun insert(repos: List<Repo>)
3434

3535
// Do a similar query as the search API:
3636
// Look for repos that contain the query string in the name or in the description
3737
// and order those results descending, by the number of stars and then by name
38-
@Query("SELECT * FROM repos ORDER BY stars DESC, name ASC")
39-
fun reposByName(): DataSource.Factory<Int, Repo>
38+
@Query("SELECT * FROM repos WHERE " +
39+
"name LIKE :queryString OR description LIKE :queryString " +
40+
"ORDER BY stars DESC, name ASC")
41+
fun reposByName(queryString: String): DataSource.Factory<Int, Repo>
4042

4143
@Query("DELETE FROM repos")
4244
suspend fun clearRepos()

app/src/main/java/com/example/android/codelabs/paging/ui/ReposAdapter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class ReposAdapter : PagingDataAdapter<UiModel, ViewHolder>(UIMODEL_COMPARATOR)
4040
return when (getItem(position)) {
4141
is UiModel.RepoItem -> R.layout.repo_view_item
4242
is UiModel.SeparatorItem -> R.layout.separator_view_item
43-
null -> throw UnsupportedOperationException("Unknown view")
43+
null -> throw UnsupportedOperationException("Unknown view for position $position")
4444
}
4545
}
4646

app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ class SearchRepositoriesActivity : AppCompatActivity() {
130130
searchJob?.cancel()
131131
searchJob = lifecycleScope.launch {
132132
viewModel.searchRepo(query).collect {
133-
Log.d("SearchRepositoriesActivity", "query: $query, collecting $it")
134133
adapter.presentData(it)
135134
}
136135
}

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ allprojects {
3535
repositories {
3636
google()
3737
jcenter()
38-
maven { url 'https://androidx-dev-prod.appspot.com/snapshots/builds/6397162/artifacts/repository' }
38+
maven { url 'https://androidx-dev-prod.appspot.com/snapshots/builds/6416866/artifacts/repository' }
3939
}
4040
}
4141

0 commit comments

Comments
 (0)