Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a03206c
Having the list backed by network only
florina-muntenescu Jan 29, 2020
ede976b
Showing an infinite scrolling list, from network, with Flow
florina-muntenescu Jan 29, 2020
b9a0c42
Adding ViewBinding
florina-muntenescu Feb 24, 2020
38dfd9c
Making the project compatible with Android Studio 3.6
florina-muntenescu Mar 10, 2020
faaa6d0
Steps 5-9 Migrating to Paging 3.0
florina-muntenescu May 4, 2020
5a07d19
Steps 5-9 Migrating to Paging 3.0
florina-muntenescu May 4, 2020
cfdb89d
Adding a loading state footer
florina-muntenescu May 4, 2020
dad577e
Steps 5-9 Migrating to Paging 3.0
florina-muntenescu May 4, 2020
344ec31
Steps 5-9 Migrating to Paging 3.0
florina-muntenescu May 4, 2020
e4086ce
Display the loading state in SearchRepositoriesActivity
florina-muntenescu May 4, 2020
4b3a833
Steps 5-9 Migrating to Paging 3.0
florina-muntenescu May 4, 2020
781c4dc
Display the loading state in SearchRepositoriesActivity
florina-muntenescu May 4, 2020
fd863b1
Adding list separators
florina-muntenescu May 4, 2020
66a3146
Adding a loading state footer
florina-muntenescu May 4, 2020
459dd92
Steps 5-9 Migrating to Paging 3.0
florina-muntenescu May 4, 2020
61de717
Display the loading state in SearchRepositoriesActivity
florina-muntenescu May 4, 2020
f690897
Steps 5-9 Migrating to Paging 3.0
florina-muntenescu May 4, 2020
e83529e
Display the loading state in SearchRepositoriesActivity
florina-muntenescu May 4, 2020
8a5b401
Step 13 - 19 Paging from network and database
florina-muntenescu May 5, 2020
e4e8756
return success with endOfPaginationReached set to true when nextkey i…
sanghoon9173 Jul 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ android {
viewBinding {
enabled = true
}

viewBinding {
enabled = true
}
}

dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import android.content.Context
import androidx.lifecycle.ViewModelProvider
import com.example.android.codelabs.paging.api.GithubService
import com.example.android.codelabs.paging.data.GithubRepository
import com.example.android.codelabs.paging.db.RepoDatabase
import com.example.android.codelabs.paging.ui.ViewModelFactory

