Merge pull request #25 from kirmanak/recipe-info-ui

Improve recipe info UI
This commit is contained in:
Kirill Kamakin
2021-12-27 19:48:12 +03:00
committed by GitHub
8 changed files with 204 additions and 114 deletions

View File

@@ -5,7 +5,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import by.kirich1409.viewbindingdelegate.viewBinding import by.kirich1409.viewbindingdelegate.viewBinding
@@ -15,50 +15,62 @@ import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.FragmentRecipeInfoBinding import gq.kirmanak.mealient.databinding.FragmentRecipeInfoBinding
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class RecipeInfoFragment : BottomSheetDialogFragment() { class RecipeInfoFragment : BottomSheetDialogFragment() {
private val binding by viewBinding(FragmentRecipeInfoBinding::bind) private val binding by viewBinding(FragmentRecipeInfoBinding::bind)
private val arguments by navArgs<RecipeInfoFragmentArgs>() private val arguments by navArgs<RecipeInfoFragmentArgs>()
private val viewModel by viewModels<RecipeInfoViewModel>() private val viewModel by viewModels<RecipeInfoViewModel>()
override fun onCreateView( @Inject
inflater: LayoutInflater, lateinit var ingredientsAdapter: RecipeIngredientsAdapter
container: ViewGroup?,
savedInstanceState: Bundle? @Inject
): View { lateinit var instructionsAdapter: RecipeInstructionsAdapter
Timber.v("onCreateView() called with: inflater = $inflater, container = $container, savedInstanceState = $savedInstanceState")
return FragmentRecipeInfoBinding.inflate(inflater, container, false).root override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
Timber.v("onCreateView() called")
return FragmentRecipeInfoBinding.inflate(inflater, container, false).root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated() called")
binding.ingredientsList.adapter = ingredientsAdapter
binding.instructionsList.adapter = instructionsAdapter
viewModel.loadRecipeImage(binding.image, arguments.recipeSlug)
viewModel.loadRecipeInfo(arguments.recipeId, arguments.recipeSlug)
viewModel.recipeInfo.observe(viewLifecycleOwner) {
Timber.d("onViewCreated: full info $it")
binding.title.text = it.recipeSummaryEntity.name
binding.description.text = it.recipeSummaryEntity.description
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.listsVisibility.observe(viewLifecycleOwner) {
super.onViewCreated(view, savedInstanceState) Timber.d("onViewCreated: lists visibility $it")
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") binding.ingredientsHolder.isVisible = it.areIngredientsVisible
binding.instructionsGroup.isVisible = it.areInstructionsVisible
binding.ingredientsList.adapter = viewModel.recipeIngredientsAdapter
binding.instructionsList.adapter = viewModel.recipeInstructionsAdapter
viewModel.loadRecipeImage(binding.image, arguments.recipeSlug)
viewModel.loadRecipeInfo(arguments.recipeId, arguments.recipeSlug)
viewModel.recipeInfo.observe(viewLifecycleOwner) {
Timber.d("onViewCreated: full info $it")
binding.title.text = it.recipeSummaryEntity.name
binding.description.text = it.recipeSummaryEntity.description
}
(requireActivity() as? AppCompatActivity)?.supportActionBar?.title = null
} }
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
BottomSheetDialog(requireContext(), R.style.NoShapeBottomSheetDialog) BottomSheetDialog(requireContext(), R.style.NoShapeBottomSheetDialog)
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
Timber.v("onDestroyView() called") Timber.v("onDestroyView() called")
// Prevent RV leaking through mObservers list in adapter // Prevent RV leaking through mObservers list in adapter
with(binding) { with(binding) {
ingredientsList.adapter = null ingredientsList.adapter = null
instructionsList.adapter = null instructionsList.adapter = null
}
} }
}
} }

View File

@@ -0,0 +1,6 @@
package gq.kirmanak.mealient.ui.recipes.info
data class RecipeInfoListsVisibility(
val areIngredientsVisible: Boolean = false,
val areInstructionsVisible: Boolean = false,
)

View File

