diff --git a/app/build.gradle b/app/build.gradle
index 3bedfb7..65bdcd6 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -14,8 +14,8 @@ android {
applicationId "gq.kirmanak.mealient"
minSdk 23
targetSdk 31
- versionCode 10
- versionName "0.2.1"
+ versionCode 11
+ versionName "0.2.2"
javaCompileOptions {
annotationProcessorOptions {
@@ -148,8 +148,15 @@ dependencies {
// https://github.com/Kotlin/kotlinx-datetime/releases
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.3.1"
- // https://github.com/square/picasso/releases
- implementation "com.squareup.picasso:picasso:2.8"
+ // https://github.com/bumptech/glide/releases
+ 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
implementation "com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6"
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 079843f..564b7b5 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -48,3 +48,12 @@
-dontwarn org.bouncycastle.**
-dontwarn org.openjsse.**
### OkHttp warnings ###
+
+### Glide https://bumptech.github.io/glide/doc/download-setup.html#proguard ###
+-keep public class * implements com.bumptech.glide.module.GlideModule
+-keep public class * extends com.bumptech.glide.module.AppGlideModule
+-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
+ **[] $VALUES;
+ public *;
+}
+### Glide https://bumptech.github.io/glide/doc/download-setup.html#proguard ###
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3c9d105..66a71cc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
package="gq.kirmanak.mealient">
+
+}
\ 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 ad4dd94..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,14 +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.impl.RecipeImageLoaderImpl
+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.RecipeImageLoader
+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
@@ -42,7 +48,11 @@ interface RecipeModule {
@Binds
@Singleton
- fun provideRecipeImageLoader(recipeImageLoaderImpl: RecipeImageLoaderImpl): RecipeImageLoader
+ fun bindImageUrlProvider(recipeImageUrlProviderImpl: RecipeImageUrlProviderImpl): RecipeImageUrlProvider
+
+ @Binds
+ @Singleton
+ fun bindModelLoaderFactory(recipeModelLoaderFactory: RecipeModelLoaderFactory): ModelLoaderFactory
companion object {
@@ -61,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 1d31cf5..54f20dd 100644
--- a/app/src/main/java/gq/kirmanak/mealient/di/UiModule.kt
+++ b/app/src/main/java/gq/kirmanak/mealient/di/UiModule.kt
@@ -1,27 +1,25 @@
package gq.kirmanak.mealient.di
-import com.squareup.picasso.Picasso
import dagger.Binds
import dagger.Module
-import dagger.Provides
import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import gq.kirmanak.mealient.ui.images.ImageLoader
-import gq.kirmanak.mealient.ui.images.ImageLoaderPicasso
-import gq.kirmanak.mealient.ui.images.PicassoBuilder
-import javax.inject.Singleton
+import dagger.hilt.android.components.FragmentComponent
+import dagger.hilt.android.scopes.FragmentScoped
+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(SingletonComponent::class)
+@InstallIn(FragmentComponent::class)
interface UiModule {
@Binds
- @Singleton
- fun bindImageLoader(imageLoaderGlide: ImageLoaderPicasso): ImageLoader
+ @FragmentScoped
+ fun provideRecipeImageLoader(recipeImageLoaderImpl: RecipeImageLoaderImpl): RecipeImageLoader
+
+ @Binds
+ @FragmentScoped
+ fun bindRecipePreloaderFactory(recipePreloaderFactoryImpl: RecipePreloaderFactoryImpl): RecipePreloaderFactory
- companion object {
- @Provides
- @Singleton
- fun providePicasso(picassoBuilder: PicassoBuilder): Picasso = picassoBuilder.buildPicasso()
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt
index b597b95..c285394 100644
--- a/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt
+++ b/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt
@@ -2,11 +2,15 @@ package gq.kirmanak.mealient.extensions
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
-inline fun Fragment.collectWithViewLifecycle(
+inline fun Fragment.collectWhenViewResumed(
flow: Flow,
crossinline collector: suspend (T) -> Unit,
-) = viewLifecycleOwner.lifecycleScope.launch { flow.collect(collector) }
\ No newline at end of file
+) = launchWhenViewResumed { flow.collect(collector) }
+
+fun Fragment.launchWhenViewResumed(
+ block: suspend CoroutineScope.() -> Unit,
+) = viewLifecycleOwner.lifecycleScope.launchWhenResumed(block)
\ No newline at end of file
diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/MealieGlideModule.kt b/app/src/main/java/gq/kirmanak/mealient/ui/MealieGlideModule.kt
new file mode 100644
index 0000000..93a040f
--- /dev/null
+++ b/app/src/main/java/gq/kirmanak/mealient/ui/MealieGlideModule.kt
@@ -0,0 +1,49 @@
+package gq.kirmanak.mealient.ui
+
+import android.content.Context
+import com.bumptech.glide.Glide
+import com.bumptech.glide.Registry
+import com.bumptech.glide.annotation.GlideModule
+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
+
+@GlideModule
+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 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/auth/AuthenticationState.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationState.kt
deleted file mode 100644
index 72429c7..0000000
--- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationState.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package gq.kirmanak.mealient.ui.auth
-
-import timber.log.Timber
-
-enum class AuthenticationState {
- AUTHORIZED,
- UNAUTHORIZED,
- HIDDEN;
-
- companion object {
-
- fun determineState(
- showLoginButton: Boolean,
- isAuthorized: Boolean,
- ): AuthenticationState {
- Timber.v("determineState() called with: showLoginButton = $showLoginButton, isAuthorized = $isAuthorized")
- val result = when {
- !showLoginButton -> HIDDEN
- isAuthorized -> AUTHORIZED
- else -> UNAUTHORIZED
- }
- Timber.v("determineState() returned: $result")
- return result
- }
- }
-}
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 cc33ed3..0000000
--- a/app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoader.kt
+++ /dev/null
@@ -1,8 +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/ImageLoaderPicasso.kt b/app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoaderPicasso.kt
deleted file mode 100644
index 5dc35ec..0000000
--- a/app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoaderPicasso.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package gq.kirmanak.mealient.ui.images
-
-import android.widget.ImageView
-import com.squareup.picasso.Picasso
-import timber.log.Timber
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class ImageLoaderPicasso @Inject constructor(
- private val picasso: Picasso
-) : ImageLoader {
-
- override fun loadImage(url: String?, placeholderId: Int, imageView: ImageView) {
- Timber.v("loadImage() called with: url = $url, placeholderId = $placeholderId, imageView = $imageView")
- val width = imageView.measuredWidth
- val height = imageView.measuredHeight
- Timber.d("loadImage: width = $width, height = $height")
- picasso.load(url).apply {
- placeholder(placeholderId)
- if (width > 0 && height > 0) resize(width, height).centerCrop()
- into(imageView)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/images/PicassoBuilder.kt b/app/src/main/java/gq/kirmanak/mealient/ui/images/PicassoBuilder.kt
deleted file mode 100644
index 9933996..0000000
--- a/app/src/main/java/gq/kirmanak/mealient/ui/images/PicassoBuilder.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package gq.kirmanak.mealient.ui.images
-
-import android.content.Context
-import com.squareup.picasso.OkHttp3Downloader
-import com.squareup.picasso.Picasso
-import dagger.hilt.android.qualifiers.ApplicationContext
-import gq.kirmanak.mealient.BuildConfig
-import gq.kirmanak.mealient.di.AUTH_OK_HTTP
-import okhttp3.OkHttpClient
-import timber.log.Timber
-import javax.inject.Inject
-import javax.inject.Named
-import javax.inject.Singleton
-
-@Singleton
-class PicassoBuilder @Inject constructor(
- @ApplicationContext private val context: Context,
- @Named(AUTH_OK_HTTP) private val okHttpClient: OkHttpClient
-) {
-
- fun buildPicasso(): Picasso {
- Timber.v("buildPicasso() called")
- val builder = Picasso.Builder(context)
- builder.downloader(OkHttp3Downloader(okHttpClient))
- if (BuildConfig.DEBUG_PICASSO) {
- builder.loggingEnabled(true)
- builder.indicatorsEnabled(true)
- builder.listener { _, uri, exception ->
- Timber.tag("Picasso").e(exception, "Can't load from $uri")
- }
- }
- return builder.build()
- }
-}
\ 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 2afa981..0000000
--- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeImageLoader.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package gq.kirmanak.mealient.ui.recipes
-
-import android.widget.ImageView
-
-interface RecipeImageLoader {
- suspend 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 64fd1f3..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,11 +4,12 @@ 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(
private val binding: ViewHolderRecipeBinding,
- private val recipeViewModel: RecipeViewModel,
+ private val recipeImageLoader: RecipeImageLoader,
private val clickListener: (RecipeSummaryEntity) -> Unit,
) : RecyclerView.ViewHolder(binding.root) {
private val loadingPlaceholder by lazy {
@@ -18,7 +19,7 @@ class RecipeViewHolder(
fun bind(item: RecipeSummaryEntity?) {
Timber.v("bind() called with: item = $item")
binding.name.text = item?.name ?: loadingPlaceholder
- recipeViewModel.loadRecipeImage(binding.image, item)
+ 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/RecipeViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewModel.kt
index 6b71714..b621488 100644
--- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewModel.kt
+++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewModel.kt
@@ -1,28 +1,15 @@
package gq.kirmanak.mealient.ui.recipes
-import android.widget.ImageView
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.recipes.RecipeRepo
-import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
-import kotlinx.coroutines.launch
-import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
-class RecipeViewModel @Inject constructor(
- recipeRepo: RecipeRepo,
- private val recipeImageLoader: RecipeImageLoader
-) : ViewModel() {
+class RecipeViewModel @Inject constructor(recipeRepo: RecipeRepo) : ViewModel() {
val pagingData = recipeRepo.createPager().flow.cachedIn(viewModelScope)
- fun loadRecipeImage(view: ImageView, recipeSummary: RecipeSummaryEntity?) {
- Timber.v("loadRecipeImage() called with: view = $view, recipeSummary = $recipeSummary")
- viewModelScope.launch {
- recipeImageLoader.loadRecipeImage(view, recipeSummary?.slug)
- }
- }
}
\ No newline at end of file
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 a961ea4..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
@@ -11,10 +11,13 @@ import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.databinding.FragmentRecipesBinding
-import gq.kirmanak.mealient.extensions.collectWithViewLifecycle
+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
@AndroidEntryPoint
class RecipesFragment : Fragment(R.layout.fragment_recipes) {
@@ -22,6 +25,12 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
private val viewModel by viewModels()
private val activityViewModel by activityViewModels()
+ @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")
@@ -41,19 +50,22 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
private fun setupRecipeAdapter() {
Timber.v("setupRecipeAdapter() called")
- val adapter = RecipesPagingAdapter(viewModel, ::navigateToRecipeInfo)
- binding.recipes.adapter = adapter
- collectWithViewLifecycle(viewModel.pagingData) {
- Timber.v("setupRecipeAdapter: received data update")
- adapter.submitData(lifecycle, it)
+ val recipesAdapter = RecipesPagingAdapter(recipeImageLoader, ::navigateToRecipeInfo)
+ with(binding.recipes) {
+ adapter = recipesAdapter
+ addOnScrollListener(recipePreloaderFactory.create(recipesAdapter))
}
- collectWithViewLifecycle(adapter.onPagesUpdatedFlow) {
+ collectWhenViewResumed(viewModel.pagingData) {
+ Timber.v("setupRecipeAdapter: received data update")
+ recipesAdapter.submitData(lifecycle, it)
+ }
+ collectWhenViewResumed(recipesAdapter.onPagesUpdatedFlow) {
Timber.v("setupRecipeAdapter: pages updated")
binding.refresher.isRefreshing = false
}
- collectWithViewLifecycle(binding.refresher.refreshRequestFlow()) {
+ 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 66d5c14..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 viewModel: RecipeViewModel,
+ 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)
@@ -21,7 +23,7 @@ class RecipesPagingAdapter(
Timber.v("onCreateViewHolder() called with: parent = $parent, viewType = $viewType")
val inflater = LayoutInflater.from(parent.context)
val binding = ViewHolderRecipeBinding.inflate(inflater, parent, false)
- return RecipeViewHolder(binding, viewModel, clickListener)
+ return RecipeViewHolder(binding, recipeImageLoader, clickListener)
}
private object RecipeDiffCallback : DiffUtil.ItemCallback() {
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 fd74d8a..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,9 @@ 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.images.RecipeImageLoader
import timber.log.Timber
+import javax.inject.Inject
@AndroidEntryPoint
class RecipeInfoFragment : BottomSheetDialogFragment() {
@@ -25,6 +27,9 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
private val ingredientsAdapter = RecipeIngredientsAdapter()
private val instructionsAdapter = RecipeInstructionsAdapter()
+ @Inject
+ lateinit var recipeImageLoader: RecipeImageLoader
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -44,7 +49,6 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
}
with(viewModel) {
- loadRecipeImage(binding.image, arguments.recipeSlug)
loadRecipeInfo(arguments.recipeId, arguments.recipeSlug)
uiState.observe(viewLifecycleOwner, ::onUiStateChange)
}
@@ -55,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/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt
index 5bc7242..08f3502 100644
--- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt
+++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt
@@ -1,6 +1,5 @@
package gq.kirmanak.mealient.ui.recipes.info
-import android.widget.ImageView
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@@ -8,7 +7,6 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
-import gq.kirmanak.mealient.ui.recipes.RecipeImageLoader
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -16,17 +14,11 @@ import javax.inject.Inject
@HiltViewModel
class RecipeInfoViewModel @Inject constructor(
private val recipeRepo: RecipeRepo,
- private val recipeImageLoader: RecipeImageLoader,
) : ViewModel() {
private val _uiState = MutableLiveData(RecipeInfoUiState())
val uiState: LiveData get() = _uiState
- fun loadRecipeImage(view: ImageView, recipeSlug: String) {
- Timber.v("loadRecipeImage() called with: view = $view, recipeSlug = $recipeSlug")
- viewModelScope.launch { recipeImageLoader.loadRecipeImage(view, recipeSlug) }
- }
-
fun loadRecipeInfo(recipeId: Long, recipeSlug: String) {
Timber.v("loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug")
_uiState.value = RecipeInfoUiState()
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 90%
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 6cedcc9..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
@@ -2,7 +2,6 @@ package gq.kirmanak.mealient.data.recipes.impl
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
@@ -12,19 +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
-
@Before
fun setUp() {
MockKAnnotations.init(this)
- subject = RecipeImageLoaderImpl(imageLoader, baseURLStorage)
+ subject = RecipeImageUrlProviderImpl(baseURLStorage)
prepareBaseURL("https://google.com/")
}
diff --git a/build.gradle b/build.gradle
index 5eab59b..f6d1e61 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,13 +1,13 @@
buildscript {
ext {
// https://developer.android.com/jetpack/androidx/releases/navigation
- nav_version = "2.4.1"
+ nav_version = "2.4.2"
// https://dagger.dev/hilt/gradle-setup
hilt_version = "2.41"
// https://kotlinlang.org/docs/gradle.html
- kotlin_version = "1.6.10"
+ kotlin_version = "1.6.20"
}
repositories {
@@ -17,7 +17,7 @@ buildscript {
dependencies {
// https://maven.google.com/web/index.html?q=com.android.tools.build#com.android.tools.build:gradle
- classpath "com.android.tools.build:gradle:7.1.2"
+ classpath "com.android.tools.build:gradle:7.1.3"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"