From 96bce0e5f438758aff57c86df568df39bb54f946 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Mon, 18 Apr 2022 12:42:08 +0200 Subject: [PATCH 1/6] Remove unused class --- .../mealient/ui/auth/AuthenticationState.kt | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationState.kt 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 - } - } -} From 2686f757ea6124196363b4e6dfe3282567735a0c Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Mon, 18 Apr 2022 13:17:07 +0200 Subject: [PATCH 2/6] Replace Picasso with Glide --- app/build.gradle | 5 +-- app/proguard-rules.pro | 9 +++++ app/src/main/AndroidManifest.xml | 1 + .../recipes/impl/RecipeImageLoaderImpl.kt | 13 ++++--- .../gq/kirmanak/mealient/di/RecipeModule.kt | 6 ---- .../java/gq/kirmanak/mealient/di/UiModule.kt | 26 +++++++------- .../mealient/extensions/FragmentExtensions.kt | 10 ++++-- .../mealient/ui/images/ImageLoader.kt | 1 + .../mealient/ui/images/ImageLoaderGlide.kt | 30 ++++++++++++++++ .../mealient/ui/images/ImageLoaderPicasso.kt | 25 -------------- .../mealient/ui/images/PicassoBuilder.kt | 34 ------------------- .../mealient/ui/recipes/RecipeImageLoader.kt | 3 +- .../mealient/ui/recipes/RecipeViewHolder.kt | 4 +-- .../mealient/ui/recipes/RecipeViewModel.kt | 15 +------- .../mealient/ui/recipes/RecipesFragment.kt | 14 +++++--- .../ui/recipes/RecipesPagingAdapter.kt | 4 +-- .../ui/recipes/info/RecipeInfoFragment.kt | 7 +++- .../ui/recipes/info/RecipeInfoViewModel.kt | 8 ----- .../recipes/impl/RecipeImageLoaderImplTest.kt | 6 +++- 19 files changed, 99 insertions(+), 122 deletions(-) create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoaderGlide.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/images/ImageLoaderPicasso.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/images/PicassoBuilder.kt 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/") } From 1ffd2d2359b938902eaa5679d60f005234a10ca8 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Mon, 18 Apr 2022 13:38:19 +0200 Subject: [PATCH 3/6] Use OkHttp for Glide requests --- app/build.gradle | 2 ++ .../ui/images/GlideModuleEntryPoint.kt | 18 +++++++++++ .../mealient/ui/images/MealieGlideModule.kt | 32 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/images/GlideModuleEntryPoint.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/images/MealieGlideModule.kt diff --git a/app/build.gradle b/app/build.gradle index 437ef44..b7a1324 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -151,6 +151,8 @@ dependencies { // 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" + 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/src/main/java/gq/kirmanak/mealient/ui/images/GlideModuleEntryPoint.kt b/app/src/main/java/gq/kirmanak/mealient/ui/images/GlideModuleEntryPoint.kt new file mode 100644 index 0000000..a842a35 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/images/GlideModuleEntryPoint.kt @@ -0,0 +1,18 @@ +package gq.kirmanak.mealient.ui.images + +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import gq.kirmanak.mealient.di.AUTH_OK_HTTP +import okhttp3.OkHttpClient +import javax.inject.Named +import javax.inject.Singleton + +@EntryPoint +@InstallIn(SingletonComponent::class) +interface GlideModuleEntryPoint { + + @Singleton + @Named(AUTH_OK_HTTP) + fun provideOkHttp(): OkHttpClient +} \ 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/images/MealieGlideModule.kt new file mode 100644 index 0000000..0a7d896 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/images/MealieGlideModule.kt @@ -0,0 +1,32 @@ +package gq.kirmanak.mealient.ui.images + +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 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) + replaceOkHttp(context, registry) + } + + 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() + registry.replace( + GlideUrl::class.java, + InputStream::class.java, + OkHttpUrlLoader.Factory(okHttp) + ) + } +} From 3079cd95886bfc9f5cc82bbd09dd564bb4253a0d Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Mon, 18 Apr 2022 18:08:06 +0200 Subject: [PATCH 4/6] 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/") } From 32a55f96b9c5cdff19480dada64f7e6184b08af7 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Mon, 18 Apr 2022 18:15:18 +0200 Subject: [PATCH 5/6] Update dependencies --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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" From 9a6fd3ca360ba56780b4c0af82a700d27260af91 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Mon, 18 Apr 2022 18:16:04 +0200 Subject: [PATCH 6/6] Bump version to 0.2.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index de24a8d..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 {