Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3fa9cba
Having the list backed by network only
florina-muntenescu Jan 29, 2020
ffe6665
Showing an infinite scrolling list, from network, with Flow
florina-muntenescu Jan 29, 2020
763640a
Adding ViewBinding
florina-muntenescu Feb 24, 2020
22edaed
Making the project compatible with Android Studio 3.6
florina-muntenescu Mar 10, 2020
d638338
Steps 5-9 Migrating to Paging 3.0
florina-muntenescu May 4, 2020
dea9811
Having the list backed by network only
florina-muntenescu Jan 29, 2020
521803f
Showing an infinite scrolling list, from network, with Flow
florina-muntenescu Jan 29, 2020
6d6614d
Adding ViewBinding
florina-muntenescu Feb 24, 2020
6667e26
Making the project compatible with Android Studio 3.6
florina-muntenescu Mar 10, 2020
41cc455
Steps 5-9 Migrating to Paging 3.0
florina-muntenescu May 4, 2020
74c497b
Adding a loading state footer
florina-muntenescu May 4, 2020
f615b7f
Having the list backed by network only
florina-muntenescu Jan 29, 2020
946cebb
Showing an infinite scrolling list, from network, with Flow
florina-muntenescu Jan 29, 2020
588bd2b
Adding ViewBinding
florina-muntenescu Feb 24, 2020
69827db
Making the project compatible with Android Studio 3.6
florina-muntenescu Mar 10, 2020
6008773
Steps 5-9 Migrating to Paging 3.0
florina-muntenescu May 4, 2020
22c5b84
Steps 5-9 Migrating to Paging 3.0
florina-muntenescu May 4, 2020
216cb26
Display the loading state in SearchRepositoriesActivity
florina-muntenescu May 4, 2020
cdc45bb
Apply code cleanups from step13
dlam Jun 30, 2020
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
3 changes: 2 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ dependencies {
implementation "com.google.android.material:material:$materialVersion"

// architecture components
implementation "androidx.core:core-ktx:$coreVersion"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.room:room-runtime:$roomVersion"
Expand Down
Original file line number Diff line number Diff line change
@@ -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<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
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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<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
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<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 Pager(
config = PagingConfig(pageSize = NETWORK_PAGE_SIZE),
pagingSourceFactory = { GithubPagingSource(service, query) }
).flow
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, androidx.recyclerview.widget.RecyclerView.ViewHolder>(REPO_COMPARATOR) {
class ReposAdapter : PagingDataAdapter<Repo, ViewHolder>(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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ReposLoadStateViewHolder>() {
override fun onBindViewHolder(holder: ReposLoadStateViewHolder, loadState: LoadState) {
holder.bind(loadState)
}

override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ReposLoadStateViewHolder {
return ReposLoadStateViewHolder.create(parent, retry)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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.ViewGroup
import androidx.core.view.isVisible
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.setOnClickListener { retry.invoke() }
}

fun bind(loadState: LoadState) {
if (loadState is LoadState.Error) {
binding.errorMsg.text = loadState.error.localizedMessage
}
binding.progressBar.isVisible = loadState is LoadState.Loading
binding.retryButton.isVisible = loadState !is LoadState.Loading
binding.errorMsg.isVisible = 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)
}
}
}
Loading