/**
Expand All @@ -33,15 +34,15 @@ object Injection {
* Creates an instance of [GithubRepository] based on the [GithubService] and a
* [GithubLocalCache]
*/
private fun provideGithubRepository(): GithubRepository {
return GithubRepository(GithubService.create())
private fun provideGithubRepository(context: Context): GithubRepository {
return GithubRepository(GithubService.create(), RepoDatabase.getInstance(context))
}

/**
* Provides the [ViewModelProvider.Factory] that is then used to get a reference to
* [ViewModel] objects.
*/
fun provideViewModelFactory(): ViewModelProvider.Factory {
return ViewModelFactory(provideGithubRepository())
fun provideViewModelFactory(context: Context): ViewModelProvider.Factory {
return ViewModelFactory(provideGithubRepository(context))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.android.codelabs.paging.data

import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.example.android.codelabs.paging.api.GithubService
import com.example.android.codelabs.paging.api.IN_QUALIFIER
import com.example.android.codelabs.paging.db.RemoteKeys
import com.example.android.codelabs.paging.db.RepoDatabase
import com.example.android.codelabs.paging.model.Repo
import retrofit2.HttpException
import java.io.IOException
import java.io.InvalidObjectException

// GitHub page API is 1 based: https://developer.github.com/v3/#pagination
private const val GITHUB_STARTING_PAGE_INDEX = 1

@OptIn(ExperimentalPagingApi::class)
class GithubRemoteMediator(
private val query: String,
private val service: GithubService,
private val repoDatabase: RepoDatabase
) : RemoteMediator<Int, Repo>() {

override suspend fun load(loadType: LoadType, state: PagingState<Int, Repo>): MediatorResult {

val page = when (loadType) {
LoadType.REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: GITHUB_STARTING_PAGE_INDEX
}
LoadType.PREPEND -> {
val remoteKeys = getRemoteKeyForFirstItem(state)
if (remoteKeys == null) {
// The LoadType is PREPEND so some data was loaded before,
// so we should have been able to get remote keys
// If the remoteKeys are null, then we're an invalid state and we have a bug
throw InvalidObjectException("Remote key and the prevKey should not be null")
}
// If the previous key is null, then we can't request more data
val prevKey = remoteKeys.prevKey
if (prevKey == null) {
return MediatorResult.Success(endOfPaginationReached = true)
}
remoteKeys.prevKey
}
LoadType.APPEND -> {
val remoteKeys = getRemoteKeyForLastItem(state)
if (remoteKeys == null) {
throw InvalidObjectException("Remote key should not be null for $loadType")
}

val nextKey = remoteKeys.nextKey
if (nextKey == null) {
return MediatorResult.Success(endOfPaginationReached = true)
}
remoteKeys.nextKey
}

}

val apiQuery = query + IN_QUALIFIER

try {
val apiResponse = service.searchRepos(apiQuery, page, state.config.pageSize)

val repos = apiResponse.items
val endOfPaginationReached = repos.isEmpty()
repoDatabase.withTransaction {
// clear all tables in the database
if (loadType == LoadType.REFRESH) {
repoDatabase.remoteKeysDao().clearRemoteKeys()
repoDatabase.reposDao().clearRepos()
}
val prevKey = if (page == GITHUB_STARTING_PAGE_INDEX) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1
val keys = repos.map {
RemoteKeys(repoId = it.id, prevKey = prevKey, nextKey = nextKey)
}
repoDatabase.remoteKeysDao().insertAll(keys)
repoDatabase.reposDao().insertAll(repos)
}
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, Repo>): RemoteKeys? {
// Get the last page that was retrieved, that contained items.
// From that last page, get the last item
return state.pages.lastOrNull() { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { repo ->
// Get the remote keys of the last item retrieved
repoDatabase.remoteKeysDao().remoteKeysRepoId(repo.id)
}
}

private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, Repo>): RemoteKeys? {
// Get the first page that was retrieved, that contained items.
// From that first page, get the first item
return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
?.let { repo ->
// Get the remote keys of the first items retrieved
repoDatabase.remoteKeysDao().remoteKeysRepoId(repo.id)
}
}

private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, Repo>
): RemoteKeys? {
// The paging library is trying to load data after the anchor position
// Get the item closest to the anchor position
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.id?.let { repoId ->
repoDatabase.remoteKeysDao().remoteKeysRepoId(repoId)
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,95 +17,42 @@
package com.example.android.codelabs.paging.data

import android.util.Log
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.example.android.codelabs.paging.api.GithubService
import com.example.android.codelabs.paging.api.IN_QUALIFIER
import com.example.android.codelabs.paging.db.RepoDatabase
import com.example.android.codelabs.paging.model.Repo
import com.example.android.codelabs.paging.model.RepoSearchResult
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import retrofit2.HttpException
import java.io.IOException

// GitHub page API is 1 based: https://developer.github.com/v3/#pagination
private const val GITHUB_STARTING_PAGE_INDEX = 1

/**
* Repository class that works with local and remote data sources.
*/
@ExperimentalCoroutinesApi
class GithubRepository(private val service: GithubService) {

// keep the list of all results received
private val inMemoryCache = mutableListOf<Repo>()

// keep channel of results. The channel allows us to broadcast updates so
// the subscriber will have the latest data
private val searchResults = ConflatedBroadcastChannel<RepoSearchResult>()

// keep the last requested page. When the request is successful, increment the page number.
private var lastRequestedPage = GITHUB_STARTING_PAGE_INDEX

// avoid triggering multiple requests in the same time
private var isRequestInProgress = false
class GithubRepository(
private val service: GithubService,
private val database: RepoDatabase
) {

/**
* Search repositories whose names match the query, exposed as a stream of data that will emit
* every time we get more data from the network.
*/
suspend fun getSearchResultStream(query: String): Flow<RepoSearchResult> {
fun getSearchResultStream(query: String): Flow<PagingData<Repo>> {
Log.d("GithubRepository", "New query: $query")
lastRequestedPage = 1
inMemoryCache.clear()
requestAndSaveData(query)

return searchResults.asFlow()
}

suspend fun requestMore(query: String) {
if (isRequestInProgress) return
val successful = requestAndSaveData(query)
if (successful) {
lastRequestedPage++
}
}

suspend fun retry(query: String) {
if (isRequestInProgress) return
requestAndSaveData(query)
}

private suspend fun requestAndSaveData(query: String): Boolean {
isRequestInProgress = true
var successful = false

val apiQuery = query + IN_QUALIFIER
try {
val response = service.searchRepos(apiQuery, lastRequestedPage, NETWORK_PAGE_SIZE)
Log.d("GithubRepository", "response $response")
val repos = response.items ?: emptyList()
inMemoryCache.addAll(repos)
val reposByName = reposByName(query)
searchResults.offer(RepoSearchResult.Success(reposByName))
successful = true
} catch (exception: IOException) {
searchResults.offer(RepoSearchResult.Error(exception))
} catch (exception: HttpException) {
searchResults.offer(RepoSearchResult.Error(exception))
}
isRequestInProgress = false
return successful
}

private fun reposByName(query: String): List<Repo> {
// from the in memory cache select only the repos whose name or description matches
// the query. Then order the results.
return inMemoryCache.filter {
it.name.contains(query, true) ||
(it.description != null && it.description.contains(query, true))
}.sortedWith(compareByDescending<Repo> { it.stars }.thenBy { it.name })
// appending '%' so we can allow other characters to be before and after the query string
val dbQuery = "%${query.replace(' ', '%')}%"
val pagingSourceFactory = { database.reposDao().reposByName(dbQuery) }

return Pager(
config = PagingConfig(pageSize = NETWORK_PAGE_SIZE),
remoteMediator = GithubRemoteMediator(
query,
service,
database
),
pagingSourceFactory = pagingSourceFactory
).flow
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.android.codelabs.paging.db

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "remote_keys")
data class RemoteKeys(
@PrimaryKey val repoId: Long,
val prevKey: Int?,
val nextKey: Int?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.android.codelabs.paging.db

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface RemoteKeysDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(remoteKey: List<RemoteKeys>)

@Query("SELECT * FROM remote_keys WHERE repoId = :repoId")
suspend fun remoteKeysRepoId(repoId: Long): RemoteKeys?

@Query("DELETE FROM remote_keys")
suspend fun clearRemoteKeys()
}

Loading