Implement Glide image preload in RecyclerView

This commit is contained in:
Kirill Kamakin
2022-04-18 18:08:06 +02:00
parent 1ffd2d2359
commit 3079cd9588
23 changed files with 289 additions and 123 deletions

View File

@@ -152,6 +152,10 @@ dependencies {
def glide_version = "4.13.1" def glide_version = "4.13.1"
implementation "com.github.bumptech.glide:glide:$glide_version" implementation "com.github.bumptech.glide:glide:$glide_version"
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version" implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
implementation("com.github.bumptech.glide:recyclerview-integration:$glide_version") {
// Excludes the support library because it's already included by Glide.
transitive = false
}
kapt "com.github.bumptech.glide:compiler:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version"
// https://github.com/androidbroadcast/ViewBindingPropertyDelegate/releases // https://github.com/androidbroadcast/ViewBindingPropertyDelegate/releases

View File

@@ -1,44 +0,0 @@
package gq.kirmanak.mealient.data.recipes.impl
import android.widget.ImageView
import androidx.annotation.VisibleForTesting
import androidx.fragment.app.Fragment
import dagger.hilt.android.scopes.FragmentScoped
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
import gq.kirmanak.mealient.extensions.launchWhenViewResumed
import gq.kirmanak.mealient.ui.images.ImageLoader
import gq.kirmanak.mealient.ui.recipes.RecipeImageLoader
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import timber.log.Timber
import javax.inject.Inject
@FragmentScoped
class RecipeImageLoaderImpl @Inject constructor(
private val imageLoader: ImageLoader,
private val baseURLStorage: BaseURLStorage,
private val fragment: Fragment,
): RecipeImageLoader {
override fun loadRecipeImage(view: ImageView, slug: String?) {
Timber.v("loadRecipeImage() called with: view = $view, slug = $slug")
fragment.launchWhenViewResumed {
imageLoader.loadImage(generateImageUrl(slug), R.drawable.placeholder_recipe, view)
}
}
@VisibleForTesting
suspend fun generateImageUrl(slug: String?): String? {
Timber.v("generateImageUrl() called with: slug = $slug")
val result = baseURLStorage.getBaseURL()
?.takeIf { it.isNotBlank() }
?.takeUnless { slug.isNullOrBlank() }
?.toHttpUrlOrNull()
?.newBuilder()
?.addPathSegments("api/media/recipes/$slug/images/original.webp")
?.build()
?.toString()
Timber.v("generateImageUrl() returned: $result")
return result
}
}

View File

@@ -0,0 +1,6 @@
package gq.kirmanak.mealient.data.recipes.impl
interface RecipeImageUrlProvider {
suspend fun generateImageUrl(slug: String?): String?
}

View File

@@ -0,0 +1,31 @@
package gq.kirmanak.mealient.data.recipes.impl
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecipeImageUrlProviderImpl @Inject constructor(
private val baseURLStorage: BaseURLStorage,
) : RecipeImageUrlProvider {
override suspend fun generateImageUrl(slug: String?): String? {
Timber.v("generateImageUrl() called with: slug = $slug")
slug?.takeUnless { it.isBlank() } ?: return null
val imagePath = IMAGE_PATH_FORMAT.format(slug)
val baseUrl = baseURLStorage.getBaseURL()?.takeUnless { it.isEmpty() }
val result = baseUrl?.toHttpUrlOrNull()
?.newBuilder()
?.addPathSegments(imagePath)
?.build()
?.toString()
Timber.v("getRecipeImageUrl() returned: $result")
return result
}
companion object {
private const val IMAGE_PATH_FORMAT = "api/media/recipes/%s/images/original.webp"
}
}

View File

@@ -1,18 +1,20 @@
package gq.kirmanak.mealient.ui.images package gq.kirmanak.mealient.di
import com.bumptech.glide.load.model.ModelLoaderFactory
import dagger.hilt.EntryPoint import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import gq.kirmanak.mealient.di.AUTH_OK_HTTP import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.io.InputStream
import javax.inject.Named import javax.inject.Named
import javax.inject.Singleton
@EntryPoint @EntryPoint
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface GlideModuleEntryPoint { interface GlideModuleEntryPoint {
@Singleton
@Named(AUTH_OK_HTTP) @Named(AUTH_OK_HTTP)
fun provideOkHttp(): OkHttpClient fun provideOkHttp(): OkHttpClient
fun provideRecipeLoaderFactory(): ModelLoaderFactory<RecipeSummaryEntity, InputStream>
} }

View File

@@ -1,11 +1,14 @@
package gq.kirmanak.mealient.di package gq.kirmanak.mealient.di
import androidx.paging.InvalidatingPagingSourceFactory import androidx.paging.InvalidatingPagingSourceFactory
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.request.RequestOptions
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
import gq.kirmanak.mealient.data.network.RetrofitBuilder import gq.kirmanak.mealient.data.network.RetrofitBuilder
import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.data.network.ServiceFactory
@@ -13,12 +16,17 @@ import gq.kirmanak.mealient.data.network.createServiceFactory
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.db.RecipeStorageImpl import gq.kirmanak.mealient.data.recipes.db.RecipeStorageImpl
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProviderImpl
import gq.kirmanak.mealient.data.recipes.impl.RecipeRepoImpl import gq.kirmanak.mealient.data.recipes.impl.RecipeRepoImpl
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSourceImpl import gq.kirmanak.mealient.data.recipes.network.RecipeDataSourceImpl
import gq.kirmanak.mealient.data.recipes.network.RecipeService import gq.kirmanak.mealient.data.recipes.network.RecipeService
import gq.kirmanak.mealient.ui.recipes.images.RecipeModelLoaderFactory
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.io.InputStream
import javax.inject.Named import javax.inject.Named
import javax.inject.Singleton import javax.inject.Singleton
@@ -38,6 +46,14 @@ interface RecipeModule {
@Singleton @Singleton
fun provideRecipeRepo(recipeRepoImpl: RecipeRepoImpl): RecipeRepo fun provideRecipeRepo(recipeRepoImpl: RecipeRepoImpl): RecipeRepo
@Binds
@Singleton
fun bindImageUrlProvider(recipeImageUrlProviderImpl: RecipeImageUrlProviderImpl): RecipeImageUrlProvider
@Binds
@Singleton
fun bindModelLoaderFactory(recipeModelLoaderFactory: RecipeModelLoaderFactory): ModelLoaderFactory<RecipeSummaryEntity, InputStream>
companion object { companion object {
@Provides @Provides
@@ -55,5 +71,10 @@ interface RecipeModule {
fun provideRecipePagingSourceFactory( fun provideRecipePagingSourceFactory(
recipeStorage: RecipeStorage recipeStorage: RecipeStorage
) = InvalidatingPagingSourceFactory { recipeStorage.queryRecipes() } ) = InvalidatingPagingSourceFactory { recipeStorage.queryRecipes() }
@Provides
@Singleton
fun provideGlideRequestOptions(): RequestOptions = RequestOptions.centerCropTransform()
.placeholder(R.drawable.placeholder_recipe)
} }
} }

View File

@@ -5,10 +5,10 @@ import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.components.FragmentComponent import dagger.hilt.android.components.FragmentComponent
import dagger.hilt.android.scopes.FragmentScoped import dagger.hilt.android.scopes.FragmentScoped
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageLoaderImpl import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
import gq.kirmanak.mealient.ui.images.ImageLoader import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoaderImpl
import gq.kirmanak.mealient.ui.images.ImageLoaderGlide import gq.kirmanak.mealient.ui.recipes.images.RecipePreloaderFactory
import gq.kirmanak.mealient.ui.recipes.RecipeImageLoader import gq.kirmanak.mealient.ui.recipes.images.RecipePreloaderFactoryImpl
@Module @Module
@InstallIn(FragmentComponent::class) @InstallIn(FragmentComponent::class)
@@ -16,10 +16,10 @@ interface UiModule {
@Binds @Binds
@FragmentScoped @FragmentScoped
fun bindImageLoader(imageLoaderGlide: ImageLoaderGlide): ImageLoader fun provideRecipeImageLoader(recipeImageLoaderImpl: RecipeImageLoaderImpl): RecipeImageLoader
@Binds @Binds
@FragmentScoped @FragmentScoped
fun provideRecipeImageLoader(recipeImageLoaderImpl: RecipeImageLoaderImpl): RecipeImageLoader fun bindRecipePreloaderFactory(recipePreloaderFactoryImpl: RecipePreloaderFactoryImpl): RecipePreloaderFactory
} }

