From 451c2e9f2c35f51df7d09416f045b51375c4d06f Mon Sep 17 00:00:00 2001 From: Adel Khaziakhmetov Date: Wed, 7 May 2025 13:41:31 +0300 Subject: [PATCH 1/3] homework --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 2 +- .../ru/otus/cookbook/CategoryViewHolder.kt | 15 +++++ .../ru/otus/cookbook/CookbookAdapter.kt | 56 +++++++++++++++++++ .../main/kotlin/ru/otus/cookbook/Listener.kt | 5 ++ .../kotlin/ru/otus/cookbook/MainActivity.kt | 8 +++ .../ru/otus/cookbook/RecipeViewHolder.kt | 23 ++++++++ .../ru/otus/cookbook/ui/CookbookFragment.kt | 23 +++++++- .../cookbook/ui/RecipeDeleteDialogFragment.kt | 33 +++++++++++ .../ru/otus/cookbook/ui/RecipeFragment.kt | 33 +++++++++-- app/src/main/res/anim/slide_in_left.xml | 7 +++ app/src/main/res/anim/slide_in_right.xml | 7 +++ app/src/main/res/anim/slide_out_left.xml | 7 +++ app/src/main/res/anim/slide_out_right.xml | 7 +++ app/src/main/res/drawable/divider.xml | 6 ++ app/src/main/res/layout/activity_main.xml | 22 ++------ app/src/main/res/layout/fragment_cookbook.xml | 10 +++- app/src/main/res/layout/fragment_recipe.xml | 53 ++++++++++++++++++ .../main/res/layout/vh_recipe_category.xml | 24 +++++++- app/src/main/res/layout/vh_recipe_item.xml | 33 ++++++++++- app/src/main/res/navigation/nav_graph.xml | 51 +++++++++++++++++ app/src/main/res/values/strings.xml | 7 +++ gradle/libs.versions.toml | 2 + 23 files changed, 405 insertions(+), 30 deletions(-) create mode 100644 app/src/main/kotlin/ru/otus/cookbook/CategoryViewHolder.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/CookbookAdapter.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/Listener.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/RecipeViewHolder.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDeleteDialogFragment.kt create mode 100644 app/src/main/res/anim/slide_in_left.xml create mode 100644 app/src/main/res/anim/slide_in_right.xml create mode 100644 app/src/main/res/anim/slide_out_left.xml create mode 100644 app/src/main/res/anim/slide_out_right.xml create mode 100644 app/src/main/res/drawable/divider.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..f8dc461 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(libs.material) implementation(libs.androidx.activity) implementation(libs.androidx.constraintlayout) + implementation(libs.coil) 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..6e79293 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ - + () { + private var list = listOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + ViewTypes.CATEGORY.id -> { + val view = LayoutInflater.from(parent.context).inflate(R.layout.vh_recipe_category, parent, false) + CategoryViewHolder(view) + } + ViewTypes.RECIPE.id -> { + val view = LayoutInflater.from(parent.context).inflate(R.layout.vh_recipe_item, parent, false) + RecipeViewHolder(view, listener) + } + else -> throw IllegalArgumentException("Not found view type for chat adapter") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = list.getOrNull(position) ?: return + when (item) { + is RecipeListItem.CategoryItem -> { + (holder as? CategoryViewHolder)?.bind(item) + } + is RecipeListItem.RecipeItem -> { + (holder as? RecipeViewHolder)?.bind(item) + } + } + } + + override fun getItemCount(): Int = list.size + + override fun getItemViewType(position: Int): Int { + return when (list[position]) { + is RecipeListItem.CategoryItem -> ViewTypes.CATEGORY.id + is RecipeListItem.RecipeItem -> ViewTypes.RECIPE.id + else -> -1 + } + } + + fun setItems(items: List) { + list = items + notifyDataSetChanged() + } + + enum class ViewTypes(val id: Int) { + CATEGORY(R.layout.vh_recipe_category), + RECIPE(R.layout.vh_recipe_item) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/Listener.kt b/app/src/main/kotlin/ru/otus/cookbook/Listener.kt new file mode 100644 index 0000000..74c1cf9 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/Listener.kt @@ -0,0 +1,5 @@ +package ru.otus.cookbook + +interface Listener { + fun onItemClicked(id: Int) +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/MainActivity.kt b/app/src/main/kotlin/ru/otus/cookbook/MainActivity.kt index 6e524b6..099533a 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/MainActivity.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/MainActivity.kt @@ -1,7 +1,9 @@ package ru.otus.cookbook import android.os.Bundle +import androidx.activity.addCallback import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.findNavController import ru.otus.cookbook.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { @@ -12,5 +14,11 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + + onBackPressedDispatcher.addCallback(this) { + if (!findNavController(R.id.fragment_container_view).popBackStack()) { + finish() + } + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/RecipeViewHolder.kt b/app/src/main/kotlin/ru/otus/cookbook/RecipeViewHolder.kt new file mode 100644 index 0000000..d6bd619 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/RecipeViewHolder.kt @@ -0,0 +1,23 @@ +package ru.otus.cookbook + +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import ru.otus.cookbook.data.RecipeListItem + +class RecipeViewHolder( + private val view: View, + private val listener: Listener): RecyclerView.ViewHolder(view) { + + private val twName: TextView by lazy { view.findViewById(R.id.tw_name) } + private val twDescription: TextView by lazy { view.findViewById(R.id.tw_description) } + + fun bind(item: RecipeListItem.RecipeItem) { + twName.text = item.title + twDescription.text = item.description + + view.setOnClickListener { + listener.onItemClicked(item.id) + } + } +} \ 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..7311301 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt @@ -8,14 +8,23 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.coroutines.launch +import ru.otus.cookbook.CategoryViewHolder +import ru.otus.cookbook.CookbookAdapter +import ru.otus.cookbook.Listener +import ru.otus.cookbook.RecipeViewHolder 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 cookbookAdapter = CookbookAdapter(this@CookbookFragment) + override fun onCreateView( inflater: LayoutInflater, @@ -37,10 +46,18 @@ class CookbookFragment : Fragment() { } private fun setupRecyclerView() = binding.withBinding { - // Setup RecyclerView + val dividerItemDecoration = DividerItemDecoration(context, LinearLayoutManager.VERTICAL) + recyclerView.addItemDecoration(dividerItemDecoration) + recyclerView.adapter = cookbookAdapter } private fun onRecipeListUpdated(recipeList: List) { - // Handle recipe list + cookbookAdapter.setItems(recipeList) + } + + override fun onItemClicked(id: Int) { + findNavController().navigate(CookbookFragmentDirections.actionCookbookToRecipe( + recipeID = id + )) } } \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDeleteDialogFragment.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDeleteDialogFragment.kt new file mode 100644 index 0000000..257ee85 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDeleteDialogFragment.kt @@ -0,0 +1,33 @@ +package ru.otus.cookbook.ui + +import android.app.Dialog +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.navigation.fragment.findNavController +import ru.otus.cookbook.R + +class RecipeDeleteDialogFragment : DialogFragment() { + private val title get() = RecipeDeleteDialogFragmentArgs.fromBundle(requireArguments()).title + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = + requireContext().let { + AlertDialog.Builder(it) + .setTitle(title) + .setMessage(R.string.rdf_message) + .setPositiveButton(R.string.rdf_ok) { dialog, button -> + findNavController().previousBackStackEntry?.savedStateHandle?.set(RESULT, 1) + } + .setNegativeButton(R.string.rdf_no) { dialog, button -> + findNavController().previousBackStackEntry?.savedStateHandle?.set(RESULT, 0) + } + .setNeutralButton(R.string.rdf_cancel) { dialog, button -> + dialog.dismiss() + } + .create() + } + + companion object { + const val RESULT = "result" + } +} \ 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..20f2f7d 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt @@ -9,13 +9,15 @@ import androidx.fragment.app.viewModels 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.data.Recipe import ru.otus.cookbook.databinding.FragmentRecipeBinding +import coil.load +import ru.otus.cookbook.R 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( @@ -26,6 +28,7 @@ class RecipeFragment : Fragment() { }, factoryProducer = { RecipeFragmentViewModel.Factory } ) + private val navigationController by lazy { findNavController() } override fun onCreateView( inflater: LayoutInflater, @@ -43,20 +46,38 @@ class RecipeFragment : Fragment() { .flowWithLifecycle(viewLifecycleOwner.lifecycle) .collect(::displayRecipe) } + + displayRecipe(model.recipe.value) + + navigationController.currentBackStackEntry?.savedStateHandle?.getLiveData( + RecipeDeleteDialogFragment.RESULT) + ?.observe(viewLifecycleOwner) { + if (it == 1) { + deleteRecipe() + } + } } - /** - * Use to get recipe title and pass to confirmation dialog - */ private fun getTitle(): String { return model.recipe.value.title } private fun displayRecipe(recipe: Recipe) { - // Display the recipe + binding.withBinding { + twName.text = recipe.title + twDescription.text = recipe.description + imageView.load(recipe.imageUrl) { + crossfade(true) + } + button.setOnClickListener { + navigationController.navigate(RecipeFragmentDirections.actionRecipeToDialog( + getTitle())) + } + } } private fun deleteRecipe() { model.delete() + findNavController().popBackStack(R.id.fragment_cookbook, false) } } \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_left.xml b/app/src/main/res/anim/slide_in_left.xml new file mode 100644 index 0000000..2640a8d --- /dev/null +++ b/app/src/main/res/anim/slide_in_left.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..e2022e6 --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..a5c9daa --- /dev/null +++ b/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml new file mode 100644 index 0000000..5f0e616 --- /dev/null +++ b/app/src/main/res/anim/slide_out_right.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/divider.xml b/app/src/main/res/drawable/divider.xml new file mode 100644 index 0000000..44e6f26 --- /dev/null +++ b/app/src/main/res/drawable/divider.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 86a5d97..e7a74fa 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,9 @@ - - - - - \ No newline at end of file + xmlns:app="http://schemas.android.com/apk/res-auto" + app:navGraph="@navigation/nav_graph" + /> \ 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..2a80697 100644 --- a/app/src/main/res/layout/fragment_cookbook.xml +++ b/app/src/main/res/layout/fragment_cookbook.xml @@ -1,6 +1,14 @@ + android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + \ 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..ab821a0 100644 --- a/app/src/main/res/layout/fragment_recipe.xml +++ b/app/src/main/res/layout/fragment_recipe.xml @@ -1,6 +1,59 @@ +