Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 24 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,45 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
}

compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}

viewBinding {
enabled = true
}
}

dependencies {
implementation fileTree(dir: 'libs')
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines"
implementation "androidx.appcompat:appcompat:$supportLibVersion"
implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
implementation "com.google.android.material:material:$materialVersion"

// architecture components
implementation "androidx.lifecycle:lifecycle-extensions:$archComponentsVersion"
implementation "androidx.lifecycle:lifecycle-runtime:$archComponentsVersion"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.room:room-runtime:$roomVersion"
implementation "androidx.room:room-ktx:$roomVersion"
implementation "androidx.paging:paging-runtime:$pagingVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$archComponentsVersion"
kapt "androidx.room:room-compiler:$roomVersion"

// retrofit
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation"com.squareup.retrofit2:converter-gson:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
implementation "com.squareup.retrofit2:retrofit-mock:$retrofitVersion"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpLoggingInterceptorVersion"

Expand Down
19 changes: 4 additions & 15 deletions app/src/main/java/com/example/android/codelabs/paging/Injection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +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.GithubLocalCache
import com.example.android.codelabs.paging.db.RepoDatabase
import com.example.android.codelabs.paging.ui.ViewModelFactory
import java.util.concurrent.Executors

/**
* Class that handles object creation.
Expand All @@ -32,27 +29,19 @@ import java.util.concurrent.Executors
*/
object Injection {

/**
* Creates an instance of [GithubLocalCache] based on the database DAO.
*/
private fun provideCache(context: Context): GithubLocalCache {
val database = RepoDatabase.getInstance(context)
return GithubLocalCache(database.reposDao(), Executors.newSingleThreadExecutor())
}

/**
* Creates an instance of [GithubRepository] based on the [GithubService] and a
* [GithubLocalCache]
*/
private fun provideGithubRepository(context: Context): GithubRepository {
return GithubRepository(GithubService.create(), provideCache(context))
private fun provideGithubRepository(): GithubRepository {
return GithubRepository(GithubService.create())
}

/**
* Provides the [ViewModelProvider.Factory] that is then used to get a reference to
* [ViewModel] objects.
*/
fun provideViewModelFactory(context: Context): ViewModelProvider.Factory {
return ViewModelFactory(provideGithubRepository(context))
fun provideViewModelFactory(): ViewModelProvider.Factory {
return ViewModelFactory(provideGithubRepository())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,67 +16,16 @@

package com.example.android.codelabs.paging.api

import android.util.Log
import com.example.android.codelabs.paging.model.Repo
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.logging.HttpLoggingInterceptor.Level
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query

private const val TAG = "GithubService"
private const val IN_QUALIFIER = "in:name,description"

/**
* Search repos based on a query.
* Trigger a request to the Github searchRepo API with the following params:
* @param query searchRepo keyword
* @param page request page index
* @param itemsPerPage number of repositories to be returned by the Github API per page
*
* The result of the request is handled by the implementation of the functions passed as params
* @param onSuccess function that defines how to handle the list of repos received
* @param onError function that defines how to handle request failure
*/
fun searchRepos(
service: GithubService,
query: String,
page: Int,
itemsPerPage: Int,
onSuccess: (repos: List<Repo>) -> Unit,
onError: (error: String) -> Unit
) {
Log.d(TAG, "query: $query, page: $page, itemsPerPage: $itemsPerPage")

val apiQuery = query + IN_QUALIFIER

service.searchRepos(apiQuery, page, itemsPerPage).enqueue(
object : Callback<RepoSearchResponse> {
override fun onFailure(call: Call<RepoSearchResponse>?, t: Throwable) {
Log.d(TAG, "fail to get data")
onError(t.message ?: "unknown error")
}

override fun onResponse(
call: Call<RepoSearchResponse>?,
response: Response<RepoSearchResponse>
) {
Log.d(TAG, "got a response $response")
if (response.isSuccessful) {
val repos = response.body()?.items ?: emptyList()
onSuccess(repos)
} else {
onError(response.errorBody()?.string() ?: "Unknown error")
}
}
}
)
}
const val IN_QUALIFIER = "in:name,description"

/**
* Github API communication setup via Retrofit.
Expand All @@ -86,11 +35,11 @@ interface GithubService {
* Get repos ordered by stars.
*/
@GET("search/repositories?sort=stars")
fun searchRepos(
@Query("q") query: String,
@Query("page") page: Int,
@Query("per_page") itemsPerPage: Int
): Call<RepoSearchResponse>
suspend fun searchRepos(
@Query("q") query: String,
@Query("page") page: Int,
@Query("per_page") itemsPerPage: Int
): RepoSearchResponse

companion object {
private const val BASE_URL = "https://api.github.com/"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,60 +17,95 @@
package com.example.android.codelabs.paging.data

import android.util.Log
import androidx.lifecycle.MutableLiveData
import com.example.android.codelabs.paging.api.GithubService
import com.example.android.codelabs.paging.api.searchRepos
import com.example.android.codelabs.paging.db.GithubLocalCache
import com.example.android.codelabs.paging.api.IN_QUALIFIER
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.
*/
class GithubRepository(
private val service: GithubService,
private val cache: GithubLocalCache
) {
@ExperimentalCoroutinesApi
class GithubRepository(private val service: GithubService) {

// keep the last requested page. When the request is successful, increment the page number.
private var lastRequestedPage = 1
// 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>()

// LiveData of network errors.
private val networkErrors = MutableLiveData<String>()
// 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

/**
* Search repositories whose names match the query.
* 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.
*/
fun search(query: String): RepoSearchResult {
suspend fun getSearchResultStream(query: String): Flow<RepoSearchResult> {
Log.d("GithubRepository", "New query: $query")
lastRequestedPage = 1
inMemoryCache.clear()
requestAndSaveData(query)

// Get data from the local cache
val data = cache.reposByName(query)

return RepoSearchResult(data, networkErrors)
return searchResults.asFlow()
}

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

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

private suspend fun requestAndSaveData(query: String): Boolean {
isRequestInProgress = true
searchRepos(service, query, lastRequestedPage, NETWORK_PAGE_SIZE, { repos ->
cache.insert(repos) {
lastRequestedPage++
isRequestInProgress = false
}
}, { error ->
networkErrors.postValue(error)
isRequestInProgress = false
})
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 })
}

companion object {
Expand Down

This file was deleted.

Loading