View File

@@ -1,4 +1,4 @@
package gq.kirmanak.mealient.ui.images package gq.kirmanak.mealient.ui
import android.content.Context import android.content.Context
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@@ -8,6 +8,8 @@ import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.AppGlideModule import com.bumptech.glide.module.AppGlideModule
import dagger.hilt.android.EntryPointAccessors.fromApplication import dagger.hilt.android.EntryPointAccessors.fromApplication
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.di.GlideModuleEntryPoint
import timber.log.Timber import timber.log.Timber
import java.io.InputStream import java.io.InputStream
@@ -16,17 +18,32 @@ class MealieGlideModule : AppGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) { override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
super.registerComponents(context, glide, registry) super.registerComponents(context, glide, registry)
Timber.v("registerComponents() called with: context = $context, glide = $glide, registry = $registry")
replaceOkHttp(context, registry) replaceOkHttp(context, registry)
appendRecipeLoader(registry, context)
}
private fun appendRecipeLoader(registry: Registry, context: Context) {
Timber.v("appendRecipeLoader() called with: registry = $registry, context = $context")
registry.append(
RecipeSummaryEntity::class.java,
InputStream::class.java,
getEntryPoint(context).provideRecipeLoaderFactory(),
)
} }
private fun replaceOkHttp(context: Context, registry: Registry) { private fun replaceOkHttp(context: Context, registry: Registry) {
Timber.v("replaceOkHttp() called with: context = $context, registry = $registry") Timber.v("replaceOkHttp() called with: context = $context, registry = $registry")
val entryPoint = fromApplication(context, GlideModuleEntryPoint::class.java) val okHttp = getEntryPoint(context).provideOkHttp()
val okHttp = entryPoint.provideOkHttp()
registry.replace( registry.replace(
GlideUrl::class.java, GlideUrl::class.java,
InputStream::class.java, InputStream::class.java,
OkHttpUrlLoader.Factory(okHttp) OkHttpUrlLoader.Factory(okHttp)
) )
} }
private fun getEntryPoint(context: Context): GlideModuleEntryPoint {
Timber.v("getEntryPoint() called with: context = $context")
return fromApplication(context, GlideModuleEntryPoint::class.java)
}
} }

