From 7aeaa791490ec3dc4999cc5e8201a4a45550636a Mon Sep 17 00:00:00 2001 From: Vp-Ma Date: Sun, 25 Jan 2026 13:29:56 +0300 Subject: [PATCH] Homework 12 ( Navigation component ) implementation. --- app/build.gradle.kts | 3 + app/src/main/AndroidManifest.xml | 2 + .../ru/otus/cookbook/ui/CookbookFragment.kt | 20 +++-- .../ru/otus/cookbook/ui/DeleteRecipeDialog.kt | 43 +++++++++++ .../cookbook/ui/RecipeCategoryViewHolder.kt | 7 ++ .../ru/otus/cookbook/ui/RecipeDiffUtil.kt | 26 +++++++ .../ru/otus/cookbook/ui/RecipeFragment.kt | 57 +++++++++++++- .../otus/cookbook/ui/RecipeItemViewHolder.kt | 7 ++ .../ru/otus/cookbook/ui/RecipesAdapter.kt | 65 ++++++++++++++++ app/src/main/res/drawable/arrow_back.xml | 9 +++ app/src/main/res/drawable/exit_icon.xml | 9 +++ app/src/main/res/drawable/icon_background.xml | 9 +++ app/src/main/res/drawable/item_background.xml | 25 ++++++ .../main/res/drawable/shape_image_view.xml | 6 ++ .../main/res/drawable/shape_recipe_image.xml | 5 ++ app/src/main/res/drawable/trash_icon.xml | 9 +++ app/src/main/res/layout/activity_main.xml | 22 +++--- app/src/main/res/layout/fragment_cookbook.xml | 27 ++++++- app/src/main/res/layout/fragment_recipe.xml | 76 ++++++++++++++++++- .../main/res/layout/vh_recipe_category.xml | 26 ++++++- app/src/main/res/layout/vh_recipe_item.xml | 75 +++++++++++++++++- app/src/main/res/navigation/nav_graph.xml | 41 ++++++++++ app/src/main/res/values/colors.xml | 7 ++ app/src/main/res/values/strings.xml | 7 ++ app/src/main/res/values/themes.xml | 7 ++ gradle/libs.versions.toml | 6 +- 26 files changed, 564 insertions(+), 32 deletions(-) create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/DeleteRecipeDialog.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/RecipeCategoryViewHolder.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDiffUtil.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/RecipeItemViewHolder.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/RecipesAdapter.kt create mode 100644 app/src/main/res/drawable/arrow_back.xml create mode 100644 app/src/main/res/drawable/exit_icon.xml create mode 100644 app/src/main/res/drawable/icon_background.xml create mode 100644 app/src/main/res/drawable/item_background.xml create mode 100644 app/src/main/res/drawable/shape_image_view.xml create mode 100644 app/src/main/res/drawable/shape_recipe_image.xml create mode 100644 app/src/main/res/drawable/trash_icon.xml create mode 100644 app/src/main/res/navigation/nav_graph.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f3207a6..3def694 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -48,6 +48,9 @@ dependencies { implementation(libs.material) implementation(libs.androidx.activity) implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.recyclerview) + implementation(libs.glide) + annotationProcessor(libs.compiler) testImplementation(libs.junit) testImplementation(libs.kotlin.coroutines.test) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 063f4d1..431aadd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + (this) private val model: CookbookFragmentViewModel by viewModels { CookbookFragmentViewModel.Factory } + private val recipesAdapter = RecipesAdapter { recipeFragmentStart(it) } override fun onCreateView( inflater: LayoutInflater, @@ -28,7 +32,14 @@ class CookbookFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupRecyclerView() + binding.withBinding { + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + (activity as? AppCompatActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(true) + toolbar.setNavigationOnClickListener { + findNavController().navigateUp() + } + categoryRecyclerView.adapter = recipesAdapter + } viewLifecycleOwner.lifecycleScope.launch { model.recipeList .flowWithLifecycle(viewLifecycleOwner.lifecycle) @@ -36,11 +47,10 @@ class CookbookFragment : Fragment() { } } - private fun setupRecyclerView() = binding.withBinding { - // Setup RecyclerView + private fun recipeFragmentStart(recipeId: Int) { + findNavController().navigate(CookbookFragmentDirections.actionCookbookFragmentToRecipeFragment(recipeId)) } - private fun onRecipeListUpdated(recipeList: List) { - // Handle recipe list + recipesAdapter.submitList(recipeList) } } \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteRecipeDialog.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteRecipeDialog.kt new file mode 100644 index 0000000..06f3c47 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteRecipeDialog.kt @@ -0,0 +1,43 @@ +package ru.otus.cookbook.ui + +import android.app.Dialog +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import ru.otus.cookbook.R +import kotlin.getValue + +class DeleteRecipeDialog: DialogFragment() { + + private val recipeArg by navArgs() + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val recipeTitle = recipeArg.recipeTitle + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.Delete)) + .setMessage(getString(R.string.Choice, recipeTitle)) + .setPositiveButton(getString(R.string.Ok)) { _, _ -> + dismiss() + setDialogResult(true) + } + .setNegativeButton(getString(R.string.Cancel)) { _, _ -> + dismiss() + setDialogResult(false) + } + .create() + } + + private fun setDialogResult(result: Boolean) { + val navController = findNavController() + navController.previousBackStackEntry?.savedStateHandle?.set( + DIALOG_RESULT, + result + ) + } + + companion object { + const val DIALOG_RESULT = "dialogRes" + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeCategoryViewHolder.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeCategoryViewHolder.kt new file mode 100644 index 0000000..7ff9269 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeCategoryViewHolder.kt @@ -0,0 +1,7 @@ +package ru.otus.cookbook.ui + +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import ru.otus.cookbook.databinding.VhRecipeCategoryBinding + +class RecipeCategoryViewHolder(val binding: VhRecipeCategoryBinding): ViewHolder(binding.root) { +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDiffUtil.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDiffUtil.kt new file mode 100644 index 0000000..0df2363 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDiffUtil.kt @@ -0,0 +1,26 @@ +package ru.otus.cookbook.ui + +import androidx.recyclerview.widget.DiffUtil +import ru.otus.cookbook.data.RecipeListItem + +class RecipeDiffUtil: DiffUtil.ItemCallback() { + + override fun areItemsTheSame( firstItem: RecipeListItem, secondItem: RecipeListItem ): Boolean { + + return when{ + firstItem is RecipeListItem.CategoryItem && secondItem is RecipeListItem.CategoryItem -> + firstItem.name == secondItem.name + firstItem is RecipeListItem.RecipeItem && secondItem is RecipeListItem.RecipeItem -> + firstItem.id == secondItem.id + else -> false + } + + } + + override fun areContentsTheSame(firsttem: RecipeListItem, secondItem: RecipeListItem): Boolean { + + return firsttem == secondItem + + } + +} 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..f30c576 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt @@ -1,23 +1,31 @@ package ru.otus.cookbook.ui import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.MutableCreationExtras +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.bumptech.glide.Glide import kotlinx.coroutines.launch 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 recipeArgs by navArgs() + private val recipeId: Int get() = recipeArgs.recipeId private val binding = FragmentBindingDelegate(this) + private lateinit var navController: NavController private val model: RecipeFragmentViewModel by viewModels( extrasProducer = { MutableCreationExtras(defaultViewModelCreationExtras).apply { @@ -38,6 +46,37 @@ class RecipeFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + navController = findNavController() + + binding.withBinding { + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + (activity as? AppCompatActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + binding.withBinding { + (activity as? AppCompatActivity)?.supportActionBar?.title = getTitle() + toolbar.setNavigationOnClickListener { + navController.navigateUp() + } + refuseBin.setOnClickListener { + navController.navigate( + RecipeFragmentDirections.actionRecipeFragmentToDeleteRecipeDialogFragment( + getTitle() + ) + ) + } + } + navController.currentBackStackEntry + ?.savedStateHandle + ?.getLiveData(DeleteRecipeDialog.DIALOG_RESULT) + ?.observe(viewLifecycleOwner) { result -> + if (result) { + deleteRecipe() + Handler(Looper.getMainLooper()).post { + navController.navigateUp() + } + } + } viewLifecycleOwner.lifecycleScope.launch { model.recipe .flowWithLifecycle(viewLifecycleOwner.lifecycle) @@ -53,9 +92,19 @@ class RecipeFragment : Fragment() { } private fun displayRecipe(recipe: Recipe) { - // Display the recipe - } + binding.withBinding { + recipeTitle.text = recipe.title + recipeDescription.text = recipe.description + recipeDetail.text = recipe.steps.mapIndexed { index, step -> "${index + 1}. $step"} + .joinToString("\n") + Glide.with(root) + .load(recipe.imageUrl) + .centerCrop() + .into(recipeImage) + } + + } private fun deleteRecipe() { model.delete() } diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeItemViewHolder.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeItemViewHolder.kt new file mode 100644 index 0000000..3b71edf --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeItemViewHolder.kt @@ -0,0 +1,7 @@ +package ru.otus.cookbook.ui + +import androidx.recyclerview.widget.RecyclerView +import ru.otus.cookbook.databinding.VhRecipeItemBinding + +class RecipeItemViewHolder(val binding: VhRecipeItemBinding): RecyclerView.ViewHolder(binding.root) { +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipesAdapter.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipesAdapter.kt new file mode 100644 index 0000000..3c13c2e --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipesAdapter.kt @@ -0,0 +1,65 @@ +package ru.otus.cookbook.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import ru.otus.cookbook.data.RecipeListItem +import ru.otus.cookbook.databinding.VhRecipeCategoryBinding +import ru.otus.cookbook.databinding.VhRecipeItemBinding + +class RecipesAdapter(val onRecipeClick: (Int) -> Unit):ListAdapter(RecipeDiffUtil()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + + return when(viewType){ + VIEW_CATEGORY -> { + val binding = VhRecipeCategoryBinding.inflate(inflater, parent, false) + RecipeCategoryViewHolder(binding) + } + VIEW_RECIPE -> { + val binding = VhRecipeItemBinding.inflate(inflater, parent, false) + RecipeItemViewHolder(binding) + } + else -> throw IllegalArgumentException("View type is not supported") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val listItem = getItem(position) + when(holder){ + is RecipeCategoryViewHolder -> { + val category = listItem as RecipeListItem.CategoryItem + holder.binding.categoryTextView.text = category.category.name + } + is RecipeItemViewHolder -> { + val recipeItem = listItem as RecipeListItem.RecipeItem + with(holder){ + binding.textBackground.text = recipeItem.title.first().toString() + binding.itemTitle.text = recipeItem.title + binding.description.text = recipeItem.description + Glide.with(binding.root.context) + .load(recipeItem.imageUrl) + .centerCrop() + .into(binding.imageViewRecipe) + binding.root.setOnClickListener { onRecipeClick( listItem.id ) } + } + } + } + } + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is RecipeListItem.CategoryItem -> VIEW_CATEGORY + is RecipeListItem.RecipeItem -> VIEW_RECIPE + } + } + + companion object{ + private const val VIEW_CATEGORY = 1 + private const val VIEW_RECIPE = 2 + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow_back.xml b/app/src/main/res/drawable/arrow_back.xml new file mode 100644 index 0000000..9241934 --- /dev/null +++ b/app/src/main/res/drawable/arrow_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/exit_icon.xml b/app/src/main/res/drawable/exit_icon.xml new file mode 100644 index 0000000..84f6e03 --- /dev/null +++ b/app/src/main/res/drawable/exit_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/icon_background.xml b/app/src/main/res/drawable/icon_background.xml new file mode 100644 index 0000000..dbe7fd6 --- /dev/null +++ b/app/src/main/res/drawable/icon_background.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/item_background.xml b/app/src/main/res/drawable/item_background.xml new file mode 100644 index 0000000..e391d08 --- /dev/null +++ b/app/src/main/res/drawable/item_background.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_image_view.xml b/app/src/main/res/drawable/shape_image_view.xml new file mode 100644 index 0000000..57581db --- /dev/null +++ b/app/src/main/res/drawable/shape_image_view.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_recipe_image.xml b/app/src/main/res/drawable/shape_recipe_image.xml new file mode 100644 index 0000000..a01f3a8 --- /dev/null +++ b/app/src/main/res/drawable/shape_recipe_image.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/trash_icon.xml b/app/src/main/res/drawable/trash_icon.xml new file mode 100644 index 0000000..a74d7a1 --- /dev/null +++ b/app/src/main/res/drawable/trash_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 86a5d97..e0f0333 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,17 @@ - - - - - \ 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..fbd562a 100644 --- a/app/src/main/res/layout/fragment_cookbook.xml +++ b/app/src/main/res/layout/fragment_cookbook.xml @@ -1,6 +1,27 @@ - + android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools" + android:orientation="vertical" + xmlns:app="http://schemas.android.com/apk/res-auto"> - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_recipe.xml b/app/src/main/res/layout/fragment_recipe.xml index 77d9ef6..1c4f87f 100644 --- a/app/src/main/res/layout/fragment_recipe.xml +++ b/app/src/main/res/layout/fragment_recipe.xml @@ -1,6 +1,76 @@ - + android:layout_height="wrap_content" + android:fillViewport="true" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> - \ No newline at end of file + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/vh_recipe_category.xml b/app/src/main/res/layout/vh_recipe_category.xml index 006fd49..8e6a4ec 100644 --- a/app/src/main/res/layout/vh_recipe_category.xml +++ b/app/src/main/res/layout/vh_recipe_category.xml @@ -1,6 +1,28 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/vh_recipe_item.xml b/app/src/main/res/layout/vh_recipe_item.xml index 006fd49..f599a1d 100644 --- a/app/src/main/res/layout/vh_recipe_item.xml +++ b/app/src/main/res/layout/vh_recipe_item.xml @@ -1,6 +1,77 @@ + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:cardCornerRadius="10dp" + android:background="@drawable/item_background" + android:layout_marginTop="12dp" + android:layout_marginHorizontal="16dp" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 0000000..2a16477 --- /dev/null +++ b/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c8524cd..4b988ba 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,4 +2,11 @@ #FF000000 #FFFFFFFF + #65558F + #EADDFF + #4F378A + #CAC4D0 + #625B71 + #7D5260 + #EFDAE5 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8fce1d1..01d0c8c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,10 @@ Cookbook + + Hello blank fragment + + Delete + Do you want to delete recipe %s? + ОК + Cancel \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 47d6575..427c7eb 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -5,5 +5,12 @@ + +