Skip to content
Closed
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
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ android {
jvmTarget = "1.8"
}

buildFeatures {
viewBinding = true
viewBinding {
enabled = true
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import java.io.IOException

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

@ExperimentalCoroutinesApi
@FlowPreview
class GithubPagingSource(
private val service: GithubService,
private val query: String
) : PagingSource<Int, Repo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
val position = params.key ?: GITHUB_STARTING_PAGE_INDEX
val apiQuery = query + IN_QUALIFIER
val apiResponse = service.searchRepos(apiQuery, position, GithubRepository.NETWORK_PAGE_SIZE)
return if (apiResponse.isSuccessful) {
val repos = apiResponse.body()?.items ?: emptyList()
LoadResult.Page(
data = repos,
prevKey = if (position == GITHUB_STARTING_PAGE_INDEX) null else -1,
// if we don't get any results, we consider that we're at the last page
nextKey = if (repos.isEmpty()) null else position + 1
)
} else {
LoadResult.Error(IOException(apiResponse.message()))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,14 @@
package com.example.android.codelabs.paging.data

import android.util.Log
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingDataFlow
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 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.
Expand All @@ -38,73 +33,20 @@ private const val GITHUB_STARTING_PAGE_INDEX = 1
@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

/**
* 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
requestAndSaveData(query)

return searchResults.asFlow()
}

suspend fun requestMore(query: String) {
requestAndSaveData(query)
}

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

isRequestInProgress = true

val apiQuery = query + IN_QUALIFIER
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))
} 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")))
}
lastRequestedPage++
isRequestInProgress = false
}

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 })
return PagingDataFlow(
config = PagingConfig(pageSize = NETWORK_PAGE_SIZE),
pagingSourceFactory = { GithubPagingSource(service, query) }
)
}

companion object {
private const val NETWORK_PAGE_SIZE = 50
const val NETWORK_PAGE_SIZE = 50
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,53 @@
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 com.example.android.codelabs.paging.model.Repo
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.example.android.codelabs.paging.R

/**
* Adapter for the list of repositories.
*/
class ReposAdapter : ListAdapter<Repo, androidx.recyclerview.widget.RecyclerView.ViewHolder>(REPO_COMPARATOR) {
class ReposAdapter : PagingDataAdapter<UiModel, ViewHolder>(UIMODEL_COMPARATOR) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): androidx.recyclerview.widget.RecyclerView.ViewHolder {
return RepoViewHolder.create(parent)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return if(viewType == R.layout.repo_view_item) {
RepoViewHolder.create(parent)
} else {
SeparatorViewHolder.create(parent)
}
}

override fun getItemViewType(position: Int): Int {
return when(getItem(position)){
is UiModel.RepoItem -> R.layout.repo_view_item
is UiModel.SeparatorItem -> R.layout.separator_view_item
null -> 0
}
}

override fun onBindViewHolder(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder, position: Int) {
val repoItem = getItem(position)
if (repoItem != null) {
(holder as RepoViewHolder).bind(repoItem)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val uiModel = getItem(position)
uiModel.let {
when(uiModel){
is UiModel.RepoItem -> (holder as RepoViewHolder).bind(uiModel.repo)
is UiModel.SeparatorItem -> (holder as SeparatorViewHolder).bind(uiModel.description)
}
}
}

companion object {
private val REPO_COMPARATOR = object : DiffUtil.ItemCallback<Repo>() {
override fun areItemsTheSame(oldItem: Repo, newItem: Repo): Boolean =
oldItem.fullName == newItem.fullName
private val UIMODEL_COMPARATOR = object : DiffUtil.ItemCallback<UiModel>() {
override fun areItemsTheSame(oldItem: UiModel, newItem: UiModel): Boolean {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will make separators always crossfade, this should check for SeparatorItem too.

return if (oldItem is UiModel.RepoItem && newItem is UiModel.RepoItem) {
oldItem.repo.fullName == newItem.repo.fullName
} else {
false
}
}

override fun areContentsTheSame(oldItem: Repo, newItem: Repo): Boolean =
override fun areContentsTheSame(oldItem: UiModel, newItem: UiModel): Boolean =
oldItem == newItem
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,23 @@
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.ViewModelProvider
import androidx.lifecycle.observe
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
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.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

@FlowPreview
@ExperimentalCoroutinesApi
Expand All @@ -41,6 +42,7 @@ class SearchRepositoriesActivity : AppCompatActivity() {
private lateinit var binding: ActivitySearchRepositoriesBinding
private lateinit var viewModel: SearchRepositoriesViewModel
private val adapter = ReposAdapter()
private var searchJob: Job? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -55,11 +57,20 @@ class SearchRepositoriesActivity : AppCompatActivity() {
// add dividers between RecyclerView's row items
val decoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
binding.list.addItemDecoration(decoration)
setupScrollListener()

initAdapter()
binding.list.adapter = adapter
adapter.addLoadStateListener { loadType, loadState ->
Log.d("SearchRepositoriesActivity", "adapter load: type = $loadType state = $loadState")
if (loadState is LoadState.Error) {
Toast.makeText(
this,
"\uD83D\uDE28 Wooops $loadState.message}",
Toast.LENGTH_LONG
).show()
}
}
val query = savedInstanceState?.getString(LAST_SEARCH_QUERY) ?: DEFAULT_QUERY
viewModel.searchRepo(query)
search(query)
initSearch(query)
}

Expand All @@ -68,25 +79,6 @@ class SearchRepositoriesActivity : AppCompatActivity() {
outState.putString(LAST_SEARCH_QUERY, binding.searchRepo.text.trim().toString())
}

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) {
binding.searchRepo.setText(query)

Expand All @@ -112,8 +104,21 @@ class SearchRepositoriesActivity : AppCompatActivity() {
binding.searchRepo.text.trim().let {
if (it.isNotEmpty()) {
binding.list.scrollToPosition(0)
viewModel.searchRepo(it.toString())
adapter.submitList(null)
search(it.toString())
// TODO how to clear the list
// might not need it because of how Paging works
// adapter.collectFrom(PagingData.empty())
}
}
}

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).collect {
Log.d("SearchRepositoriesActivity", "query: $query, collecting $it")
adapter.presentData(it)
}
}
}
Expand All @@ -128,20 +133,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"
Expand Down
Loading