From 3079cd95886bfc9f5cc82bbd09dd564bb4253a0d Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Mon, 18 Apr 2022 18:08:06 +0200 Subject: [PATCH] Implement Glide image preload in RecyclerView --- app/build.gradle | 4 ++ .../recipes/impl/RecipeImageLoaderImpl.kt | 44 ------------------- .../recipes/impl/RecipeImageUrlProvider.kt | 6 +++ .../impl/RecipeImageUrlProviderImpl.kt | 31 +++++++++++++ .../images => di}/GlideModuleEntryPoint.kt | 10 +++-- .../gq/kirmanak/mealient/di/RecipeModule.kt | 21 +++++++++ .../java/gq/kirmanak/mealient/di/UiModule.kt | 12 ++--- .../ui/{images => }/MealieGlideModule.kt | 23 ++++++++-- .../mealient/ui/images/ImageLoader.kt | 9 ---- .../mealient/ui/images/ImageLoaderGlide.kt | 30 ------------- .../mealient/ui/recipes/RecipeImageLoader.kt | 8 ---- .../mealient/ui/recipes/RecipeViewHolder.kt | 3 +- .../mealient/ui/recipes/RecipesFragment.kt | 18 +++++--- .../ui/recipes/RecipesPagingAdapter.kt | 2 + .../ui/recipes/images/RecipeImageLoader.kt | 9 ++++ .../recipes/images/RecipeImageLoaderImpl.kt | 22 ++++++++++ .../ui/recipes/images/RecipeModelLoader.kt | 31 +++++++++++++ .../images/RecipeModelLoaderFactory.kt | 27 ++++++++++++ .../images/RecipePreloadModelProvider.kt | 40 +++++++++++++++++ .../recipes/images/RecipePreloaderFactory.kt | 10 +++++ .../images/RecipePreloaderFactoryImpl.kt | 33 ++++++++++++++ .../ui/recipes/info/RecipeInfoFragment.kt | 4 +- ...t.kt => RecipeImageUrlProviderImplTest.kt} | 15 ++----- 23 files changed, 289 insertions(+), 123 deletions(-) delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageLoaderImpl.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProvider.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImpl.kt rename app/src/main/java/gq/kirmanak/mealient/{ui/images => di}/GlideModuleEntryPoint.kt (52%) rename app/src/main/java/gq/kirmanak/mealient/ui/{images => }/MealieGlideModule.kt (51%) delete mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoader.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoaderGlide.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeImageLoader.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeImageLoader.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeImageLoaderImpl.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoader.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoaderFactory.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloadModelProvider.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloaderFactory.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloaderFactoryImpl.kt rename app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/{RecipeImageLoaderImplTest.kt => RecipeImageUrlProviderImplTest.kt} (86%) diff --git a/app/build.gradle b/app/build.gradle index b7a1324..de24a8d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -152,6 +152,10 @@ dependencies { def glide_version = "4.13.1" implementation "com.github.bumptech.glide:glide:$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" // https://github.com/androidbroadcast/ViewBindingPropertyDelegate/releases diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageLoaderImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageLoaderImpl.kt deleted file mode 100644 index c3dfdda..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageLoaderImpl.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProvider.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProvider.kt new file mode 100644 index 0000000..2d05869 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProvider.kt @@ -0,0 +1,6 @@ +package gq.kirmanak.mealient.data.recipes.impl + +interface RecipeImageUrlProvider { + + suspend fun generateImageUrl(slug: String?): String? +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImpl.kt new file mode 100644 index 0000000..94a83e6 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImpl.kt @@ -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" + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/images/GlideModuleEntryPoint.kt b/app/src/main/java/gq/kirmanak/mealient/di/GlideModuleEntryPoint.kt similarity index 52% rename from app/src/main/java/gq/kirmanak/mealient/ui/images/GlideModuleEntryPoint.kt rename to app/src/main/java/gq/kirmanak/mealient/di/GlideModuleEntryPoint.kt index a842a35..b135470 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/images/GlideModuleEntryPoint.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/GlideModuleEntryPoint.kt @@ -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.InstallIn 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 java.io.InputStream import javax.inject.Named -import javax.inject.Singleton @EntryPoint @InstallIn(SingletonComponent::class) interface GlideModuleEntryPoint { - @Singleton @Named(AUTH_OK_HTTP) fun provideOkHttp(): OkHttpClient + + fun provideRecipeLoaderFactory(): ModelLoaderFactory } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt index 710a43a..fc015b5 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt @@ -1,11 +1,14 @@ package gq.kirmanak.mealient.di import androidx.paging.InvalidatingPagingSourceFactory +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.request.RequestOptions import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.network.RetrofitBuilder 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.db.RecipeStorage 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.network.RecipeDataSource import gq.kirmanak.mealient.data.recipes.network.RecipeDataSourceImpl import gq.kirmanak.mealient.data.recipes.network.RecipeService +import gq.kirmanak.mealient.ui.recipes.images.RecipeModelLoaderFactory import kotlinx.serialization.json.Json import okhttp3.OkHttpClient +import java.io.InputStream import javax.inject.Named import javax.inject.Singleton @@ -38,6 +46,14 @@ interface RecipeModule { @Singleton fun provideRecipeRepo(recipeRepoImpl: RecipeRepoImpl): RecipeRepo + @Binds + @Singleton + fun bindImageUrlProvider(recipeImageUrlProviderImpl: RecipeImageUrlProviderImpl): RecipeImageUrlProvider + + @Binds + @Singleton + fun bindModelLoaderFactory(recipeModelLoaderFactory: RecipeModelLoaderFactory): ModelLoaderFactory + companion object { @Provides @@ -55,5 +71,10 @@ interface RecipeModule { fun provideRecipePagingSourceFactory( recipeStorage: RecipeStorage ) = InvalidatingPagingSourceFactory { recipeStorage.queryRecipes() } + + @Provides + @Singleton + fun provideGlideRequestOptions(): RequestOptions = RequestOptions.centerCropTransform() + .placeholder(R.drawable.placeholder_recipe) } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/di/UiModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/UiModule.kt index 71d11e5..54f20dd 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/UiModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/UiModule.kt @@ -5,10 +5,10 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.android.components.FragmentComponent import dagger.hilt.android.scopes.FragmentScoped -import gq.kirmanak.mealient.data.recipes.impl.RecipeImageLoaderImpl -import gq.kirmanak.mealient.ui.images.ImageLoader -import gq.kirmanak.mealient.ui.images.ImageLoaderGlide -import gq.kirmanak.mealient.ui.recipes.RecipeImageLoader +import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader +import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoaderImpl +import gq.kirmanak.mealient.ui.recipes.images.RecipePreloaderFactory +import gq.kirmanak.mealient.ui.recipes.images.RecipePreloaderFactoryImpl @Module @InstallIn(FragmentComponent::class) @@ -16,10 +16,10 @@ interface UiModule { @Binds @FragmentScoped - fun bindImageLoader(imageLoaderGlide: ImageLoaderGlide): ImageLoader + fun provideRecipeImageLoader(recipeImageLoaderImpl: RecipeImageLoaderImpl): RecipeImageLoader @Binds @FragmentScoped - fun provideRecipeImageLoader(recipeImageLoaderImpl: RecipeImageLoaderImpl): RecipeImageLoader + fun bindRecipePreloaderFactory(recipePreloaderFactoryImpl: RecipePreloaderFactoryImpl): RecipePreloaderFactory } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/images/MealieGlideModule.kt b/app/src/main/java/gq/kirmanak/mealient/ui/MealieGlideModule.kt similarity index 51% rename from app/src/main/java/gq/kirmanak/mealient/ui/images/MealieGlideModule.kt rename to app/src/main/java/gq/kirmanak/mealient/ui/MealieGlideModule.kt index 0a7d896..93a040f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/images/MealieGlideModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/MealieGlideModule.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.ui.images +package gq.kirmanak.mealient.ui import android.content.Context 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.module.AppGlideModule 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 java.io.InputStream @@ -16,17 +18,32 @@ class MealieGlideModule : AppGlideModule() { override fun registerComponents(context: Context, glide: Glide, registry: Registry) { super.registerComponents(context, glide, registry) + Timber.v("registerComponents() called with: context = $context, glide = $glide, registry = $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) { Timber.v("replaceOkHttp() called with: context = $context, registry = $registry") - val entryPoint = fromApplication(context, GlideModuleEntryPoint::class.java) - val okHttp = entryPoint.provideOkHttp() + val okHttp = getEntryPoint(context).provideOkHttp() registry.replace( GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(okHttp) ) } + + private fun getEntryPoint(context: Context): GlideModuleEntryPoint { + Timber.v("getEntryPoint() called with: context = $context") + return fromApplication(context, GlideModuleEntryPoint::class.java) + } } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoader.kt b/app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoader.kt deleted file mode 100644 index d69f946..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoader.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoaderGlide.kt b/app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoaderGlide.kt deleted file mode 100644 index 263f63c..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoaderGlide.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeImageLoader.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeImageLoader.kt deleted file mode 100644 index 92e5e3f..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeImageLoader.kt +++ /dev/null @@ -1,8 +0,0 @@ -package gq.kirmanak.mealient.ui.recipes - -import android.widget.ImageView - -interface RecipeImageLoader { - - fun loadRecipeImage(view: ImageView, slug: String?) -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt index b7b5eb3..f55c9fd 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt @@ -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") diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt index 0ee16fe..f4a6d44 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt @@ -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() } } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesPagingAdapter.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesPagingAdapter.kt index 29cd13a..a1d5ec0 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesPagingAdapter.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesPagingAdapter.kt @@ -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(RecipeDiffCallback) { + override fun onBindViewHolder(holder: RecipeViewHolder, position: Int) { val item = getItem(position) holder.bind(item) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeImageLoader.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeImageLoader.kt new file mode 100644 index 0000000..ea06a04 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeImageLoader.kt @@ -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?) +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeImageLoaderImpl.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeImageLoaderImpl.kt new file mode 100644 index 0000000..1cedd13 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeImageLoaderImpl.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoader.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoader.kt new file mode 100644 index 0000000..267d4f1 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoader.kt @@ -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, + cache: ModelCache, +) : BaseGlideUrlLoader(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) } + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoaderFactory.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoaderFactory.kt new file mode 100644 index 0000000..59ee91f --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoaderFactory.kt @@ -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 { + + private val cache = ModelCache() + + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloadModelProvider.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloadModelProvider.kt new file mode 100644 index 0000000..577e988 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloadModelProvider.kt @@ -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, + private val fragment: Fragment, + private val requestOptions: RequestOptions, +) : ListPreloader.PreloadModelProvider { + + override fun getPreloadItems(position: Int): List { + 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, + ) = RecipePreloadModelProvider(adapter, fragment, requestOptions) + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloaderFactory.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloaderFactory.kt new file mode 100644 index 0000000..0455fe5 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloaderFactory.kt @@ -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): RecyclerViewPreloader +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloaderFactoryImpl.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloaderFactoryImpl.kt new file mode 100644 index 0000000..c9c707c --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloaderFactoryImpl.kt @@ -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, + ): RecyclerViewPreloader { + val preloadSizeProvider = ViewPreloadSizeProvider() + val preloadModelProvider = recipePreloadModelProvider.create(adapter) + return RecyclerViewPreloader( + fragment, + preloadModelProvider, + preloadSizeProvider, + MAX_PRELOAD + ) + } + + companion object { + const val MAX_PRELOAD = 10 + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt index 30c55a3..e52ff11 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt @@ -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) diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageLoaderImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImplTest.kt similarity index 86% rename from app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageLoaderImplTest.kt rename to app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImplTest.kt index 8a99478..03661e4 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageLoaderImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImplTest.kt @@ -1,9 +1,7 @@ package gq.kirmanak.mealient.data.recipes.impl -import androidx.fragment.app.Fragment import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.baseurl.BaseURLStorage -import gq.kirmanak.mealient.ui.images.ImageLoader import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.impl.annotations.MockK @@ -13,22 +11,17 @@ import org.junit.Before import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) -class RecipeImageLoaderImplTest { - lateinit var subject: RecipeImageLoaderImpl +class RecipeImageUrlProviderImplTest { + + lateinit var subject: RecipeImageUrlProvider @MockK lateinit var baseURLStorage: BaseURLStorage - @MockK - lateinit var imageLoader: ImageLoader - - @MockK - lateinit var fragment: Fragment - @Before fun setUp() { MockKAnnotations.init(this) - subject = RecipeImageLoaderImpl(imageLoader, baseURLStorage, fragment) + subject = RecipeImageUrlProviderImpl(baseURLStorage) prepareBaseURL("https://google.com/") }