diff --git a/app/build.gradle b/app/build.gradle index 7960af71..8d7d5419 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/java/com/example/android/codelabs/paging/Injection.kt b/app/src/main/java/com/example/android/codelabs/paging/Injection.kt index c67a1950..619d00c9 100644 --- a/app/src/main/java/com/example/android/codelabs/paging/Injection.kt +++ b/app/src/main/java/com/example/android/codelabs/paging/Injection.kt @@ -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. @@ -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()) } } diff --git a/app/src/main/java/com/example/android/codelabs/paging/api/GithubService.kt b/app/src/main/java/com/example/android/codelabs/paging/api/GithubService.kt index 91114950..1d9180e3 100644 --- a/app/src/main/java/com/example/android/codelabs/paging/api/GithubService.kt +++ b/app/src/main/java/com/example/android/codelabs/paging/api/GithubService.kt @@ -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) -> 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 { - override fun onFailure(call: Call?, t: Throwable) { - Log.d(TAG, "fail to get data") - onError(t.message ?: "unknown error") - } - - override fun onResponse( - call: Call?, - response: Response - ) { - 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. @@ -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 + 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/" diff --git a/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt b/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt index b1b9b6c4..77000d4f 100644 --- a/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt +++ b/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt @@ -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() + + // keep channel of results. The channel allows us to broadcast updates so + // the subscriber will have the latest data + private val searchResults = ConflatedBroadcastChannel() - // LiveData of network errors. - private val networkErrors = MutableLiveData() + // 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 { 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 { + // 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 { it.stars }.thenBy { it.name }) } companion object { diff --git a/app/src/main/java/com/example/android/codelabs/paging/db/GithubLocalCache.kt b/app/src/main/java/com/example/android/codelabs/paging/db/GithubLocalCache.kt deleted file mode 100644 index e427134f..00000000 --- a/app/src/main/java/com/example/android/codelabs/paging/db/GithubLocalCache.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2018 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 android.util.Log -import androidx.lifecycle.LiveData -import com.example.android.codelabs.paging.model.Repo -import java.util.concurrent.Executor - -/** - * Class that handles the DAO local data source. This ensures that methods are triggered on the - * correct executor. - */ -class GithubLocalCache( - private val repoDao: RepoDao, - private val ioExecutor: Executor -) { - - /** - * Insert a list of repos in the database, on a background thread. - */ - fun insert(repos: List, insertFinished: () -> Unit) { - ioExecutor.execute { - Log.d("GithubLocalCache", "inserting ${repos.size} repos") - repoDao.insert(repos) - insertFinished() - } - } - - /** - * Request a LiveData> from the Dao, based on a repo name. If the name contains - * multiple words separated by spaces, then we're emulating the GitHub API behavior and allow - * any characters between the words. - * @param name repository name - */ - fun reposByName(name: String): LiveData> { - // appending '%' so we can allow other characters to be before and after the query string - val query = "%${name.replace(' ', '%')}%" - return repoDao.reposByName(query) - } -} diff --git a/app/src/main/java/com/example/android/codelabs/paging/db/RepoDao.kt b/app/src/main/java/com/example/android/codelabs/paging/db/RepoDao.kt deleted file mode 100644 index 60665fad..00000000 --- a/app/src/main/java/com/example/android/codelabs/paging/db/RepoDao.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2018 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.lifecycle.LiveData -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.example.android.codelabs.paging.model.Repo - -/** - * Room data access object for accessing the [Repo] table. - */ -@Dao -interface RepoDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(posts: List) - - // Do a similar query as the search API: - // Look for repos that contain the query string in the name or in the description - // and order those results descending, by the number of stars and then by name - @Query("SELECT * FROM repos WHERE (name LIKE :queryString) OR (description LIKE " + - ":queryString) ORDER BY stars DESC, name ASC") - fun reposByName(queryString: String): LiveData> -} diff --git a/app/src/main/java/com/example/android/codelabs/paging/db/RepoDatabase.kt b/app/src/main/java/com/example/android/codelabs/paging/db/RepoDatabase.kt deleted file mode 100644 index 2c5db0c0..00000000 --- a/app/src/main/java/com/example/android/codelabs/paging/db/RepoDatabase.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2018 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.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import android.content.Context -import com.example.android.codelabs.paging.model.Repo - -/** - * Database schema that holds the list of repos. - */ -@Database( - entities = [Repo::class], - version = 1, - exportSchema = false -) -abstract class RepoDatabase : RoomDatabase() { - - abstract fun reposDao(): RepoDao - - companion object { - - @Volatile - private var INSTANCE: RepoDatabase? = null - - fun getInstance(context: Context): RepoDatabase = - INSTANCE ?: synchronized(this) { - INSTANCE - ?: buildDatabase(context).also { INSTANCE = it } - } - - private fun buildDatabase(context: Context) = - Room.databaseBuilder(context.applicationContext, - RepoDatabase::class.java, "Github.db") - .build() - } -} diff --git a/app/src/main/java/com/example/android/codelabs/paging/model/Repo.kt b/app/src/main/java/com/example/android/codelabs/paging/model/Repo.kt index 87fbc41a..bb42ab69 100644 --- a/app/src/main/java/com/example/android/codelabs/paging/model/Repo.kt +++ b/app/src/main/java/com/example/android/codelabs/paging/model/Repo.kt @@ -16,8 +16,6 @@ package com.example.android.codelabs.paging.model -import androidx.room.Entity -import androidx.room.PrimaryKey import com.google.gson.annotations.SerializedName /** @@ -26,9 +24,8 @@ import com.google.gson.annotations.SerializedName * with the serialized name. * This class also defines the Room repos table, where the repo [id] is the primary key. */ -@Entity(tableName = "repos") data class Repo( - @PrimaryKey @field:SerializedName("id") val id: Long, + @field:SerializedName("id") val id: Long, @field:SerializedName("name") val name: String, @field:SerializedName("full_name") val fullName: String, @field:SerializedName("description") val description: String?, diff --git a/app/src/main/java/com/example/android/codelabs/paging/model/RepoSearchResult.kt b/app/src/main/java/com/example/android/codelabs/paging/model/RepoSearchResult.kt index 75688b35..d00c1f07 100644 --- a/app/src/main/java/com/example/android/codelabs/paging/model/RepoSearchResult.kt +++ b/app/src/main/java/com/example/android/codelabs/paging/model/RepoSearchResult.kt @@ -16,13 +16,13 @@ package com.example.android.codelabs.paging.model -import androidx.lifecycle.LiveData +import java.lang.Exception /** - * RepoSearchResult from a search, which contains LiveData> holding query data, - * and a LiveData of network error state. + * RepoSearchResult from a search, which contains List holding query data, + * and a String of network error state. */ -data class RepoSearchResult( - val data: LiveData>, - val networkErrors: LiveData -) +sealed class RepoSearchResult { + data class Success(val data: List) : RepoSearchResult() + data class Error(val error: Exception) : RepoSearchResult() +} diff --git a/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt b/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt index 53029c1c..64cad7ae 100644 --- a/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt +++ b/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt @@ -17,65 +17,80 @@ package com.example.android.codelabs.paging.ui import android.os.Bundle -import android.util.Log import android.view.KeyEvent import android.view.View import android.view.inputmethod.EditorInfo import android.widget.Toast import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.observe import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.OnScrollListener import com.example.android.codelabs.paging.Injection -import com.example.android.codelabs.paging.R -import com.example.android.codelabs.paging.model.Repo -import kotlinx.android.synthetic.main.activity_search_repositories.* +import com.example.android.codelabs.paging.databinding.ActivitySearchRepositoriesBinding +import com.example.android.codelabs.paging.model.RepoSearchResult +import kotlinx.coroutines.ExperimentalCoroutinesApi +@ExperimentalCoroutinesApi class SearchRepositoriesActivity : AppCompatActivity() { + private lateinit var binding: ActivitySearchRepositoriesBinding private lateinit var viewModel: SearchRepositoriesViewModel private val adapter = ReposAdapter() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_search_repositories) + binding = ActivitySearchRepositoriesBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) // get the view model - viewModel = ViewModelProviders.of(this, Injection.provideViewModelFactory(this)) + viewModel = ViewModelProvider(this, Injection.provideViewModelFactory()) .get(SearchRepositoriesViewModel::class.java) // add dividers between RecyclerView's row items val decoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) - list.addItemDecoration(decoration) + binding.list.addItemDecoration(decoration) setupScrollListener() initAdapter() val query = savedInstanceState?.getString(LAST_SEARCH_QUERY) ?: DEFAULT_QUERY - viewModel.searchRepo(query) + if (viewModel.repoResult.value == null) { + viewModel.searchRepo(query) + } initSearch(query) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putString(LAST_SEARCH_QUERY, viewModel.lastQueryValue()) + outState.putString(LAST_SEARCH_QUERY, binding.searchRepo.text.trim().toString()) } private fun initAdapter() { - list.adapter = adapter - viewModel.repos.observe(this, Observer> { - Log.d("Activity", "list: ${it?.size}") - showEmptyList(it?.size == 0) - adapter.submitList(it) - }) - viewModel.networkErrors.observe(this, Observer { - Toast.makeText(this, "\uD83D\uDE28 Wooops $it", Toast.LENGTH_LONG).show() - }) + binding.list.adapter = adapter + viewModel.repoResult.observe(this) { result -> + when (result) { + is RepoSearchResult.Success -> { + showEmptyList(result.data.isEmpty()) + adapter.submitList(result.data) + } + is RepoSearchResult.Error -> { + Toast.makeText( + this, + "\uD83D\uDE28 Wooops $result.message}", + Toast.LENGTH_LONG + ).show() + } + } + } } private fun initSearch(query: String) { - search_repo.setText(query) + binding.searchRepo.setText(query) - search_repo.setOnEditorActionListener { _, actionId, _ -> + binding.searchRepo.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_GO) { updateRepoListFromInput() true @@ -83,7 +98,7 @@ class SearchRepositoriesActivity : AppCompatActivity() { false } } - search_repo.setOnKeyListener { _, keyCode, event -> + binding.searchRepo.setOnKeyListener { _, keyCode, event -> if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) { updateRepoListFromInput() true @@ -94,29 +109,28 @@ class SearchRepositoriesActivity : AppCompatActivity() { } private fun updateRepoListFromInput() { - search_repo.text.trim().let { + binding.searchRepo.text.trim().let { if (it.isNotEmpty()) { - list.scrollToPosition(0) + binding.list.scrollToPosition(0) viewModel.searchRepo(it.toString()) - adapter.submitList(null) } } } private fun showEmptyList(show: Boolean) { if (show) { - emptyList.visibility = View.VISIBLE - list.visibility = View.GONE + binding.emptyList.visibility = View.VISIBLE + binding.list.visibility = View.GONE } else { - emptyList.visibility = View.GONE - list.visibility = View.VISIBLE + binding.emptyList.visibility = View.GONE + binding.list.visibility = View.VISIBLE } } private fun setupScrollListener() { - val layoutManager = list.layoutManager as androidx.recyclerview.widget.LinearLayoutManager - list.addOnScrollListener(object : androidx.recyclerview.widget.RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: androidx.recyclerview.widget.RecyclerView, dx: Int, dy: Int) { + val layoutManager = binding.list.layoutManager as LinearLayoutManager + binding.list.addOnScrollListener(object : OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) val totalItemCount = layoutManager.itemCount val visibleItemCount = layoutManager.childCount diff --git a/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesViewModel.kt b/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesViewModel.kt index 4034f91e..e7748559 100644 --- a/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesViewModel.kt +++ b/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesViewModel.kt @@ -16,18 +16,19 @@ package com.example.android.codelabs.paging.ui -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Transformations -import androidx.lifecycle.ViewModel +import androidx.lifecycle.* import com.example.android.codelabs.paging.data.GithubRepository -import com.example.android.codelabs.paging.model.Repo import com.example.android.codelabs.paging.model.RepoSearchResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.launch /** * ViewModel for the [SearchRepositoriesActivity] screen. * The ViewModel works with the [GithubRepository] to get the data. */ +@ExperimentalCoroutinesApi class SearchRepositoriesViewModel(private val repository: GithubRepository) : ViewModel() { companion object { @@ -35,13 +36,11 @@ class SearchRepositoriesViewModel(private val repository: GithubRepository) : Vi } private val queryLiveData = MutableLiveData() - private val repoResult: LiveData = Transformations.map(queryLiveData) { - repository.search(it) - } - - val repos: LiveData> = Transformations.switchMap(repoResult) { it -> it.data } - val networkErrors: LiveData = Transformations.switchMap(repoResult) { it -> - it.networkErrors + val repoResult: LiveData = queryLiveData.switchMap { queryString -> + liveData { + val repos = repository.getSearchResultStream(queryString).asLiveData(Dispatchers.Main) + emitSource(repos) + } } /** @@ -53,15 +52,12 @@ class SearchRepositoriesViewModel(private val repository: GithubRepository) : Vi fun listScrolled(visibleItemCount: Int, lastVisibleItemPosition: Int, totalItemCount: Int) { if (visibleItemCount + lastVisibleItemPosition + VISIBLE_THRESHOLD >= totalItemCount) { - val immutableQuery = lastQueryValue() + val immutableQuery = queryLiveData.value if (immutableQuery != null) { - repository.requestMore(immutableQuery) + viewModelScope.launch { + repository.requestMore(immutableQuery) + } } } } - - /** - * Get the last query value. - */ - fun lastQueryValue(): String? = queryLiveData.value -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android/codelabs/paging/ui/UiUtils.kt b/app/src/main/java/com/example/android/codelabs/paging/ui/UiUtils.kt new file mode 100644 index 00000000..ddf003b1 --- /dev/null +++ b/app/src/main/java/com/example/android/codelabs/paging/ui/UiUtils.kt @@ -0,0 +1,9 @@ +package com.example.android.codelabs.paging.ui + +import android.view.View + +fun toVisibility(constraint: Boolean): Int = if (constraint) { + View.VISIBLE +} else { + View.GONE +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search_repositories.xml b/app/src/main/res/layout/activity_search_repositories.xml index fbeef8b2..738040a3 100644 --- a/app/src/main/res/layout/activity_search_repositories.xml +++ b/app/src/main/res/layout/activity_search_repositories.xml @@ -31,6 +31,7 @@ android:layout_marginTop="8dp" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -41,6 +42,7 @@ android:hint="@string/search_hint" android:imeOptions="actionSearch" android:inputType="textNoSuggestions" + android:selectAllOnFocus="true" tools:text="Android"/> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 066c3cc0..de598ecb 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -22,4 +22,6 @@ #1F75FE #551F75FE #55999999 + #AAAAAA + #FFFFFF diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 3dc950c3..936be331 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -23,4 +23,5 @@ 24dp 36dp 32dp + 24sp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a9ed9fe1..cb6aa759 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,4 +21,5 @@ GitHub Repository No results 😓 \? + Retry diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index d0c539d1..dd6eb3f8 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -17,7 +17,7 @@ -