From a4e9eace7c5737d4125d1b9db0abcfa79792fb91 Mon Sep 17 00:00:00 2001 From: Tiem Song Date: Wed, 15 Jul 2020 10:52:26 -0700 Subject: [PATCH] Add Paging 3.0 for gallery screen (#629) * 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 <2998890+florina-muntenescu@users.noreply.github.com> * Update recyclerview version Co-authored-by: Florina Muntenescu <2998890+florina-muntenescu@users.noreply.github.com> * Address PR comments - catch Exception instead of IO/HttpException - disable Paging placeholders - remove unneeded local variable in searchPictures Co-authored-by: Florina Muntenescu <2998890+florina-muntenescu@users.noreply.github.com> --- app/build.gradle | 1 + .../samples/apps/sunflower/GalleryFragment.kt | 36 ++++++---------- .../apps/sunflower/PlantDetailFragment.kt | 2 +- .../apps/sunflower/adapters/GalleryAdapter.kt | 8 ++-- .../apps/sunflower/api/UnsplashService.kt | 6 +-- .../sunflower/data/UnsplashPagingSource.kt | 43 +++++++++++++++++++ .../apps/sunflower/data/UnsplashRepository.kt | 28 +++++------- .../sunflower/data/UnsplashSearchResponse.kt | 3 +- .../sunflower/data/UnsplashSearchResult.kt | 22 ---------- .../sunflower/viewmodels/GalleryViewModel.kt | 33 ++++++-------- build.gradle | 3 +- 11 files changed, 93 insertions(+), 92 deletions(-) create mode 100644 app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashPagingSource.kt delete mode 100644 app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashSearchResult.kt diff --git a/app/build.gradle b/app/build.gradle index 25aa23645..23f9d023e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/java/com/google/samples/apps/sunflower/GalleryFragment.kt b/app/src/main/java/com/google/samples/apps/sunflower/GalleryFragment.kt index 074032075..d03202e01 100644 --- a/app/src/main/java/com/google/samples/apps/sunflower/GalleryFragment.kt +++ b/app/src/main/java/com/google/samples/apps/sunflower/GalleryFragment.kt @@ -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() } @@ -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() @@ -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 - } } diff --git a/app/src/main/java/com/google/samples/apps/sunflower/PlantDetailFragment.kt b/app/src/main/java/com/google/samples/apps/sunflower/PlantDetailFragment.kt index 072d8bdf5..be29fd0b5 100644 --- a/app/src/main/java/com/google/samples/apps/sunflower/PlantDetailFragment.kt +++ b/app/src/main/java/com/google/samples/apps/sunflower/PlantDetailFragment.kt @@ -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() diff --git a/app/src/main/java/com/google/samples/apps/sunflower/adapters/GalleryAdapter.kt b/app/src/main/java/com/google/samples/apps/sunflower/adapters/GalleryAdapter.kt index aa5a6623a..1453b5adb 100644 --- a/app/src/main/java/com/google/samples/apps/sunflower/adapters/GalleryAdapter.kt +++ b/app/src/main/java/com/google/samples/apps/sunflower/adapters/GalleryAdapter.kt @@ -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 @@ -32,7 +32,7 @@ import com.google.samples.apps.sunflower.databinding.ListItemPhotoBinding * Adapter for the [RecyclerView] in [GalleryFragment]. */ -class GalleryAdapter : ListAdapter(GalleryDiffCallback()) { +class GalleryAdapter : PagingDataAdapter(GalleryDiffCallback()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GalleryViewHolder { return GalleryViewHolder(ListItemPhotoBinding.inflate( @@ -41,7 +41,9 @@ class GalleryAdapter : ListAdapter(GalleryDiff override fun onBindViewHolder(holder: GalleryViewHolder, position: Int) { val photo = getItem(position) - holder.bind(photo) + if (photo != null) { + holder.bind(photo) + } } class GalleryViewHolder( diff --git a/app/src/main/java/com/google/samples/apps/sunflower/api/UnsplashService.kt b/app/src/main/java/com/google/samples/apps/sunflower/api/UnsplashService.kt index 0a31ca5ee..02e18da3c 100644 --- a/app/src/main/java/com/google/samples/apps/sunflower/api/UnsplashService.kt +++ b/app/src/main/java/com/google/samples/apps/sunflower/api/UnsplashService.kt @@ -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 { diff --git a/app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashPagingSource.kt b/app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashPagingSource.kt new file mode 100644 index 000000000..7c9555d0c --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashPagingSource.kt @@ -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() { + + override suspend fun load(params: LoadParams): LoadResult { + 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) + } + } +} diff --git a/app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashRepository.kt b/app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashRepository.kt index 7426625c1..3145b93de 100644 --- a/app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashRepository.kt +++ b/app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashRepository.kt @@ -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() - - suspend fun getSearchResultStream(query: String): Flow { - requestData(query) - return searchResult.asFlow() + fun getSearchResultStream(query: String): Flow> { + 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 } } diff --git a/app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashSearchResponse.kt b/app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashSearchResponse.kt index 3af2324f5..07adc6fc8 100644 --- a/app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashSearchResponse.kt +++ b/app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashSearchResponse.kt @@ -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 + @field:SerializedName("results") val results: List, + @field:SerializedName("total_pages") val totalPages: Int ) diff --git a/app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashSearchResult.kt b/app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashSearchResult.kt deleted file mode 100644 index 6129705e8..000000000 --- a/app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashSearchResult.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 - -sealed class UnsplashSearchResult { - data class Success(val data: UnsplashSearchResponse) : UnsplashSearchResult() - data class Error(val error: Exception) : UnsplashSearchResult() -} diff --git a/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/GalleryViewModel.kt b/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/GalleryViewModel.kt index 6037496f9..c37116ce0 100644 --- a/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/GalleryViewModel.kt +++ b/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/GalleryViewModel.kt @@ -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>? = null - private val queryLiveData = MutableLiveData() - - val repoResult: LiveData = queryLiveData.switchMap { queryString -> - liveData { - val repos = repository.getSearchResultStream(queryString).asLiveData() - emitSource(repos) - } - } - - fun searchPictures(queryString: String) { - queryLiveData.postValue(queryString) + fun searchPictures(queryString: String): Flow> { + currentQueryValue = queryString + val newResult: Flow> = + repository.getSearchResultStream(queryString).cachedIn(viewModelScope) + currentSearchResult = newResult + return newResult } } diff --git a/build.gradle b/build.gradle index 325b90970..0205a04f7 100644 --- a/build.gradle +++ b/build.gradle @@ -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'