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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package com.example.android.codelabs.paging.api

import android.util.Log
import com.example.android.codelabs.paging.model.RepoSearchResult
import com.example.android.codelabs.paging.data.RepoSearchResult
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.logging.HttpLoggingInterceptor.Level
Expand Down Expand Up @@ -53,7 +53,7 @@ suspend fun searchRepos(
Log.d(TAG, "got a response $response")
if (response.isSuccessful) {
val repos = response.body()?.items ?: emptyList()
return RepoSearchResult.Success(repos)
return RepoSearchResult.Success(repos, response.body()?.total ?: 0)

Choose a reason for hiding this comment

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

What does it look like if you just return the response object directly from the GithubService?

I think removing the RepoSearchResult wrapper may make it easier for folks to see retrofit <-> paging connection all in one place, and copy the code out.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If this is only used in PagingSource then, it should be ok. I just don't want to Retrofit Response to leak in Repository.

} else {
return RepoSearchResult.Error(IOException(response.message() ?: "Unknown error"))
}
Expand Down Expand Up @@ -82,7 +82,7 @@ interface GithubService {

fun create(): GithubService {
val logger = HttpLoggingInterceptor()
logger.level = Level.BASIC
logger.level = Level.BODY

val client = OkHttpClient.Builder()
.addInterceptor(logger)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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.searchRepos
import com.example.android.codelabs.paging.model.Repo
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview

@ExperimentalCoroutinesApi
@FlowPreview
class GithubPagingSource(
private val service: GithubService,
private val query: String
) : PagingSource<Int, Repo>() {

private var _totalReposCount = 0
val totalReposCount: Int
get() = _totalReposCount

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
val position = params.key ?: 0
val apiResponse = searchRepos(service, query, position, GithubRepository.NETWORK_PAGE_SIZE)
return when (apiResponse) {
is RepoSearchResult.Success -> {
_totalReposCount = apiResponse.totalReposCount
LoadResult.Page(
data = apiResponse.data,
prevKey = if (position == 0) null else -1,
// if we don't get any results, we consider that we're at the last page
nextKey = if (apiResponse.data.isEmpty()) null else position + 1
)
}
is RepoSearchResult.Error -> {
_totalReposCount = -1
LoadResult.Error(apiResponse.error)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
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.searchRepos
import com.example.android.codelabs.paging.model.Repo
import com.example.android.codelabs.paging.model.RepoSearchResult
import com.example.android.codelabs.paging.model.SearchResult
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow

/**
* Repository class that works with local and remote data sources.
Expand All @@ -34,66 +34,21 @@ import kotlinx.coroutines.flow.asFlow
@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 = 1

// 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): SearchResult {
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 apiResponse = searchRepos(service, query, lastRequestedPage, NETWORK_PAGE_SIZE)
Log.d("GithubRepository", "response $apiResponse")
// add the new result list to the existing list
when (apiResponse) {
is RepoSearchResult.Success -> {
inMemoryCache.addAll(apiResponse.data)
val reposByName = reposByName(query)
searchResults.offer(RepoSearchResult.Success(reposByName))
}
is RepoSearchResult.Error -> {
searchResults.offer(RepoSearchResult.Error(apiResponse.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 })
val pagingSource = GithubPagingSource(service, query)
return SearchResult(PagingDataFlow(
config = PagingConfig(pageSize = NETWORK_PAGE_SIZE),
pagingSourceFactory = { pagingSource }
), pagingSource.totalReposCount)
}

companion object {
private const val NETWORK_PAGE_SIZE = 50
const val NETWORK_PAGE_SIZE = 50
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@
* limitations under the License.
*/

package com.example.android.codelabs.paging.model
package com.example.android.codelabs.paging.data

import com.example.android.codelabs.paging.model.Repo
import java.lang.Exception

/**
* RepoSearchResult from a search, which contains List<Repo> holding query data,
* and a String of network error state.
*/
sealed class RepoSearchResult {
data class Success(val data: List<Repo>) : RepoSearchResult()
data class Success(val data: List<Repo>, val totalReposCount: Int) : RepoSearchResult()
data class Error(val error: Exception) : RepoSearchResult()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.android.codelabs.paging.model

import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow

data class SearchResult(val pagingDataFlow: Flow<PagingData<Repo>>, val totalResultCount: Int)
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.example.android.codelabs.paging.ui

/*
* 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.
*/

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.android.codelabs.paging.R

class HeaderViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val description: TextView = view.findViewById(R.id.separator_description)

fun bind(separatorText: String) {
description.text = separatorText
}

companion object {
fun create(parent: ViewGroup): HeaderViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.header_view_item, parent, false)
return HeaderViewHolder(view)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,63 @@
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
import java.lang.UnsupportedOperationException

/**
* 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 when (viewType) {
R.layout.repo_view_item -> {
RepoViewHolder.create(parent)
}
R.layout.separator_view_item -> {
SeparatorViewHolder.create(parent)
}
R.layout.header_view_item -> {
HeaderViewHolder.create(parent)
}
else -> throw UnsupportedOperationException("Can't handle view type $viewType")
}
}

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
is UiModel.HeaderItem -> R.layout.header_view_item
null -> throw UnsupportedOperationException("Can't handle null items")
}
}

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)
is UiModel.HeaderItem -> (holder as HeaderViewHolder).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 {
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
Loading