Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import com.google.gson.annotations.SerializedName
* Data class to hold repo responses from searchRepo API calls.
*/
data class RepoSearchResponse(
@SerializedName("total_count") val total: Int = 0,
@SerializedName("items") val items: List<Repo> = emptyList(),
val nextPage: Int? = null
)
@SerializedName("total_count") val total: Int = 0,
@SerializedName("items") val items: List<Repo> = emptyList(),
val nextPage: Int? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,49 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.example.android.codelabs.paging.R
import com.example.android.codelabs.paging.model.Repo
import java.lang.UnsupportedOperationException

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

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return RepoViewHolder.create(parent)
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 -> throw UnsupportedOperationException("Unknown view")
}
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val repo = getItem(position)
(holder as RepoViewHolder).bind(repo)
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>() {

Choose a reason for hiding this comment

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

The code as written will cause all separators to shimmer, it should be checking for separators too

Should be fine to say all separators are the same, since DiffUtil will be doing greedy matching

I don't think any of this matters in behavior here, since without swipe to refresh, you're not actually going to see any diffing 🤷‍♂️ but still good to recommend correct code in case they do

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

added separator as well

override fun areItemsTheSame(oldItem: UiModel, newItem: UiModel): Boolean {
return (oldItem is UiModel.RepoItem && newItem is UiModel.RepoItem &&
oldItem.repo.fullName == newItem.repo.fullName) ||
(oldItem is UiModel.SeparatorItem && newItem is UiModel.SeparatorItem &&
oldItem.description == newItem.description)
}

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 @@ -62,3 +62,4 @@ class ReposLoadStateViewHolder(
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class SearchRepositoriesActivity : AppCompatActivity() {
adapter.addLoadStateListener { loadType, loadState ->
Log.d("SearchRepositoriesActivity", "adapter load: type = $loadType state = $loadState")
if (loadType == LoadType.REFRESH) {
binding.list.visibility = View.GONE
binding.list.visibility = toVisibility(loadState == LoadState.Idle)
binding.progressBar.visibility = toVisibility(loadState == LoadState.Loading)
binding.retryButton.visibility = toVisibility(loadState is LoadState.Error)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@

package com.example.android.codelabs.paging.ui

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.insertSeparators
import com.example.android.codelabs.paging.data.GithubRepository
import com.example.android.codelabs.paging.model.Repo
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

/**
* ViewModel for the [SearchRepositoriesActivity] screen.
Expand All @@ -36,20 +39,46 @@ class SearchRepositoriesViewModel(private val repository: GithubRepository) : Vi
private var currentQueryValue: String? = null

@Volatile
private var currentSearchResult: Flow<PagingData<Repo>>? = null
private var currentSearchResult: Flow<PagingData<UiModel>>? = null

/**
* Search a repository based on a query string.
*/
fun searchRepo(queryString: String): Flow<PagingData<Repo>> {
fun searchRepo(queryString: String): Flow<PagingData<UiModel>> {
val lastResult = currentSearchResult
if (queryString == currentQueryValue && lastResult != null) {
return lastResult
}
currentQueryValue = queryString
val newResult: Flow<PagingData<Repo>> = repository.getSearchResultStream(queryString)
val newResult: Flow<PagingData<UiModel>> = repository.getSearchResultStream(queryString)
.map { pagingData -> pagingData.map { UiModel.RepoItem(it) } }
.map {
it.insertSeparators<UiModel.RepoItem, UiModel> { before, after ->
if (before == null && after != null) {
UiModel.SeparatorItem("${after.roundedStarCount}0.000+ stars")
} else if (before != null && after != null
&& before.roundedStarCount > after.roundedStarCount) {
if (after.roundedStarCount >= 1) {
UiModel.SeparatorItem("${after.roundedStarCount}0.000+ stars")
} else {
UiModel.SeparatorItem("< 10.000+ stars")
}
} else {
// no separator
null
}
}
}
.cachedIn(viewModelScope)
currentSearchResult = newResult
return newResult
}
}

sealed class UiModel {
data class RepoItem(val repo: Repo) : UiModel()
data class SeparatorItem(val description: String) : UiModel()
}

private val UiModel.RepoItem.roundedStarCount: Int
get() = this.repo.stars / 10_000
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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.ui

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 SeparatorViewHolder(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): SeparatorViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.separator_view_item, parent, false)
return SeparatorViewHolder(view)
}
}
}
34 changes: 34 additions & 0 deletions app/src/main/res/layout/separator_view_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/separatorBackground">

<TextView
android:id="@+id/separator_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="@dimen/row_item_margin_horizontal"
android:textColor="@color/separatorText"
android:textSize="@dimen/repo_name_size"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="10000+ stars" />
</androidx.constraintlayout.widget.ConstraintLayout>