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/") }