Skip to content

Commit

Permalink
Add Paging 3.0 for gallery screen (android#629)
Browse files Browse the repository at this point in the history
* Add Paging dependency

Also updates RecyclerView version and update ShareCompat API call

* Update UnsplashService param order

Move client_id to the last param

* Implement Paging 3.0

Also remove default values for UnsplashService#searchPhotos

* Remove unused method

* Update paging version

Co-authored-by: Florina Muntenescu <[email protected]>

* Update recyclerview version

Co-authored-by: Florina Muntenescu <[email protected]>

* Address PR comments

- catch Exception instead of IO/HttpException
- disable Paging placeholders
- remove unneeded local variable in searchPictures

Co-authored-by: Florina Muntenescu <[email protected]>
  • Loading branch information
tiembo and florina-muntenescu authored Jul 15, 2020
1 parent 4ea76fb commit a4e9eac
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 92 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.lifecycleVersion"
implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"
implementation "androidx.paging:paging-runtime:$rootProject.pagingVersion"
implementation "androidx.recyclerview:recyclerview:$rootProject.recyclerViewVersion"
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
implementation "androidx.room:room-ktx:$rootProject.roomVersion"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,22 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.observe
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.snackbar.Snackbar
import com.google.samples.apps.sunflower.adapters.GalleryAdapter
import com.google.samples.apps.sunflower.data.UnsplashSearchResult
import com.google.samples.apps.sunflower.databinding.FragmentGalleryBinding
import com.google.samples.apps.sunflower.utilities.InjectorUtils
import com.google.samples.apps.sunflower.viewmodels.GalleryViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch

class GalleryFragment : Fragment() {

private val adapter = GalleryAdapter()
private val args: GalleryFragmentArgs by navArgs()
private var searchJob: Job? = null
private val viewModel: GalleryViewModel by viewModels {
InjectorUtils.provideGalleryViewModelFactory()
}
Expand All @@ -48,9 +50,8 @@ class GalleryFragment : Fragment() {
val binding = FragmentGalleryBinding.inflate(inflater, container, false)
context ?: return binding.root

viewModel.searchPictures(args.plantName)
binding.photoList.adapter = adapter
subscribeUi(adapter, binding.root)
search(args.plantName)

binding.toolbar.setNavigationOnClickListener { view ->
view.findNavController().navigateUp()
Expand All @@ -59,26 +60,13 @@ class GalleryFragment : Fragment() {
return binding.root
}

private fun subscribeUi(adapter: GalleryAdapter, rootView: View) {
viewModel.repoResult.observe(viewLifecycleOwner) { result ->
when (result) {
is UnsplashSearchResult.Success -> {
showPlaceholder(result.data.results.isEmpty())
adapter.submitList(result.data.results)
}
is UnsplashSearchResult.Error -> {
Snackbar.make(
rootView,
"Error: ${result.error}",
Snackbar.LENGTH_LONG
).show()
}
private fun search(query: String) {
// Make sure we cancel the previous job before creating a new one
searchJob?.cancel()
searchJob = lifecycleScope.launch {
viewModel.searchPictures(query).collectLatest {
adapter.submitData(it)
}
}
}

// Show placeholder if the list of pictures is empty
private fun showPlaceholder(isListEmpty: Boolean) {
// TODO: implement this
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ class PlantDetailFragment : Fragment() {
getString(R.string.share_text_plant, plant.name)
}
}
val shareIntent = ShareCompat.IntentBuilder.from(activity)
val shareIntent = ShareCompat.IntentBuilder.from(requireActivity())
.setText(shareText)
.setType("text/plain")
.createChooserIntent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import android.content.Intent
import android.net.Uri
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.samples.apps.sunflower.GalleryFragment
import com.google.samples.apps.sunflower.adapters.GalleryAdapter.GalleryViewHolder
Expand All @@ -32,7 +32,7 @@ import com.google.samples.apps.sunflower.databinding.ListItemPhotoBinding
* Adapter for the [RecyclerView] in [GalleryFragment].
*/

class GalleryAdapter : ListAdapter<UnsplashPhoto, GalleryViewHolder>(GalleryDiffCallback()) {
class GalleryAdapter : PagingDataAdapter<UnsplashPhoto, GalleryViewHolder>(GalleryDiffCallback()) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GalleryViewHolder {
return GalleryViewHolder(ListItemPhotoBinding.inflate(
Expand All @@ -41,7 +41,9 @@ class GalleryAdapter : ListAdapter<UnsplashPhoto, GalleryViewHolder>(GalleryDiff

override fun onBindViewHolder(holder: GalleryViewHolder, position: Int) {
val photo = getItem(position)
holder.bind(photo)
if (photo != null) {
holder.bind(photo)
}
}

class GalleryViewHolder(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ interface UnsplashService {
@GET("search/photos")
suspend fun searchPhotos(
@Query("query") query: String,
@Query("client_id") clientId: String = BuildConfig.UNSPLASH_ACCESS_KEY,
@Query("page") page: Int = 1,
@Query("per_page") perPage: Int = 20
@Query("page") page: Int,
@Query("per_page") perPage: Int,
@Query("client_id") clientId: String = BuildConfig.UNSPLASH_ACCESS_KEY
) : UnsplashSearchResponse

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2020 Google LLC
*
* 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
*
* https://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.google.samples.apps.sunflower.data

import androidx.paging.PagingSource
import com.google.samples.apps.sunflower.api.UnsplashService

private const val UNSPLASH_STARTING_PAGE_INDEX = 1

class UnsplashPagingSource (
private val service: UnsplashService,
private val query: String
) : PagingSource<Int, UnsplashPhoto>() {

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UnsplashPhoto> {
val page = params.key ?: UNSPLASH_STARTING_PAGE_INDEX
return try {
val response = service.searchPhotos(query, page, params.loadSize)
val photos = response.results
LoadResult.Page(
data = photos,
prevKey = if (page == UNSPLASH_STARTING_PAGE_INDEX) null else page - 1,
nextKey = if (page == response.totalPages) null else page + 1
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,22 @@

package com.google.samples.apps.sunflower.data

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.google.samples.apps.sunflower.api.UnsplashService
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import retrofit2.HttpException
import java.io.IOException

class UnsplashRepository (private val service: UnsplashService) {

private val searchResult = ConflatedBroadcastChannel<UnsplashSearchResult>()

suspend fun getSearchResultStream(query: String): Flow<UnsplashSearchResult> {
requestData(query)
return searchResult.asFlow()
fun getSearchResultStream(query: String): Flow<PagingData<UnsplashPhoto>> {
return Pager(
config = PagingConfig(enablePlaceholders = false, pageSize = NETWORK_PAGE_SIZE),
pagingSourceFactory = { UnsplashPagingSource(service, query) }
).flow
}

private suspend fun requestData(query: String) {
try {
val response = service.searchPhotos(query)
searchResult.offer(UnsplashSearchResult.Success(response))
} catch (exception: IOException) {
searchResult.offer(UnsplashSearchResult.Error(exception))
} catch (exception: HttpException) {
searchResult.offer(UnsplashSearchResult.Error(exception))
}
companion object {
private const val NETWORK_PAGE_SIZE = 25
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ import com.google.gson.annotations.SerializedName
* [here](https://unsplash.com/documentation#search-photos).
*/
data class UnsplashSearchResponse(
@field:SerializedName("results") val results: List<UnsplashPhoto>
@field:SerializedName("results") val results: List<UnsplashPhoto>,
@field:SerializedName("total_pages") val totalPages: Int
)

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,25 @@

package com.google.samples.apps.sunflower.viewmodels

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.google.samples.apps.sunflower.data.UnsplashPhoto
import com.google.samples.apps.sunflower.data.UnsplashRepository
import com.google.samples.apps.sunflower.data.UnsplashSearchResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow

class GalleryViewModel internal constructor(
repository: UnsplashRepository
private val repository: UnsplashRepository
) : ViewModel() {
private var currentQueryValue: String? = null
private var currentSearchResult: Flow<PagingData<UnsplashPhoto>>? = null

private val queryLiveData = MutableLiveData<String>()

val repoResult: LiveData<UnsplashSearchResult> = queryLiveData.switchMap { queryString ->
liveData {
val repos = repository.getSearchResultStream(queryString).asLiveData()
emitSource(repos)
}
}

fun searchPictures(queryString: String) {
queryLiveData.postValue(queryString)
fun searchPictures(queryString: String): Flow<PagingData<UnsplashPhoto>> {
currentQueryValue = queryString
val newResult: Flow<PagingData<UnsplashPhoto>> =
repository.getSearchResultStream(queryString).cachedIn(viewModelScope)
currentSearchResult = newResult
return newResult
}
}
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ buildscript {
materialVersion = '1.1.0-alpha09'
navigationVersion = '2.2.0'
okhttpLoggingVersion = '4.7.2'
recyclerViewVersion = '1.1.0-alpha05'
pagingVersion = '3.0.0-alpha02'
recyclerViewVersion = '1.2.0-alpha04'
retrofitVersion = '2.9.0'
roomVersion = '2.1.0'
runnerVersion = '1.0.1'
Expand Down

0 comments on commit a4e9eac

Please sign in to comment.