From fbd41ed54f3310e0d7f9a9911743d1a3736d912b Mon Sep 17 00:00:00 2001 From: Ekaterina Leonidova Date: Sat, 25 Jan 2025 17:08:35 +0300 Subject: [PATCH] Homework 13 --- .../ru/otus/cookbook/ui/CookbookAdapter.kt | 69 +++++++++++++++++++ .../ru/otus/cookbook/ui/CookbookFragment.kt | 17 ++++- .../otus/cookbook/ui/DeleteDialogFragment.kt | 44 ++++++++++++ .../kotlin/ru/otus/cookbook/ui/Listener.kt | 5 ++ .../ru/otus/cookbook/ui/RecipeFragment.kt | 50 +++++++++++++- .../ru/otus/cookbook/ui/RecipeViewHolder.kt | 35 ++++++++++ app/src/main/res/drawable/arrow_back_24px.xml | 11 +++ app/src/main/res/drawable/delete_24px.xml | 10 +++ app/src/main/res/layout/activity_main.xml | 21 +++--- app/src/main/res/layout/fragment_cookbook.xml | 15 +++- .../res/layout/fragment_delete_dialog.xml | 45 ++++++++++++ app/src/main/res/layout/fragment_recipe.xml | 63 ++++++++++++++++- .../main/res/layout/vh_recipe_category.xml | 17 ++++- app/src/main/res/layout/vh_recipe_item.xml | 38 +++++++++- app/src/main/res/menu/recipe_menu.xml | 9 +++ app/src/main/res/navigation/navigation.xml | 40 +++++++++++ app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/strings.xml | 5 ++ gradle/libs.versions.toml | 2 +- 19 files changed, 473 insertions(+), 26 deletions(-) create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/CookbookAdapter.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/DeleteDialogFragment.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/Listener.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/RecipeViewHolder.kt create mode 100644 app/src/main/res/drawable/arrow_back_24px.xml create mode 100644 app/src/main/res/drawable/delete_24px.xml create mode 100644 app/src/main/res/layout/fragment_delete_dialog.xml create mode 100644 app/src/main/res/menu/recipe_menu.xml create mode 100644 app/src/main/res/navigation/navigation.xml diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookAdapter.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookAdapter.kt new file mode 100644 index 0000000..f18b895 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookAdapter.kt @@ -0,0 +1,69 @@ +package ru.otus.cookbook.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import ru.otus.cookbook.R +import ru.otus.cookbook.data.RecipeListItem +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter + + +class CookbookAdapter( + private val listener: Listener +) : ListAdapter(DiffUtilItem()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + RecipeListItem.RecipeItem.layoutId -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.vh_recipe_item, parent, false) + RecipeViewHolder(view, listener) + } + + RecipeListItem.CategoryItem.layoutId -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.vh_recipe_category, parent, false) + CategoryViewHolder(view) + } + + else -> throw IllegalArgumentException("Not found view type for chat adapter") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is RecipeViewHolder -> holder.bind(getItem(position) as RecipeListItem.RecipeItem) + is CategoryViewHolder -> holder.bind(getItem(position) as RecipeListItem.CategoryItem) + } + } + + override fun getItemViewType(position: Int): Int { + return when (currentList[position]) { + is RecipeListItem.RecipeItem -> RecipeListItem.RecipeItem.layoutId + is RecipeListItem.CategoryItem -> RecipeListItem.CategoryItem.layoutId + else -> -1 + } + } +} + +class DiffUtilItem : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: RecipeListItem, newItem: RecipeListItem): Boolean { + if (oldItem::class != newItem::class) return false + + return when { + oldItem is RecipeListItem.CategoryItem && newItem is RecipeListItem.CategoryItem -> oldItem.name == newItem.name + oldItem is RecipeListItem.RecipeItem && newItem is RecipeListItem.RecipeItem -> oldItem.id == newItem.id + else -> false + } + } + + override fun areContentsTheSame(oldItem: RecipeListItem, newItem: RecipeListItem): Boolean { + return when { + oldItem is RecipeListItem.CategoryItem && newItem is RecipeListItem.CategoryItem -> oldItem.name == newItem.name + oldItem is RecipeListItem.RecipeItem && newItem is RecipeListItem.RecipeItem -> oldItem.title == newItem.title && oldItem.description == newItem.description && oldItem.imageUrl == newItem.imageUrl + else -> false + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt index efe6939..25afc2b 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt @@ -1,6 +1,7 @@ package ru.otus.cookbook.ui import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -8,14 +9,17 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController import kotlinx.coroutines.launch +import ru.otus.cookbook.R import ru.otus.cookbook.data.RecipeListItem import ru.otus.cookbook.databinding.FragmentCookbookBinding -class CookbookFragment : Fragment() { +class CookbookFragment : Fragment(), Listener { private val binding = FragmentBindingDelegate(this) private val model: CookbookFragmentViewModel by viewModels { CookbookFragmentViewModel.Factory } + private val adapter: CookbookAdapter by lazy { CookbookAdapter(this) } override fun onCreateView( inflater: LayoutInflater, @@ -37,10 +41,17 @@ class CookbookFragment : Fragment() { } private fun setupRecyclerView() = binding.withBinding { - // Setup RecyclerView + cookbook.adapter = adapter + } private fun onRecipeListUpdated(recipeList: List) { - // Handle recipe list + adapter.submitList(recipeList) + } + + override fun onItemClicked(id: Int) { + Log.d("onItemClicked", id.toString()) + val action = CookbookFragmentDirections.actionCookbookFragmentToRecipeFragment(id) + findNavController().navigate(action) } } \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteDialogFragment.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteDialogFragment.kt new file mode 100644 index 0000000..da228dd --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteDialogFragment.kt @@ -0,0 +1,44 @@ +package ru.otus.cookbook.ui + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.DialogFragment +import androidx.navigation.fragment.findNavController +import ru.otus.cookbook.R + + +class DeleteDialogFragment : DialogFragment() { + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_delete_dialog, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val dialogText = view.findViewById(R.id.dialogText) + + dialogText.text = getString( + R.string.are_you_sure_you_wand_to_delete, + DeleteDialogFragmentArgs.fromBundle(requireArguments()).title + ) + view.findViewById(R.id.ok).setOnClickListener { + val navController = findNavController() + navController.previousBackStackEntry?.savedStateHandle?.set(CONFIRMATION_RESULT, true) + navController.popBackStack() + } + view.findViewById(R.id.cancel).setOnClickListener { + findNavController().popBackStack() + } + } + + companion object { + const val CONFIRMATION_RESULT = "result" + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/Listener.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/Listener.kt new file mode 100644 index 0000000..5b66e0b --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/Listener.kt @@ -0,0 +1,5 @@ +package ru.otus.cookbook.ui + +interface Listener { + fun onItemClicked(id: Int) +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt index e4460c1..f111f00 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt @@ -6,16 +6,22 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.MutableCreationExtras +import androidx.navigation.fragment.findNavController import kotlinx.coroutines.launch +import ru.otus.cookbook.R import ru.otus.cookbook.data.Recipe import ru.otus.cookbook.databinding.FragmentRecipeBinding class RecipeFragment : Fragment() { - private val recipeId: Int get() = TODO("Use Safe Args to get the recipe ID: https://developer.android.com/guide/navigation/use-graph/pass-data#Safe-args") + private val recipeId: Int get() = RecipeFragmentArgs.fromBundle(requireArguments()).recipeId private val binding = FragmentBindingDelegate(this) private val model: RecipeFragmentViewModel by viewModels( @@ -43,6 +49,42 @@ class RecipeFragment : Fragment() { .flowWithLifecycle(viewLifecycleOwner.lifecycle) .collect(::displayRecipe) } + binding.withBinding { + recipeMenuBar.setNavigationOnClickListener { + findNavController().popBackStack() + } + recipeMenuBar.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.delete -> { + val action = RecipeFragmentDirections.actionRecipeFragmentToDeleteDialogFragment(getTitle()) + findNavController().navigate(action) + true + } + else -> false + } + } + } + + val navBackStackEntry = findNavController().getBackStackEntry(R.id.recipeFragment) + + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME + && navBackStackEntry.savedStateHandle.contains(DeleteDialogFragment.CONFIRMATION_RESULT)) { + val isConfirmed = navBackStackEntry.savedStateHandle.get(DeleteDialogFragment.CONFIRMATION_RESULT) ?: false; + if (isConfirmed) { + deleteRecipe() + findNavController().popBackStack() + } + } + } + + navBackStackEntry.lifecycle.addObserver(observer) + + viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_DESTROY) { + navBackStackEntry.lifecycle.removeObserver(observer) + } + }) } /** @@ -52,8 +94,10 @@ class RecipeFragment : Fragment() { return model.recipe.value.title } - private fun displayRecipe(recipe: Recipe) { - // Display the recipe + private fun displayRecipe(recipe: Recipe) = binding.withBinding { + title.text = recipe.title + desc.text = recipe.description + steps.text = recipe.steps.joinToString("\n") } private fun deleteRecipe() { diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeViewHolder.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeViewHolder.kt new file mode 100644 index 0000000..af75481 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeViewHolder.kt @@ -0,0 +1,35 @@ +package ru.otus.cookbook.ui + +import android.view.View +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.RecyclerView +import ru.otus.cookbook.R +import ru.otus.cookbook.data.RecipeListItem + +class RecipeViewHolder ( + private val view: View, + private val listener: Listener, +) : RecyclerView.ViewHolder(view) { + private val root: ConstraintLayout by lazy { view.findViewById(R.id.recipeItem) } + private val letter: TextView by lazy { view.findViewById(R.id.letter) } + private val title: TextView by lazy { view.findViewById(R.id.recipeTitle) } + private val desc: TextView by lazy { view.findViewById(R.id.recipeDesc) } + + fun bind(item: RecipeListItem.RecipeItem) { + letter.text = item.title.substring(0, 1) + title.text = item.title + desc.text = item.description + root.setOnClickListener { listener.onItemClicked(item.id) } + } +} + +class CategoryViewHolder ( + private val view: View, +) : RecyclerView.ViewHolder(view) { + private val title: TextView by lazy { view.findViewById(R.id.categoryTitle) } + + fun bind(item: RecipeListItem.CategoryItem) { + title.text = item.name + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow_back_24px.xml b/app/src/main/res/drawable/arrow_back_24px.xml new file mode 100644 index 0000000..0e2e863 --- /dev/null +++ b/app/src/main/res/drawable/arrow_back_24px.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/delete_24px.xml b/app/src/main/res/drawable/delete_24px.xml new file mode 100644 index 0000000..d724c2e --- /dev/null +++ b/app/src/main/res/drawable/delete_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 86a5d97..c4292fa 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,18 @@ - - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_cookbook.xml b/app/src/main/res/layout/fragment_cookbook.xml index 77d9ef6..a53d8f5 100644 --- a/app/src/main/res/layout/fragment_cookbook.xml +++ b/app/src/main/res/layout/fragment_cookbook.xml @@ -1,6 +1,17 @@ - - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_delete_dialog.xml b/app/src/main/res/layout/fragment_delete_dialog.xml new file mode 100644 index 0000000..c94957b --- /dev/null +++ b/app/src/main/res/layout/fragment_delete_dialog.xml @@ -0,0 +1,45 @@ + + + + + + + +