@@ -14,35 +14,44 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class RecipeInfoViewModel @Inject constructor( class RecipeInfoViewModel
@Inject
constructor(
private val recipeRepo: RecipeRepo, private val recipeRepo: RecipeRepo,
private val recipeImageLoader: RecipeImageLoader private val recipeImageLoader: RecipeImageLoader,
private val recipeIngredientsAdapter: RecipeIngredientsAdapter,
private val recipeInstructionsAdapter: RecipeInstructionsAdapter,
) : ViewModel() { ) : ViewModel() {
private val _recipeInfo = MutableLiveData<FullRecipeInfo>() private val _recipeInfo = MutableLiveData<FullRecipeInfo>()
val recipeInfo: LiveData<FullRecipeInfo> = _recipeInfo val recipeInfo: LiveData<FullRecipeInfo> by ::_recipeInfo
val recipeIngredientsAdapter = RecipeIngredientsAdapter() private val _listsVisibility = MutableLiveData(RecipeInfoListsVisibility())
val recipeInstructionsAdapter = RecipeInstructionsAdapter() val listsVisibility: LiveData<RecipeInfoListsVisibility> by ::_listsVisibility
fun loadRecipeImage(view: ImageView, recipeSlug: String) { fun loadRecipeImage(view: ImageView, recipeSlug: String) {
Timber.v("loadRecipeImage() called with: view = $view, recipeSlug = $recipeSlug") Timber.v("loadRecipeImage() called with: view = $view, recipeSlug = $recipeSlug")
viewModelScope.launch { viewModelScope.launch { recipeImageLoader.loadRecipeImage(view, recipeSlug) }
recipeImageLoader.loadRecipeImage(view, recipeSlug)
}
} }
fun loadRecipeInfo(recipeId: Long, recipeSlug: String) { fun loadRecipeInfo(recipeId: Long, recipeSlug: String) {
Timber.v("loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug") Timber.v("loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug")
_listsVisibility.value = RecipeInfoListsVisibility()
recipeIngredientsAdapter.submitList(null)
recipeInstructionsAdapter.submitList(null)
viewModelScope.launch { viewModelScope.launch {
runCatching { runCatching { recipeRepo.loadRecipeInfo(recipeId, recipeSlug) }
recipeRepo.loadRecipeInfo(recipeId, recipeSlug) .onSuccess {
}.onSuccess { Timber.d("loadRecipeInfo: received recipe info = $it")
Timber.d("loadRecipeInfo: received recipe info = $it") _recipeInfo.value = it
_recipeInfo.value = it recipeIngredientsAdapter.submitList(it.recipeIngredients)
recipeIngredientsAdapter.submitList(it.recipeIngredients) recipeInstructionsAdapter.submitList(it.recipeInstructions)
recipeInstructionsAdapter.submitList(it.recipeInstructions) _listsVisibility.value =
}.onFailure { RecipeInfoListsVisibility(
Timber.e(it, "loadRecipeInfo: can't load recipe info") areIngredientsVisible = it.recipeIngredients.isNotEmpty(),
} areInstructionsVisible = it.recipeInstructions.isNotEmpty()
)
}
.onFailure { Timber.e(it, "loadRecipeInfo: can't load recipe info") }
} }
} }
} }

View File

