Implement Glide image preload in RecyclerView
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
package gq.kirmanak.mealient.ui.recipes
|
||||
|
||||
import android.widget.ImageView
|
||||
|
||||
interface RecipeImageLoader {
|
||||
|
||||
fun loadRecipeImage(view: ImageView, slug: String?)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
|
||||
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
||||
import timber.log.Timber
|
||||
|
||||
class RecipeViewHolder(
|
||||
@@ -18,7 +19,7 @@ class RecipeViewHolder(
|
||||
fun bind(item: RecipeSummaryEntity?) {
|
||||
Timber.v("bind() called with: item = $item")
|
||||
binding.name.text = item?.name ?: loadingPlaceholder
|
||||
recipeImageLoader.loadRecipeImage(binding.image, item?.slug)
|
||||
recipeImageLoader.loadRecipeImage(binding.image, item)
|
||||
item?.let { entity ->
|
||||
binding.root.setOnClickListener {
|
||||
Timber.d("bind: item clicked $entity")
|
||||
|
||||
@@ -14,6 +14,8 @@ import gq.kirmanak.mealient.databinding.FragmentRecipesBinding
|
||||
import gq.kirmanak.mealient.extensions.collectWhenViewResumed
|
||||
import gq.kirmanak.mealient.extensions.refreshRequestFlow
|
||||
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
|
||||
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
||||
import gq.kirmanak.mealient.ui.recipes.images.RecipePreloaderFactory
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -26,6 +28,9 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
|
||||
@Inject
|
||||
lateinit var recipeImageLoader: RecipeImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var recipePreloaderFactory: RecipePreloaderFactory
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
||||
@@ -45,19 +50,22 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
|
||||
|
||||
private fun setupRecipeAdapter() {
|
||||
Timber.v("setupRecipeAdapter() called")
|
||||
val adapter = RecipesPagingAdapter(recipeImageLoader, ::navigateToRecipeInfo)
|
||||
binding.recipes.adapter = adapter
|
||||
val recipesAdapter = RecipesPagingAdapter(recipeImageLoader, ::navigateToRecipeInfo)
|
||||
with(binding.recipes) {
|
||||
adapter = recipesAdapter
|
||||
addOnScrollListener(recipePreloaderFactory.create(recipesAdapter))
|
||||
}
|
||||
collectWhenViewResumed(viewModel.pagingData) {
|
||||
Timber.v("setupRecipeAdapter: received data update")
|
||||
adapter.submitData(lifecycle, it)
|
||||
recipesAdapter.submitData(lifecycle, it)
|
||||
}
|
||||
collectWhenViewResumed(adapter.onPagesUpdatedFlow) {
|
||||
collectWhenViewResumed(recipesAdapter.onPagesUpdatedFlow) {
|
||||
Timber.v("setupRecipeAdapter: pages updated")
|
||||
binding.refresher.isRefreshing = false
|
||||
}
|
||||
collectWhenViewResumed(binding.refresher.refreshRequestFlow()) {
|
||||
Timber.v("setupRecipeAdapter: received refresh request")
|
||||
adapter.refresh()
|
||||
recipesAdapter.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
|
||||
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
||||
import timber.log.Timber
|
||||
|
||||
class RecipesPagingAdapter(
|
||||
private val recipeImageLoader: RecipeImageLoader,
|
||||
private val clickListener: (RecipeSummaryEntity) -> Unit
|
||||
) : PagingDataAdapter<RecipeSummaryEntity, RecipeViewHolder>(RecipeDiffCallback) {
|
||||
|
||||
override fun onBindViewHolder(holder: RecipeViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
holder.bind(item)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.images
|
||||
|
||||
import android.widget.ImageView
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
|
||||
interface RecipeImageLoader {
|
||||
|
||||
fun loadRecipeImage(view: ImageView, recipe: RecipeSummaryEntity?)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.images
|
||||
|
||||
import android.widget.ImageView
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import dagger.hilt.android.scopes.FragmentScoped
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@FragmentScoped
|
||||
class RecipeImageLoaderImpl @Inject constructor(
|
||||
private val fragment: Fragment,
|
||||
private val requestOptions: RequestOptions,
|
||||
) : RecipeImageLoader {
|
||||
|
||||
override fun loadRecipeImage(view: ImageView, recipe: RecipeSummaryEntity?) {
|
||||
Timber.v("loadRecipeImage() called with: view = $view, recipe = $recipe")
|
||||
Glide.with(fragment).load(recipe).apply(requestOptions).into(view)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.images
|
||||
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.model.ModelCache
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
import com.bumptech.glide.load.model.stream.BaseGlideUrlLoader
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
|
||||
class RecipeModelLoader(
|
||||
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
||||
concreteLoader: ModelLoader<GlideUrl, InputStream>,
|
||||
cache: ModelCache<RecipeSummaryEntity, GlideUrl>,
|
||||
) : BaseGlideUrlLoader<RecipeSummaryEntity>(concreteLoader, cache) {
|
||||
|
||||
override fun handles(model: RecipeSummaryEntity): Boolean = true
|
||||
|
||||
override fun getUrl(
|
||||
model: RecipeSummaryEntity?,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options?
|
||||
): String? {
|
||||
Timber.v("getUrl() called with: model = $model, width = $width, height = $height, options = $options")
|
||||
return runBlocking { recipeImageUrlProvider.generateImageUrl(model?.slug) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.images
|
||||
|
||||
import com.bumptech.glide.load.model.*
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class RecipeModelLoaderFactory @Inject constructor(
|
||||
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
||||
) : ModelLoaderFactory<RecipeSummaryEntity, InputStream> {
|
||||
|
||||
private val cache = ModelCache<RecipeSummaryEntity, GlideUrl>()
|
||||
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<RecipeSummaryEntity, InputStream> {
|
||||
Timber.v("build() called with: multiFactory = $multiFactory")
|
||||
val concreteLoader = multiFactory.build(GlideUrl::class.java, InputStream::class.java)
|
||||
return RecipeModelLoader(recipeImageUrlProvider, concreteLoader, cache)
|
||||
}
|
||||
|
||||
override fun teardown() {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.images
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.ListPreloader
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import dagger.hilt.android.scopes.FragmentScoped
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class RecipePreloadModelProvider(
|
||||
private val adapter: PagingDataAdapter<RecipeSummaryEntity, *>,
|
||||
private val fragment: Fragment,
|
||||
private val requestOptions: RequestOptions,
|
||||
) : ListPreloader.PreloadModelProvider<RecipeSummaryEntity> {
|
||||
|
||||
override fun getPreloadItems(position: Int): List<RecipeSummaryEntity> {
|
||||
Timber.v("getPreloadItems() called with: position = $position")
|
||||
return adapter.peek(position)?.let { listOf(it) } ?: emptyList()
|
||||
}
|
||||
|
||||
override fun getPreloadRequestBuilder(item: RecipeSummaryEntity): RequestBuilder<*> {
|
||||
Timber.v("getPreloadRequestBuilder() called with: item = $item")
|
||||
return Glide.with(fragment).load(item).apply(requestOptions)
|
||||
}
|
||||
|
||||
@FragmentScoped
|
||||
class Factory @Inject constructor(
|
||||
private val fragment: Fragment,
|
||||
private val requestOptions: RequestOptions,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
adapter: PagingDataAdapter<RecipeSummaryEntity, *>,
|
||||
) = RecipePreloadModelProvider(adapter, fragment, requestOptions)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.images
|
||||
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
|
||||
interface RecipePreloaderFactory {
|
||||
|
||||
fun create(adapter: PagingDataAdapter<RecipeSummaryEntity, *>): RecyclerViewPreloader<RecipeSummaryEntity>
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.images
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider
|
||||
import dagger.hilt.android.scopes.FragmentScoped
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
@FragmentScoped
|
||||
class RecipePreloaderFactoryImpl @Inject constructor(
|
||||
private val recipePreloadModelProvider: RecipePreloadModelProvider.Factory,
|
||||
private val fragment: Fragment,
|
||||
) : RecipePreloaderFactory {
|
||||
|
||||
override fun create(
|
||||
adapter: PagingDataAdapter<RecipeSummaryEntity, *>,
|
||||
): RecyclerViewPreloader<RecipeSummaryEntity> {
|
||||
val preloadSizeProvider = ViewPreloadSizeProvider<RecipeSummaryEntity>()
|
||||
val preloadModelProvider = recipePreloadModelProvider.create(adapter)
|
||||
return RecyclerViewPreloader(
|
||||
fragment,
|
||||
preloadModelProvider,
|
||||
preloadSizeProvider,
|
||||
MAX_PRELOAD
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_PRELOAD = 10
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.databinding.FragmentRecipeInfoBinding
|
||||
import gq.kirmanak.mealient.ui.recipes.RecipeImageLoader
|
||||
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -46,7 +46,6 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
|
||||
with(binding) {
|
||||
ingredientsList.adapter = ingredientsAdapter
|
||||
instructionsList.adapter = instructionsAdapter
|
||||
recipeImageLoader.loadRecipeImage(image, arguments.recipeSlug)
|
||||
}
|
||||
|
||||
with(viewModel) {
|
||||
@@ -60,6 +59,7 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
|
||||
ingredientsHolder.isVisible = uiState.areIngredientsVisible
|
||||
instructionsGroup.isVisible = uiState.areInstructionsVisible
|
||||
uiState.recipeInfo?.let {
|
||||
recipeImageLoader.loadRecipeImage(image, it.recipeSummaryEntity)
|
||||
title.text = it.recipeSummaryEntity.name
|
||||
description.text = it.recipeSummaryEntity.description
|
||||
ingredientsAdapter.submitList(it.recipeIngredients)
|
||||
|
||||
Reference in New Issue
Block a user