Update favorite status on icon click

This commit is contained in:
Kirill Kamakin
2022-12-13 20:14:16 +01:00
parent 3eb99206e8
commit 2fa43f57b7
16 changed files with 170 additions and 21 deletions

View File

@@ -68,4 +68,26 @@ class MealieDataSourceWrapper @Inject constructor(
ServerVersion.V0 -> v0Source.requestUserInfo().favoriteRecipes
ServerVersion.V1 -> v1Source.requestUserInfo().favoriteRecipes
}
override suspend fun removeFavoriteRecipe(recipeSlug: String) = when (getVersion()) {
ServerVersion.V0 -> {
val userId = v0Source.requestUserInfo().id
v0Source.removeFavoriteRecipe(userId, recipeSlug)
}
ServerVersion.V1 -> {
val userId = v1Source.requestUserInfo().id
v1Source.removeFavoriteRecipe(userId, recipeSlug)
}
}
override suspend fun addFavoriteRecipe(recipeSlug: String) = when (getVersion()) {
ServerVersion.V0 -> {
val userId = v0Source.requestUserInfo().id
v0Source.addFavoriteRecipe(userId, recipeSlug)
}
ServerVersion.V1 -> {
val userId = v1Source.requestUserInfo().id
v1Source.addFavoriteRecipe(userId, recipeSlug)
}
}
}

View File

@@ -17,4 +17,8 @@ interface RecipeRepo {
fun updateNameQuery(name: String?)
suspend fun refreshRecipes()
suspend fun removeFavoriteRecipe(recipeSlug: String)
suspend fun addFavoriteRecipe(recipeSlug: String)
}

View File