View File

@@ -1,9 +0,0 @@
package gq.kirmanak.mealient.ui.images
import android.widget.ImageView
import androidx.annotation.DrawableRes
interface ImageLoader {
fun loadImage(url: String?, @DrawableRes placeholderId: Int, imageView: ImageView)
}

View File

@@ -1,30 +0,0 @@
package gq.kirmanak.mealient.ui.images
import android.widget.ImageView
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import dagger.hilt.android.scopes.FragmentScoped
import timber.log.Timber
import javax.inject.Inject
@FragmentScoped
class ImageLoaderGlide @Inject constructor(
private val fragment: Fragment,
) : ImageLoader {
private val requestManager: RequestManager
get() = Glide.with(fragment)
init {
Timber.v("init called with fragment = ${fragment.javaClass.simpleName}")
}
override fun loadImage(url: String?, placeholderId: Int, imageView: ImageView) {
Timber.v("loadImage() called with: url = $url, placeholderId = $placeholderId, imageView = $imageView")
requestManager.load(url)
.placeholder(placeholderId)
.centerCrop()
.into(imageView)
}
}

View File

@@ -1,8 +0,0 @@
package gq.kirmanak.mealient.ui.recipes
import android.widget.ImageView
interface RecipeImageLoader {
fun loadRecipeImage(view: ImageView, slug: String?)
}

View File

