From 0991574e5e3c45169790e209ec7f94a625ef27bc Mon Sep 17 00:00:00 2001 From: Florina Muntenescu Date: Mon, 4 May 2020 09:31:57 +0100 Subject: [PATCH 01/10] Libraries bump and removing Room annotations from Repo --- app/build.gradle | 2 +- .../codelabs/paging/api/GithubService.kt | 2 +- .../codelabs/paging/data/GithubRepository.kt | 22 +++++-------------- .../android/codelabs/paging/model/Repo.kt | 5 +---- .../android/codelabs/paging/ui/UiUtils.kt | 9 ++++++++ build.gradle | 16 +++++++------- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 7 files changed, 27 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/com/example/android/codelabs/paging/ui/UiUtils.kt diff --git a/app/build.gradle b/app/build.gradle index 7ad34dd9..8d7d5419 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -67,8 +67,8 @@ dependencies { 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:$lifecycleVersion" kapt "androidx.room:room-compiler:$roomVersion" // retrofit 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 eb36bddc..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 @@ -39,7 +39,7 @@ interface GithubService { @Query("q") query: String, @Query("page") page: Int, @Query("per_page") itemsPerPage: Int - ): Response + ): 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 7ebed906..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 @@ -85,23 +85,11 @@ class GithubRepository(private val service: GithubService) { try { val response = service.searchRepos(apiQuery, lastRequestedPage, NETWORK_PAGE_SIZE) Log.d("GithubRepository", "response $response") - if (response.isSuccessful) { - if (response.isSuccessful) { - val repos = response.body()?.items ?: emptyList() - inMemoryCache.addAll(repos) - val reposByName = reposByName(query) - searchResults.offer(RepoSearchResult.Success(reposByName)) - successful = true - } else { - Log.d("GithubRepository", "fail to get data") - searchResults.offer(RepoSearchResult.Error(IOException(response.message() - ?: "Unknown error"))) - } - } else { - Log.d("GithubRepository", "fail to get data") - searchResults.offer(RepoSearchResult.Error(IOException(response.message() - ?: "Unknown error"))) - } + 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) { 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/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/build.gradle b/build.gradle index 130aa572..764fcd27 100644 --- a/build.gradle +++ b/build.gradle @@ -17,13 +17,13 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.70' + ext.kotlin_version = '1.3.71' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' + classpath 'com.android.tools.build:gradle:4.0.0-rc01' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong @@ -35,7 +35,7 @@ allprojects { repositories { google() jcenter() - maven { url 'https://androidx-dev-prod.appspot.com/snapshots/builds/6295087/artifacts/repository' } + maven { url 'https://androidx-dev-prod.appspot.com/snapshots/builds/6535997/artifacts/repository' } } } @@ -48,18 +48,18 @@ ext { minSdkVersion = 15 targetSdkVersion = 28 supportLibVersion = '1.1.0' - recyclerViewVersion = '1.2.0-SNAPSHOT' + recyclerViewVersion = '1.2.0-alpha03' constraintLayoutVersion = '1.1.3' materialVersion = '1.1.0' lifecycleVersion = '2.2.0' - roomVersion = '2.2.4' + roomVersion = '2.3.0-SNAPSHOT' pagingVersion = '3.0.0-SNAPSHOT' retrofitVersion = '2.7.2' okhttpLoggingInterceptorVersion = '4.0.0' coroutines = '1.3.4' - runnerVersion = '1.3.0-alpha03' + runnerVersion = '1.3.0-beta01' rulesVersion = '1.0.1' - junitVersion = '4.12' - espressoVersion = '3.3.0-alpha03' + junitVersion = '4.13' + espressoVersion = '3.3.0-beta01' } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ae16ddc..e7bfeb95 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Jan 28 21:53:17 GMT 2020 +#Mon May 04 09:17:05 BST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-rc-1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip From 8f899f849151e31553af96e15f6940aab7c8cf05 Mon Sep 17 00:00:00 2001 From: Florina Muntenescu Date: Mon, 4 May 2020 11:01:00 +0100 Subject: [PATCH 02/10] Steps 5-9 Migrating to Paging 3.0 --- .../paging/data/GithubPagingSource.kt | 50 +++++++++++ .../codelabs/paging/data/GithubRepository.kt | 83 ++----------------- .../codelabs/paging/ui/ReposAdapter.kt | 9 +- .../paging/ui/SearchRepositoriesActivity.kt | 62 +++++--------- .../paging/ui/SearchRepositoriesViewModel.kt | 40 ++++----- 5 files changed, 98 insertions(+), 146 deletions(-) create mode 100644 app/src/main/java/com/example/android/codelabs/paging/data/GithubPagingSource.kt diff --git a/app/src/main/java/com/example/android/codelabs/paging/data/GithubPagingSource.kt b/app/src/main/java/com/example/android/codelabs/paging/data/GithubPagingSource.kt new file mode 100644 index 00000000..5e7dda97 --- /dev/null +++ b/app/src/main/java/com/example/android/codelabs/paging/data/GithubPagingSource.kt @@ -0,0 +1,50 @@ +/* + * 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.data + +import androidx.paging.PagingSource +import com.example.android.codelabs.paging.api.GithubService +import com.example.android.codelabs.paging.api.IN_QUALIFIER +import com.example.android.codelabs.paging.model.Repo +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 + +class GithubPagingSource( + private val service: GithubService, + private val query: String +) : PagingSource() { + override suspend fun load(params: LoadParams): LoadResult { + val position = params.key ?: GITHUB_STARTING_PAGE_INDEX + val apiQuery = query + IN_QUALIFIER + return try { + val response = service.searchRepos(apiQuery, position, params.loadSize) + val repos = response.items + LoadResult.Page( + data = repos, + prevKey = if (position == GITHUB_STARTING_PAGE_INDEX) null else position - 1, + nextKey = if (repos.isEmpty()) null else position + 1 + ) + } catch (exception: IOException) { + LoadResult.Error(exception) + } catch (exception: HttpException) { + LoadResult.Error(exception) + } + } +} 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 77000d4f..1b18eb34 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,95 +17,28 @@ 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.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() - - // keep channel of results. The channel allows us to broadcast updates so - // the subscriber will have the latest data - private val searchResults = ConflatedBroadcastChannel() - - // 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, exposed as a stream of data that will emit * every time we get more data from the network. */ - suspend fun getSearchResultStream(query: String): Flow { + fun getSearchResultStream(query: String): Flow> { 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 { - // 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 }) + return Pager( + config = PagingConfig(pageSize = NETWORK_PAGE_SIZE), + pagingSourceFactory = { GithubPagingSource(service, query) } + ).flow } companion object { diff --git a/app/src/main/java/com/example/android/codelabs/paging/ui/ReposAdapter.kt b/app/src/main/java/com/example/android/codelabs/paging/ui/ReposAdapter.kt index d959d265..3a5a4e73 100644 --- a/app/src/main/java/com/example/android/codelabs/paging/ui/ReposAdapter.kt +++ b/app/src/main/java/com/example/android/codelabs/paging/ui/ReposAdapter.kt @@ -17,20 +17,21 @@ package com.example.android.codelabs.paging.ui import android.view.ViewGroup +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.example.android.codelabs.paging.model.Repo /** * Adapter for the list of repositories. */ -class ReposAdapter : ListAdapter(REPO_COMPARATOR) { +class ReposAdapter : PagingDataAdapter(REPO_COMPARATOR) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): androidx.recyclerview.widget.RecyclerView.ViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return RepoViewHolder.create(parent) } - override fun onBindViewHolder(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder, position: Int) { + override fun onBindViewHolder(holder: ViewHolder, position: Int) { val repoItem = getItem(position) if (repoItem != null) { (holder as RepoViewHolder).bind(repoItem) 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 7b82d6e7..5b553b97 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 @@ -20,18 +20,17 @@ import android.os.Bundle 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.ViewModelProvider -import androidx.lifecycle.observe +import androidx.lifecycle.lifecycleScope 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.databinding.ActivitySearchRepositoriesBinding -import com.example.android.codelabs.paging.model.RepoSearchResult import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch @ExperimentalCoroutinesApi class SearchRepositoriesActivity : AppCompatActivity() { @@ -40,6 +39,18 @@ class SearchRepositoriesActivity : AppCompatActivity() { private lateinit var viewModel: SearchRepositoriesViewModel private val adapter = ReposAdapter() + private var searchJob: Job? = null + + private fun search(query: String) { + // Make sure we cancel the previous job before creating a new one + searchJob?.cancel() + searchJob = lifecycleScope.launch { + viewModel.searchRepo(query).collectLatest { + adapter.submitData(it) + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivitySearchRepositoriesBinding.inflate(layoutInflater) @@ -53,13 +64,10 @@ class SearchRepositoriesActivity : AppCompatActivity() { // add dividers between RecyclerView's row items val decoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) binding.list.addItemDecoration(decoration) - setupScrollListener() initAdapter() val query = savedInstanceState?.getString(LAST_SEARCH_QUERY) ?: DEFAULT_QUERY - if (viewModel.repoResult.value == null) { - viewModel.searchRepo(query) - } + search(query) initSearch(query) } @@ -70,21 +78,6 @@ class SearchRepositoriesActivity : AppCompatActivity() { private fun initAdapter() { 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) { @@ -111,9 +104,8 @@ class SearchRepositoriesActivity : AppCompatActivity() { private fun updateRepoListFromInput() { binding.searchRepo.text.trim().let { if (it.isNotEmpty()) { - // binding.list.scrollToPosition(0) - viewModel.searchRepo(it.toString()) - //adapter.submitList(null) + binding.list.scrollToPosition(0) + search(it.toString()) } } } @@ -128,20 +120,6 @@ class SearchRepositoriesActivity : AppCompatActivity() { } } - private fun setupScrollListener() { - 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 - val lastVisibleItem = layoutManager.findLastVisibleItemPosition() - - viewModel.listScrolled(visibleItemCount, lastVisibleItem, totalItemCount) - } - }) - } - companion object { private const val LAST_SEARCH_QUERY: String = "last_search_query" private const val DEFAULT_QUERY = "Android" 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 e7748559..cdb849b0 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 @@ -17,11 +17,15 @@ package com.example.android.codelabs.paging.ui import androidx.lifecycle.* +import androidx.paging.PagingData +import androidx.paging.cachedIn 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.flow.Flow import kotlinx.coroutines.launch /** @@ -30,34 +34,20 @@ import kotlinx.coroutines.launch */ @ExperimentalCoroutinesApi class SearchRepositoriesViewModel(private val repository: GithubRepository) : ViewModel() { + private var currentQueryValue: String? = null - companion object { - private const val VISIBLE_THRESHOLD = 5 - } + private var currentSearchResult: Flow>? = null - private val queryLiveData = MutableLiveData() - val repoResult: LiveData = queryLiveData.switchMap { queryString -> - liveData { - val repos = repository.getSearchResultStream(queryString).asLiveData(Dispatchers.Main) - emitSource(repos) + fun searchRepo(queryString: String): Flow> { + val lastResult = currentSearchResult + if (queryString == currentQueryValue && lastResult != null) { + return lastResult } + currentQueryValue = queryString + val newResult: Flow> = repository.getSearchResultStream(queryString) + .cachedIn(viewModelScope) + currentSearchResult = newResult + return newResult } - /** - * Search a repository based on a query string. - */ - fun searchRepo(queryString: String) { - queryLiveData.postValue(queryString) - } - - fun listScrolled(visibleItemCount: Int, lastVisibleItemPosition: Int, totalItemCount: Int) { - if (visibleItemCount + lastVisibleItemPosition + VISIBLE_THRESHOLD >= totalItemCount) { - val immutableQuery = queryLiveData.value - if (immutableQuery != null) { - viewModelScope.launch { - repository.requestMore(immutableQuery) - } - } - } - } } \ No newline at end of file From 8a13e54012bc099914094b0c6c93b593aae9f869 Mon Sep 17 00:00:00 2001 From: Florina Muntenescu Date: Mon, 4 May 2020 11:01:00 +0100 Subject: [PATCH 03/10] Steps 5-9 Migrating to Paging 3.0 --- .../android/codelabs/paging/ui/SearchRepositoriesActivity.kt | 1 - 1 file changed, 1 deletion(-) 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 5b553b97..574be5fc 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 @@ -28,7 +28,6 @@ import com.example.android.codelabs.paging.Injection import com.example.android.codelabs.paging.databinding.ActivitySearchRepositoriesBinding import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch From 514b9d83c4af0ad3d5540d5e94a516b44077e3a2 Mon Sep 17 00:00:00 2001 From: Florina Muntenescu Date: Mon, 4 May 2020 11:25:29 +0100 Subject: [PATCH 04/10] Adding a loading state footer --- .../paging/ui/ReposLoadStateAdapter.kt | 33 +++++++++++ .../paging/ui/ReposLoadStateViewHolder.kt | 55 +++++++++++++++++++ .../paging/ui/SearchRepositoriesActivity.kt | 5 +- .../repos_load_state_footer_view_item.xml | 45 +++++++++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/example/android/codelabs/paging/ui/ReposLoadStateAdapter.kt create mode 100644 app/src/main/java/com/example/android/codelabs/paging/ui/ReposLoadStateViewHolder.kt create mode 100644 app/src/main/res/layout/repos_load_state_footer_view_item.xml diff --git a/app/src/main/java/com/example/android/codelabs/paging/ui/ReposLoadStateAdapter.kt b/app/src/main/java/com/example/android/codelabs/paging/ui/ReposLoadStateAdapter.kt new file mode 100644 index 00000000..1350bd66 --- /dev/null +++ b/app/src/main/java/com/example/android/codelabs/paging/ui/ReposLoadStateAdapter.kt @@ -0,0 +1,33 @@ +/* + * 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.ui + +import android.view.ViewGroup +import androidx.paging.LoadState +import androidx.paging.LoadStateAdapter + +class ReposLoadStateAdapter( + private val retry: () -> Unit +) : LoadStateAdapter() { + override fun onBindViewHolder(holder: ReposLoadStateViewHolder, loadState: LoadState) { + holder.bind(loadState) + } + + override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ReposLoadStateViewHolder { + return ReposLoadStateViewHolder.create(parent, retry) + } +} diff --git a/app/src/main/java/com/example/android/codelabs/paging/ui/ReposLoadStateViewHolder.kt b/app/src/main/java/com/example/android/codelabs/paging/ui/ReposLoadStateViewHolder.kt new file mode 100644 index 00000000..d5c82175 --- /dev/null +++ b/app/src/main/java/com/example/android/codelabs/paging/ui/ReposLoadStateViewHolder.kt @@ -0,0 +1,55 @@ +/* + * 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.ui + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.paging.LoadState +import androidx.recyclerview.widget.RecyclerView +import com.example.android.codelabs.paging.R +import com.example.android.codelabs.paging.databinding.ReposLoadStateFooterViewItemBinding + +class ReposLoadStateViewHolder( + private val binding: ReposLoadStateFooterViewItemBinding, + retry: () -> Unit +) : RecyclerView.ViewHolder(binding.root) { + + init { + binding.retryButton.also { + it.setOnClickListener { retry.invoke() } + } + } + + fun bind(loadState: LoadState) { + if (loadState is LoadState.Error) { + binding.errorMsg.text = loadState.error.localizedMessage + } + binding.progressBar.visibility = toVisibility(loadState is LoadState.Loading) + binding.retryButton.visibility = toVisibility(loadState !is LoadState.Loading) + binding.errorMsg.visibility = toVisibility(loadState !is LoadState.Loading) + } + + companion object { + fun create(parent: ViewGroup, retry: () -> Unit): ReposLoadStateViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.repos_load_state_footer_view_item, parent, false) + val binding = ReposLoadStateFooterViewItemBinding.bind(view) + return ReposLoadStateViewHolder(binding, retry) + } + } +} 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 574be5fc..47897c5c 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 @@ -76,7 +76,10 @@ class SearchRepositoriesActivity : AppCompatActivity() { } private fun initAdapter() { - binding.list.adapter = adapter + binding.list.adapter = adapter.withLoadStateHeaderAndFooter( + header = ReposLoadStateAdapter { adapter.retry() }, + footer = ReposLoadStateAdapter { adapter.retry() } + ) } private fun initSearch(query: String) { diff --git a/app/src/main/res/layout/repos_load_state_footer_view_item.xml b/app/src/main/res/layout/repos_load_state_footer_view_item.xml new file mode 100644 index 00000000..2183cd29 --- /dev/null +++ b/app/src/main/res/layout/repos_load_state_footer_view_item.xml @@ -0,0 +1,45 @@ + + + + + + +