@@ -72,6 +72,24 @@ class RecipeRepoImpl @Inject constructor(
}
}
override suspend fun removeFavoriteRecipe(recipeSlug: String) {
logger.v { "removeFavoriteRecipe() called with: recipeSlug = $recipeSlug" }
runCatchingExceptCancel {
dataSource.removeFavoriteRecipe(recipeSlug)
}.onFailure {
logger.e(it) { "Can't remove a favorite recipe" }
}
}
override suspend fun addFavoriteRecipe(recipeSlug: String) {
logger.v { "addFavoriteRecipe() called with: recipeSlug = $recipeSlug" }
runCatchingExceptCancel {
dataSource.addFavoriteRecipe(recipeSlug)
}.onFailure {
logger.e(it) { "Can't add a favorite recipe" }
}
}
companion object {
private const val LOAD_PAGE_SIZE = 50
private const val INITIAL_LOAD_PAGE_SIZE = LOAD_PAGE_SIZE * 3

View File

@@ -6,4 +6,8 @@ interface RecipeDataSource {
suspend fun requestRecipeInfo(slug: String): FullRecipeInfo
suspend fun getFavoriteRecipes(): List<String>
suspend fun removeFavoriteRecipe(recipeSlug: String)
suspend fun addFavoriteRecipe(recipeSlug: String)
}

View File

@@ -3,6 +3,7 @@ package gq.kirmanak.mealient.ui.recipes
import android.view.View
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.scopes.FragmentScoped
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
@@ -10,28 +11,41 @@ import gq.kirmanak.mealient.extensions.resources
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
import javax.inject.Inject
import javax.inject.Singleton
class RecipeViewHolder private constructor(
private val logger: Logger,
private val binding: ViewHolderRecipeBinding,
private val recipeImageLoader: RecipeImageLoader,
private val clickListener: (RecipeSummaryEntity) -> Unit,
private val clickListener: (ClickEvent) -> Unit,
) : RecyclerView.ViewHolder(binding.root) {
@Singleton
@FragmentScoped
class Factory @Inject constructor(
private val recipeImageLoader: RecipeImageLoader,
private val logger: Logger,
) {
fun build(
recipeImageLoader: RecipeImageLoader,
binding: ViewHolderRecipeBinding,
clickListener: (RecipeSummaryEntity) -> Unit,
clickListener: (ClickEvent) -> Unit,
) = RecipeViewHolder(logger, binding, recipeImageLoader, clickListener)
}
sealed class ClickEvent {
abstract val recipeSummaryEntity: RecipeSummaryEntity
data class FavoriteClick(
override val recipeSummaryEntity: RecipeSummaryEntity
) : ClickEvent()
data class RecipeClick(
override val recipeSummaryEntity: RecipeSummaryEntity
) : ClickEvent()
}
private val loadingPlaceholder by lazy {
binding.resources.getString(R.string.view_holder_recipe_text_placeholder)
}
@@ -43,7 +57,10 @@ class RecipeViewHolder private constructor(
item?.let { entity ->
binding.root.setOnClickListener {
logger.d { "bind: item clicked $entity" }
clickListener(entity)
clickListener(ClickEvent.RecipeClick(entity))
}
binding.favoriteIcon.setOnClickListener {
clickListener(ClickEvent.FavoriteClick(entity))
}
binding.favoriteIcon.setImageResource(
if (item.isFavorite) {

View File

@@ -90,7 +90,16 @@ class RecipesListFragment : Fragment(R.layout.fragment_recipes_list) {
private fun setupRecipeAdapter() {
logger.v { "setupRecipeAdapter() called" }
val recipesAdapter = recipePagingAdapterFactory.build { onRecipeClicked(it) }
val recipesAdapter = recipePagingAdapterFactory.build {
when (it) {
is RecipeViewHolder.ClickEvent.FavoriteClick -> {
viewModel.onFavoriteIconClick(it.recipeSummaryEntity)
}
is RecipeViewHolder.ClickEvent.RecipeClick -> {
onRecipeClicked(it.recipeSummaryEntity)
}
}
}
with(binding.recipes) {
adapter = recipesAdapter

View File

@@ -8,10 +8,12 @@ import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.extensions.valueUpdatesOnly
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
@@ -38,4 +40,15 @@ class RecipesListViewModel @Inject constructor(
emit(result)
}
}
fun onFavoriteIconClick(recipeSummaryEntity: RecipeSummaryEntity) {
logger.v { "onFavoriteIconClick() called with: recipeSummaryEntity = $recipeSummaryEntity" }
viewModelScope.launch {
if (recipeSummaryEntity.isFavorite) {
recipeRepo.removeFavoriteRecipe(recipeSummaryEntity.slug)
} else {
recipeRepo.addFavoriteRecipe(recipeSummaryEntity.slug)
}
}
}
}

View File

@@ -8,28 +8,22 @@ import dagger.hilt.android.scopes.FragmentScoped
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
import javax.inject.Inject
class RecipesPagingAdapter private constructor(
private val logger: Logger,
private val recipeImageLoader: RecipeImageLoader,
private val recipeViewHolderFactory: RecipeViewHolder.Factory,
private val clickListener: (RecipeSummaryEntity) -> Unit
private val clickListener: (RecipeViewHolder.ClickEvent) -> Unit
) : PagingDataAdapter<RecipeSummaryEntity, RecipeViewHolder>(RecipeDiffCallback) {
@FragmentScoped
class Factory @Inject constructor(
private val logger: Logger,
private val recipeViewHolderFactory: RecipeViewHolder.Factory,
private val recipeImageLoader: RecipeImageLoader,
) {
fun build(clickListener: (RecipeSummaryEntity) -> Unit) = RecipesPagingAdapter(
logger,
recipeImageLoader,
recipeViewHolderFactory,
clickListener
fun build(clickListener: (RecipeViewHolder.ClickEvent) -> Unit) = RecipesPagingAdapter(
logger, recipeViewHolderFactory, clickListener
)
}
@@ -43,18 +37,16 @@ class RecipesPagingAdapter private constructor(
logger.v { "onCreateViewHolder() called with: parent = $parent, viewType = $viewType" }
val inflater = LayoutInflater.from(parent.context)
val binding = ViewHolderRecipeBinding.inflate(inflater, parent, false)
return recipeViewHolderFactory.build(recipeImageLoader, binding, clickListener)
return recipeViewHolderFactory.build(binding, clickListener)
}
private object RecipeDiffCallback : DiffUtil.ItemCallback<RecipeSummaryEntity>() {
override fun areItemsTheSame(
oldItem: RecipeSummaryEntity,
newItem: RecipeSummaryEntity
oldItem: RecipeSummaryEntity, newItem: RecipeSummaryEntity
): Boolean = oldItem.remoteId == newItem.remoteId
override fun areContentsTheSame(
oldItem: RecipeSummaryEntity,
newItem: RecipeSummaryEntity
oldItem: RecipeSummaryEntity, newItem: RecipeSummaryEntity
): Boolean = oldItem.name == newItem.name && oldItem.slug == newItem.slug
}
}

View File

@@ -42,4 +42,8 @@ interface MealieDataSourceV0 {
): String
suspend fun requestUserInfo(): GetUserInfoResponseV0
suspend fun removeFavoriteRecipe(userId: Int, recipeSlug: String)
suspend fun addFavoriteRecipe(userId: Int, recipeSlug: String)
}

View File

@@ -97,4 +97,22 @@ class MealieDataSourceV0Impl @Inject constructor(
logMethod = { "requestUserInfo" },
)
}
override suspend fun removeFavoriteRecipe(
userId: Int,
recipeSlug: String
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.removeFavoriteRecipe(userId, recipeSlug) },
logMethod = { "removeFavoriteRecipe" },
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
)
override suspend fun addFavoriteRecipe(
userId: Int,
recipeSlug: String
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.addFavoriteRecipe(userId, recipeSlug) },
logMethod = { "addFavoriteRecipe" },
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
)
}

View File

@@ -43,4 +43,16 @@ interface MealieServiceV0 {
@GET("/api/users/self")
suspend fun getUserSelfInfo(): GetUserInfoResponseV0
@DELETE("/api/users/{userId}/favorites/{recipeSlug}")
suspend fun removeFavoriteRecipe(
@Path("userId") userId: Int,
@Path("recipeSlug") recipeSlug: String
)
@POST("/api/users/{userId}/favorites/{recipeSlug}")
suspend fun addFavoriteRecipe(
@Path("userId") userId: Int,
@Path("recipeSlug") recipeSlug: String
)
}

View File

@@ -5,5 +5,6 @@ import kotlinx.serialization.Serializable
@Serializable
data class GetUserInfoResponseV0(
@SerialName("id") val id: Int,
@SerialName("favoriteRecipes") val favoriteRecipes: List<String> = emptyList(),
)

View File

@@ -50,4 +50,8 @@ interface MealieDataSourceV1 {
): CreateApiTokenResponseV1
suspend fun requestUserInfo(): GetUserInfoResponseV1
suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String)
suspend fun addFavoriteRecipe(userId: String, recipeSlug: String)
}

View File

@@ -108,5 +108,23 @@ class MealieDataSourceV1Impl @Inject constructor(
logMethod = { "requestUserInfo" },
)
}
override suspend fun removeFavoriteRecipe(
userId: String,
recipeSlug: String
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.removeFavoriteRecipe(userId, recipeSlug) },
logMethod = { "removeFavoriteRecipe" },
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
)
override suspend fun addFavoriteRecipe(
userId: String,
recipeSlug: String
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.addFavoriteRecipe(userId, recipeSlug) },
logMethod = { "addFavoriteRecipe" },
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
)
}

View File

@@ -49,4 +49,16 @@ interface MealieServiceV1 {
@GET("/api/users/self")
suspend fun getUserSelfInfo(): GetUserInfoResponseV1
@DELETE("/api/users/{userId}/favorites/{recipeSlug}")
suspend fun removeFavoriteRecipe(
@Path("userId") userId: String,
@Path("recipeSlug") recipeSlug: String
)
@POST("/api/users/{userId}/favorites/{recipeSlug}")
suspend fun addFavoriteRecipe(
@Path("userId") userId: String,
@Path("recipeSlug") recipeSlug: String
)
}

View File

@@ -5,5 +5,6 @@ import kotlinx.serialization.Serializable
@Serializable
data class GetUserInfoResponseV1(
@SerialName("id") val id: String,
@SerialName("favoriteRecipes") val favoriteRecipes: List<String> = emptyList(),
)