@@ -4,6 +4,7 @@ import androidx.recyclerview.widget.RecyclerView
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
import timber.log.Timber import timber.log.Timber
class RecipeViewHolder( class RecipeViewHolder(
@@ -18,7 +19,7 @@ class RecipeViewHolder(
fun bind(item: RecipeSummaryEntity?) { fun bind(item: RecipeSummaryEntity?) {
Timber.v("bind() called with: item = $item") Timber.v("bind() called with: item = $item")
binding.name.text = item?.name ?: loadingPlaceholder binding.name.text = item?.name ?: loadingPlaceholder
recipeImageLoader.loadRecipeImage(binding.image, item?.slug) recipeImageLoader.loadRecipeImage(binding.image, item)
item?.let { entity -> item?.let { entity ->
binding.root.setOnClickListener { binding.root.setOnClickListener {
Timber.d("bind: item clicked $entity") Timber.d("bind: item clicked $entity")

View File

@@ -14,6 +14,8 @@ import gq.kirmanak.mealient.databinding.FragmentRecipesBinding
import gq.kirmanak.mealient.extensions.collectWhenViewResumed import gq.kirmanak.mealient.extensions.collectWhenViewResumed
import gq.kirmanak.mealient.extensions.refreshRequestFlow import gq.kirmanak.mealient.extensions.refreshRequestFlow
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel 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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -26,6 +28,9 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
@Inject @Inject
lateinit var recipeImageLoader: RecipeImageLoader lateinit var recipeImageLoader: RecipeImageLoader
@Inject
lateinit var recipePreloaderFactory: RecipePreloaderFactory
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
@@ -45,19 +50,22 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
private fun setupRecipeAdapter() { private fun setupRecipeAdapter() {
Timber.v("setupRecipeAdapter() called") Timber.v("setupRecipeAdapter() called")
val adapter = RecipesPagingAdapter(recipeImageLoader, ::navigateToRecipeInfo) val recipesAdapter = RecipesPagingAdapter(recipeImageLoader, ::navigateToRecipeInfo)
binding.recipes.adapter = adapter with(binding.recipes) {
adapter = recipesAdapter
addOnScrollListener(recipePreloaderFactory.create(recipesAdapter))
}
collectWhenViewResumed(viewModel.pagingData) { collectWhenViewResumed(viewModel.pagingData) {
Timber.v("setupRecipeAdapter: received data update") 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") Timber.v("setupRecipeAdapter: pages updated")
binding.refresher.isRefreshing = false binding.refresher.isRefreshing = false
} }
collectWhenViewResumed(binding.refresher.refreshRequestFlow()) { collectWhenViewResumed(binding.refresher.refreshRequestFlow()) {
Timber.v("setupRecipeAdapter: received refresh request") Timber.v("setupRecipeAdapter: received refresh request")
adapter.refresh() recipesAdapter.refresh()
} }
} }

View File

@@ -6,12 +6,14 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
import timber.log.Timber import timber.log.Timber
class RecipesPagingAdapter( class RecipesPagingAdapter(
private val recipeImageLoader: RecipeImageLoader, private val recipeImageLoader: RecipeImageLoader,
private val clickListener: (RecipeSummaryEntity) -> Unit private val clickListener: (RecipeSummaryEntity) -> Unit
) : PagingDataAdapter<RecipeSummaryEntity, RecipeViewHolder>(RecipeDiffCallback) { ) : PagingDataAdapter<RecipeSummaryEntity, RecipeViewHolder>(RecipeDiffCallback) {
override fun onBindViewHolder(holder: RecipeViewHolder, position: Int) { override fun onBindViewHolder(holder: RecipeViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
holder.bind(item) holder.bind(item)

View File

@@ -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?)
}

View File

@@ -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)
}
}

View File

@@ -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) }
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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>
}

View File

@@ -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
}
}

View File

@@ -14,7 +14,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.FragmentRecipeInfoBinding 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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -46,7 +46,6 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
with(binding) { with(binding) {
ingredientsList.adapter = ingredientsAdapter ingredientsList.adapter = ingredientsAdapter
instructionsList.adapter = instructionsAdapter instructionsList.adapter = instructionsAdapter
recipeImageLoader.loadRecipeImage(image, arguments.recipeSlug)
} }
with(viewModel) { with(viewModel) {
@@ -60,6 +59,7 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
ingredientsHolder.isVisible = uiState.areIngredientsVisible ingredientsHolder.isVisible = uiState.areIngredientsVisible
instructionsGroup.isVisible = uiState.areInstructionsVisible instructionsGroup.isVisible = uiState.areInstructionsVisible
uiState.recipeInfo?.let { uiState.recipeInfo?.let {
recipeImageLoader.loadRecipeImage(image, it.recipeSummaryEntity)
title.text = it.recipeSummaryEntity.name title.text = it.recipeSummaryEntity.name
description.text = it.recipeSummaryEntity.description description.text = it.recipeSummaryEntity.description
ingredientsAdapter.submitList(it.recipeIngredients) ingredientsAdapter.submitList(it.recipeIngredients)

View File

@@ -1,9 +1,7 @@
package gq.kirmanak.mealient.data.recipes.impl package gq.kirmanak.mealient.data.recipes.impl
import androidx.fragment.app.Fragment
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
import gq.kirmanak.mealient.ui.images.ImageLoader
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
@@ -13,22 +11,17 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class RecipeImageLoaderImplTest { class RecipeImageUrlProviderImplTest {
lateinit var subject: RecipeImageLoaderImpl
lateinit var subject: RecipeImageUrlProvider
@MockK @MockK
lateinit var baseURLStorage: BaseURLStorage lateinit var baseURLStorage: BaseURLStorage
@MockK
lateinit var imageLoader: ImageLoader
@MockK
lateinit var fragment: Fragment
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
subject = RecipeImageLoaderImpl(imageLoader, baseURLStorage, fragment) subject = RecipeImageUrlProviderImpl(baseURLStorage)
prepareBaseURL("https://google.com/") prepareBaseURL("https://google.com/")
} }