diff --git a/app/build.gradle b/app/build.gradle
index 3bedfb7..437ef44 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -148,8 +148,9 @@ 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"
// 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">
+
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/images/ImageLoader.kt b/app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoader.kt
index cc33ed3..d69f946 100644
--- a/app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoader.kt
+++ b/app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoader.kt
@@ -4,5 +4,6 @@ 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
new file mode 100644
index 0000000..263f63c
--- /dev/null
+++ b/app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoaderGlide.kt
@@ -0,0 +1,30 @@
+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/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
index 2afa981..92e5e3f 100644
--- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeImageLoader.kt
+++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeImageLoader.kt
@@ -3,5 +3,6 @@ package gq.kirmanak.mealient.ui.recipes
import android.widget.ImageView
interface RecipeImageLoader {
- suspend fun loadRecipeImage(view: ImageView, slug: String?)
+
+ 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..b7b5eb3 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
@@ -8,7 +8,7 @@ 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 +18,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?.slug)
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..0ee16fe 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,11 @@ 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 timber.log.Timber
+import javax.inject.Inject
@AndroidEntryPoint
class RecipesFragment : Fragment(R.layout.fragment_recipes) {
@@ -22,6 +23,9 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
private val viewModel by viewModels()
private val activityViewModel by activityViewModels()
+ @Inject
+ lateinit var recipeImageLoader: RecipeImageLoader
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
@@ -41,17 +45,17 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
private fun setupRecipeAdapter() {
Timber.v("setupRecipeAdapter() called")
- val adapter = RecipesPagingAdapter(viewModel, ::navigateToRecipeInfo)
+ val adapter = RecipesPagingAdapter(recipeImageLoader, ::navigateToRecipeInfo)
binding.recipes.adapter = adapter
- collectWithViewLifecycle(viewModel.pagingData) {
+ collectWhenViewResumed(viewModel.pagingData) {
Timber.v("setupRecipeAdapter: received data update")
adapter.submitData(lifecycle, it)
}
- collectWithViewLifecycle(adapter.onPagesUpdatedFlow) {
+ collectWhenViewResumed(adapter.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()
}
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..29cd13a 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
@@ -9,7 +9,7 @@ import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
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) {
@@ -21,7 +21,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/info/RecipeInfoFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt
index fd74d8a..30c55a3 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.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?,
@@ -41,10 +46,10 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
with(binding) {
ingredientsList.adapter = ingredientsAdapter
instructionsList.adapter = instructionsAdapter
+ recipeImageLoader.loadRecipeImage(image, arguments.recipeSlug)
}
with(viewModel) {
- loadRecipeImage(binding.image, arguments.recipeSlug)
loadRecipeInfo(arguments.recipeId, arguments.recipeSlug)
uiState.observe(viewLifecycleOwner, ::onUiStateChange)
}
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/RecipeImageLoaderImplTest.kt
index 6cedcc9..8a99478 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/RecipeImageLoaderImplTest.kt
@@ -1,5 +1,6 @@
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
@@ -21,10 +22,13 @@ class RecipeImageLoaderImplTest {
@MockK
lateinit var imageLoader: ImageLoader
+ @MockK
+ lateinit var fragment: Fragment
+
@Before
fun setUp() {
MockKAnnotations.init(this)
- subject = RecipeImageLoaderImpl(imageLoader, baseURLStorage)
+ subject = RecipeImageLoaderImpl(imageLoader, baseURLStorage, fragment)
prepareBaseURL("https://google.com/")
}