@@ -9,8 +9,11 @@ import gq.kirmanak.mealient.data.recipes.db.entity.RecipeIngredientEntity
import gq.kirmanak.mealient.databinding.ViewHolderIngredientBinding import gq.kirmanak.mealient.databinding.ViewHolderIngredientBinding
import gq.kirmanak.mealient.ui.recipes.info.RecipeIngredientsAdapter.RecipeIngredientViewHolder import gq.kirmanak.mealient.ui.recipes.info.RecipeIngredientsAdapter.RecipeIngredientViewHolder
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
class RecipeIngredientsAdapter : @Singleton
class RecipeIngredientsAdapter @Inject constructor() :
ListAdapter<RecipeIngredientEntity, RecipeIngredientViewHolder>(RecipeIngredientDiffCallback) { ListAdapter<RecipeIngredientEntity, RecipeIngredientViewHolder>(RecipeIngredientDiffCallback) {
class RecipeIngredientViewHolder( class RecipeIngredientViewHolder(

View File

@@ -10,8 +10,11 @@ import gq.kirmanak.mealient.data.recipes.db.entity.RecipeInstructionEntity
import gq.kirmanak.mealient.databinding.ViewHolderInstructionBinding import gq.kirmanak.mealient.databinding.ViewHolderInstructionBinding
import gq.kirmanak.mealient.ui.recipes.info.RecipeInstructionsAdapter.RecipeInstructionViewHolder import gq.kirmanak.mealient.ui.recipes.info.RecipeInstructionsAdapter.RecipeInstructionViewHolder
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
class RecipeInstructionsAdapter : @Singleton
class RecipeInstructionsAdapter @Inject constructor() :
ListAdapter<RecipeInstructionEntity, RecipeInstructionViewHolder>(RecipeInstructionDiffCallback) { ListAdapter<RecipeInstructionEntity, RecipeInstructionViewHolder>(RecipeInstructionDiffCallback) {
private object RecipeInstructionDiffCallback : private object RecipeInstructionDiffCallback :

View File

@@ -10,85 +10,134 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/end_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintGuide_end="4dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/start_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintGuide_begin="4dp"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image" android:id="@+id/image"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:layout_width="0dp" android:layout_width="0dp"
app:shapeAppearance="@style/ShapeAppearance.AllCornersRounded" app:shapeAppearance="@style/ShapeAppearance.AllCornersRounded"
android:layout_height="@dimen/fragment_recipe_info_image_height" android:layout_height="@dimen/fragment_recipe_info_image_height"
android:layout_marginBottom="@dimen/margin_small"
android:contentDescription="@string/content_description_fragment_recipe_info_image" android:contentDescription="@string/content_description_fragment_recipe_info_image"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/title"
tools:srcCompat="@drawable/placeholder_recipe" /> tools:srcCompat="@drawable/placeholder_recipe" />
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small" android:layout_marginHorizontal="8dp"
android:layout_marginTop="7dp"
android:textAppearance="?textAppearanceHeadline6" android:textAppearance="?textAppearanceHeadline6"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toTopOf="@+id/description"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@+id/end_guide"
app:layout_constraintStart_toEndOf="@+id/start_guide"
app:layout_constraintTop_toBottomOf="@+id/image" app:layout_constraintTop_toBottomOf="@+id/image"
tools:text="Best-Ever Beef Stew" /> tools:text="Best-Ever Beef Stew" />
<TextView <TextView
android:id="@+id/description" android:id="@+id/description"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_marginHorizontal="8dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small" android:layout_marginTop="6dp"
android:textAppearance="?textAppearanceBody2" android:textAppearance="?textAppearanceBody2"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toTopOf="@+id/ingredients_holder"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@+id/end_guide"
app:layout_constraintStart_toEndOf="@+id/start_guide"
app:layout_constraintTop_toBottomOf="@+id/title" app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="Stay warm all winter with this classic Beef Stew made with red wine and beef stock from Delish.com." /> tools:text="Stay warm all winter with this classic Beef Stew made with red wine and beef stock from Delish.com." />
<TextView <com.google.android.material.card.MaterialCardView
android:id="@+id/ingredients_header" android:id="@+id/ingredients_holder"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small" android:layout_marginHorizontal="8dp"
android:text="@string/fragment_recipe_info_ingredients_header" android:layout_marginTop="11dp"
android:textAppearance="?textAppearanceHeadline6" android:layout_marginBottom="20dp"
app:layout_constraintEnd_toEndOf="parent" app:cardCornerRadius="@dimen/rounded_corner_size_default"
app:layout_constraintStart_toStartOf="parent" app:cardElevation="10dp"
app:layout_constraintTop_toBottomOf="@+id/description" /> app:layout_constraintBottom_toTopOf="@+id/instructions_header"
app:layout_constraintEnd_toStartOf="@+id/end_guide"
app:layout_constraintStart_toEndOf="@+id/start_guide"
app:layout_constraintTop_toBottomOf="@+id/description">
<androidx.recyclerview.widget.RecyclerView <LinearLayout
android:id="@+id/ingredients_list" android:layout_width="match_parent"
android:layout_width="0dp" android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/ingredients_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="17dp"
android:layout_marginTop="16dp"
android:text="@string/fragment_recipe_info_ingredients_header"
android:textAppearance="?textAppearanceHeadline6" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/ingredients_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="@layout/view_holder_ingredient" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<androidx.constraintlayout.widget.Group
android:id="@+id/instructions_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:constraint_referenced_ids="instructions_header,instructions_list" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ingredients_header"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="@layout/view_holder_ingredient" />
<TextView <TextView
android:id="@+id/instructions_header" android:id="@+id/instructions_header"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small" android:layout_marginStart="35dp"
android:layout_marginEnd="8dp"
android:text="@string/fragment_recipe_info_instructions_header" android:text="@string/fragment_recipe_info_instructions_header"
android:textAppearance="?textAppearanceHeadline6" android:textAppearance="?textAppearanceHeadline6"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toTopOf="@+id/instructions_list"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@+id/end_guide"
app:layout_constraintTop_toBottomOf="@+id/ingredients_list" /> app:layout_constraintStart_toEndOf="@+id/start_guide"
app:layout_constraintTop_toBottomOf="@+id/ingredients_holder" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/instructions_list" android:id="@+id/instructions_list"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@+id/end_guide"
app:layout_constraintStart_toStartOf="parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@+id/instructions_header" app:layout_constraintTop_toBottomOf="@+id/instructions_header"
android:paddingBottom="@dimen/bottom_padding_instructions_list_fragment_recipe_info" app:layout_constraintStart_toEndOf="@+id/start_guide"
tools:itemCount="2" tools:itemCount="2"
tools:listitem="@layout/view_holder_instruction" /> tools:listitem="@layout/view_holder_instruction" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -9,7 +9,6 @@
android:id="@+id/checkBox" android:id="@+id/checkBox"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

View File

@@ -1,30 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_margin="8dp"
app:cardCornerRadius="@dimen/rounded_corner_size_default"
app:cardElevation="10dp"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/step" android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small" android:paddingHorizontal="18dp"
android:textAppearance="?textAppearanceHeadline6" android:paddingVertical="12dp">
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Step: 1" />
<TextView <TextView
android:id="@+id/instruction" android:id="@+id/step"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small" android:textAppearance="?textAppearanceHeadline6"
android:textAppearance="?textAppearanceBody2" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@+id/step" tools:text="Step: 1" />
tools:text="In a large dutch oven or heavy-bottomed pot over medium heat, heat oil. Add beef and cook until seared on all sides, 10 minutes, working in batches if necessary. Transfer beef to a plate." />
</androidx.constraintlayout.widget.ConstraintLayout> <TextView
android:id="@+id/instruction"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textAppearance="?textAppearanceBody2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/step"
tools:text="In a large dutch oven or heavy-bottomed pot over medium heat, heat oil. Add beef and cook until seared on all sides, 10 minutes, working in batches if necessary. Transfer beef to a plate." />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>