From 21e4d78c95b846cb7c6719f9e34ce704562262ce Mon Sep 17 00:00:00 2001 From: Florina Muntenescu Date: Sat, 14 Apr 2018 14:46:35 +0100 Subject: [PATCH 1/3] Paging codelab - solution --- .../codelabs/paging/data/GithubRepository.kt | 49 ++++------- .../paging/data/RepoBoundaryCallback.kt | 83 +++++++++++++++++++ .../codelabs/paging/db/GithubLocalCache.kt | 5 +- .../android/codelabs/paging/db/RepoDao.kt | 4 +- .../codelabs/paging/model/RepoSearchResult.kt | 3 +- .../codelabs/paging/ui/ReposAdapter.kt | 4 +- .../paging/ui/SearchRepositoriesActivity.kt | 18 +--- .../paging/ui/SearchRepositoriesViewModel.kt | 16 +--- 8 files changed, 110 insertions(+), 72 deletions(-) create mode 100644 app/src/main/java/com/example/android/codelabs/paging/data/RepoBoundaryCallback.kt 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 267f3fcb..b01973a4 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 @@ -16,10 +16,9 @@ package com.example.android.codelabs.paging.data -import android.arch.lifecycle.MutableLiveData +import android.arch.paging.LivePagedListBuilder import android.util.Log 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.model.RepoSearchResult @@ -31,49 +30,31 @@ class GithubRepository( private val cache: GithubLocalCache ) { - // keep the last requested page. When the request is successful, increment the page number. - private var lastRequestedPage = 1 - - // LiveData of network errors. - private val networkErrors = MutableLiveData() - - // avoid triggering multiple requests in the same time - private var isRequestInProgress = false - /** * Search repositories whose names match the query. */ fun search(query: String): RepoSearchResult { Log.d("GithubRepository", "New query: $query") - lastRequestedPage = 1 - requestAndSaveData(query) - - // Get data from the local cache - val data = cache.reposByName(query) - return RepoSearchResult(data, networkErrors) - } + // Get data source factory from the local cache + val dataSourceFactory = cache.reposByName(query) - fun requestMore(query: String) { - requestAndSaveData(query) - } + // every new query creates a new BoundaryCallback + // The BoundaryCallback will observe when the user reaches to the edges of + // the list and update the database with extra data + val boundaryCallback = RepoBoundaryCallback(query, service, cache) + val networkErrors = boundaryCallback.networkErrors - private fun requestAndSaveData(query: String) { - if (isRequestInProgress) return + // Get the paged list + val data = LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE) + .setBoundaryCallback(boundaryCallback) + .build() - isRequestInProgress = true - searchRepos(service, query, lastRequestedPage, NETWORK_PAGE_SIZE, { repos -> - cache.insert(repos, { - lastRequestedPage++ - isRequestInProgress = false - }) - }, { error -> - networkErrors.postValue(error) - isRequestInProgress = false - }) + // Get the network errors exposed by the boundary callback + return RepoSearchResult(data, networkErrors) } companion object { - private const val NETWORK_PAGE_SIZE = 50 + private const val DATABASE_PAGE_SIZE = 20 } } \ No newline at end of file diff --git a/app/src/main/java/com/example/android/codelabs/paging/data/RepoBoundaryCallback.kt b/app/src/main/java/com/example/android/codelabs/paging/data/RepoBoundaryCallback.kt new file mode 100644 index 00000000..7db1cc80 --- /dev/null +++ b/app/src/main/java/com/example/android/codelabs/paging/data/RepoBoundaryCallback.kt @@ -0,0 +1,83 @@ +/* + * 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 android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import android.arch.paging.PagedList +import android.util.Log +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.model.Repo + +/** + * This boundary callback gets notified when user reaches to the edges of the list for example when + * the database cannot provide any more data. + **/ +class RepoBoundaryCallback( + private val query: String, + private val service: GithubService, + private val cache: GithubLocalCache +) : PagedList.BoundaryCallback() { + + companion object { + private const val NETWORK_PAGE_SIZE = 50 + } + + // keep the last requested page. When the request is successful, increment the page number. + private var lastRequestedPage = 1 + + private val _networkErrors = MutableLiveData() + // LiveData of network errors. + val networkErrors: LiveData + get() = _networkErrors + + // avoid triggering multiple requests in the same time + private var isRequestInProgress = false + + /** + * Database returned 0 items. We should query the backend for more items. + */ + override fun onZeroItemsLoaded() { + Log.d("RepoBoundaryCallback", "onZeroItemsLoaded") + requestAndSaveData(query) + } + + /** + * When all items in the database were loaded, we need to query the backend for more items. + */ + override fun onItemAtEndLoaded(itemAtEnd: Repo) { + Log.d("RepoBoundaryCallback", "onItemAtEndLoaded") + requestAndSaveData(query) + } + + private fun requestAndSaveData(query: String) { + if (isRequestInProgress) return + + isRequestInProgress = true + searchRepos(service, query, lastRequestedPage, NETWORK_PAGE_SIZE, { repos -> + cache.insert(repos, { + lastRequestedPage++ + isRequestInProgress = false + }) + }, { error -> + _networkErrors.postValue(error) + isRequestInProgress = false + }) + } +} \ No newline at end of file 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 index a014fd2f..b4fd025f 100644 --- 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 @@ -16,7 +16,6 @@ package com.example.android.codelabs.paging.db -import android.arch.lifecycle.LiveData import android.arch.paging.DataSource import android.util.Log import com.example.android.codelabs.paging.model.Repo @@ -34,7 +33,7 @@ class GithubLocalCache( /** * Insert a list of repos in the database, on a background thread. */ - fun insert(repos: List, insertFinished: ()-> Unit) { + fun insert(repos: List, insertFinished: () -> Unit) { ioExecutor.execute { Log.d("GithubLocalCache", "inserting ${repos.size} repos") repoDao.insert(repos) @@ -48,7 +47,7 @@ class GithubLocalCache( * any characters between the words. * @param name repository name */ - fun reposByName(name: String): LiveData> { + fun reposByName(name: String): DataSource.Factory { // 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 index f5a371f7..a9a1fa66 100644 --- 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 @@ -16,7 +16,7 @@ package com.example.android.codelabs.paging.db -import android.arch.lifecycle.LiveData +import android.arch.paging.DataSource import android.arch.persistence.room.Dao import android.arch.persistence.room.Insert import android.arch.persistence.room.OnConflictStrategy @@ -37,6 +37,6 @@ interface RepoDao { // 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> + fun reposByName(queryString: String): DataSource.Factory } \ No newline at end of file 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 3654fb5e..32c31b77 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 @@ -17,12 +17,13 @@ package com.example.android.codelabs.paging.model import android.arch.lifecycle.LiveData +import android.arch.paging.PagedList /** * RepoSearchResult from a search, which contains LiveData> holding query data, * and a LiveData of network error state. */ data class RepoSearchResult( - val data: LiveData>, + val data: LiveData>, val networkErrors: LiveData ) \ No newline at end of file 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 5c23d30b..81f60e2a 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 @@ -16,7 +16,7 @@ package com.example.android.codelabs.paging.ui -import android.support.v7.recyclerview.extensions.ListAdapter +import android.arch.paging.PagedListAdapter import android.support.v7.util.DiffUtil import android.support.v7.widget.RecyclerView import android.view.ViewGroup @@ -25,7 +25,7 @@ import com.example.android.codelabs.paging.model.Repo /** * Adapter for the list of repositories. */ -class ReposAdapter : ListAdapter(REPO_COMPARATOR) { +class ReposAdapter : PagedListAdapter(REPO_COMPARATOR) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return RepoViewHolder.create(parent) 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 097b145b..e6eb00af 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 @@ -18,6 +18,7 @@ package com.example.android.codelabs.paging.ui import android.arch.lifecycle.Observer import android.arch.lifecycle.ViewModelProviders +import android.arch.paging.PagedList import android.os.Bundle import android.support.v7.app.AppCompatActivity import android.support.v7.widget.DividerItemDecoration @@ -50,7 +51,6 @@ class SearchRepositoriesActivity : AppCompatActivity() { // add dividers between RecyclerView's row items val decoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) list.addItemDecoration(decoration) - setupScrollListener() initAdapter() val query = savedInstanceState?.getString(LAST_SEARCH_QUERY) ?: DEFAULT_QUERY @@ -65,7 +65,7 @@ class SearchRepositoriesActivity : AppCompatActivity() { private fun initAdapter() { list.adapter = adapter - viewModel.repos.observe(this, Observer> { + viewModel.repos.observe(this, Observer> { Log.d("Activity", "list: ${it?.size}") showEmptyList(it?.size == 0) adapter.submitList(it) @@ -116,20 +116,6 @@ class SearchRepositoriesActivity : AppCompatActivity() { } } - private fun setupScrollListener() { - val layoutManager = list.layoutManager as LinearLayoutManager - list.addOnScrollListener(object : RecyclerView.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 29c380df..72cfe533 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 @@ -20,6 +20,7 @@ import android.arch.lifecycle.LiveData import android.arch.lifecycle.MutableLiveData import android.arch.lifecycle.Transformations import android.arch.lifecycle.ViewModel +import android.arch.paging.PagedList import com.example.android.codelabs.paging.data.GithubRepository import com.example.android.codelabs.paging.model.Repo import com.example.android.codelabs.paging.model.RepoSearchResult @@ -30,16 +31,12 @@ import com.example.android.codelabs.paging.model.RepoSearchResult */ class SearchRepositoriesViewModel(private val repository: GithubRepository) : ViewModel() { - companion object { - private const val VISIBLE_THRESHOLD = 5 - } - private val queryLiveData = MutableLiveData() private val repoResult: LiveData = Transformations.map(queryLiveData, { repository.search(it) }) - val repos: LiveData> = Transformations.switchMap(repoResult, + val repos: LiveData> = Transformations.switchMap(repoResult, { it -> it.data }) val networkErrors: LiveData = Transformations.switchMap(repoResult, { it -> it.networkErrors }) @@ -51,15 +48,6 @@ class SearchRepositoriesViewModel(private val repository: GithubRepository) : Vi queryLiveData.postValue(queryString) } - fun listScrolled(visibleItemCount: Int, lastVisibleItemPosition: Int, totalItemCount: Int) { - if (visibleItemCount + lastVisibleItemPosition + VISIBLE_THRESHOLD >= totalItemCount) { - val immutableQuery = lastQueryValue() - if (immutableQuery != null) { - repository.requestMore(immutableQuery) - } - } - } - /** * Get the last query value. */ From f11e42edaa0cbca5a4ff601b28c6861f1b25a361 Mon Sep 17 00:00:00 2001 From: XinyueZ Date: Mon, 23 Jul 2018 13:06:13 +0200 Subject: [PATCH 2/3] Code optimise - For warnings of lint, IDE - Add clearFindViewByIdCache() in onDestroy() --- .../codelabs/paging/data/RepoBoundaryCallback.kt | 7 ++----- .../paging/ui/SearchRepositoriesActivity.kt | 14 ++++++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/example/android/codelabs/paging/data/RepoBoundaryCallback.kt b/app/src/main/java/com/example/android/codelabs/paging/data/RepoBoundaryCallback.kt index 7db1cc80..ade2948c 100644 --- a/app/src/main/java/com/example/android/codelabs/paging/data/RepoBoundaryCallback.kt +++ b/app/src/main/java/com/example/android/codelabs/paging/data/RepoBoundaryCallback.kt @@ -16,9 +16,6 @@ package com.example.android.codelabs.paging.data -import android.arch.lifecycle.LiveData -import android.arch.lifecycle.MutableLiveData -import android.arch.paging.PagedList import android.util.Log import com.example.android.codelabs.paging.api.GithubService import com.example.android.codelabs.paging.api.searchRepos @@ -71,10 +68,10 @@ class RepoBoundaryCallback( isRequestInProgress = true searchRepos(service, query, lastRequestedPage, NETWORK_PAGE_SIZE, { repos -> - cache.insert(repos, { + cache.insert(repos) { lastRequestedPage++ isRequestInProgress = false - }) + } }, { error -> _networkErrors.postValue(error) isRequestInProgress = false 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 e6eb00af..80307c4c 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 @@ -32,6 +32,7 @@ import android.widget.Toast import com.example.android.codelabs.paging.R import com.example.android.codelabs.paging.Injection import com.example.android.codelabs.paging.model.Repo +import kotlinx.android.synthetic.clearFindViewByIdCache import kotlinx.android.synthetic.main.activity_search_repositories.* @@ -58,6 +59,11 @@ class SearchRepositoriesActivity : AppCompatActivity() { initSearch(query) } + override fun onDestroy() { + super.onDestroy() + clearFindViewByIdCache() + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString(LAST_SEARCH_QUERY, viewModel.lastQueryValue()) @@ -78,22 +84,22 @@ class SearchRepositoriesActivity : AppCompatActivity() { private fun initSearch(query: String) { search_repo.setText(query) - search_repo.setOnEditorActionListener({ _, actionId, _ -> + search_repo.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_GO) { updateRepoListFromInput() true } else { false } - }) - search_repo.setOnKeyListener({ _, keyCode, event -> + } + search_repo.setOnKeyListener { _, keyCode, event -> if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) { updateRepoListFromInput() true } else { false } - }) + } } private fun updateRepoListFromInput() { From 37546054d0c3765150d3efff639df099a3a7eb38 Mon Sep 17 00:00:00 2001 From: XinyueZ Date: Mon, 23 Jul 2018 13:13:55 +0200 Subject: [PATCH 3/3] Fix imports --- .../android/codelabs/paging/data/RepoBoundaryCallback.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/example/android/codelabs/paging/data/RepoBoundaryCallback.kt b/app/src/main/java/com/example/android/codelabs/paging/data/RepoBoundaryCallback.kt index ade2948c..47409a1a 100644 --- a/app/src/main/java/com/example/android/codelabs/paging/data/RepoBoundaryCallback.kt +++ b/app/src/main/java/com/example/android/codelabs/paging/data/RepoBoundaryCallback.kt @@ -16,6 +16,9 @@ package com.example.android.codelabs.paging.data +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import android.arch.paging.PagedList import android.util.Log import com.example.android.codelabs.paging.api.GithubService import com.example.android.codelabs.paging.api.searchRepos @@ -77,4 +80,4 @@ class RepoBoundaryCallback( isRequestInProgress = false }) } -} \ No newline at end of file +}