From 4b1167d44cfcc4375883637cf12d4b9dd3159285 Mon Sep 17 00:00:00 2001 From: Sergei Korotaev Date: Thu, 8 May 2025 21:43:15 +0300 Subject: [PATCH 1/2] Initial commit. Tasks: 1,2 & 3. --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 2 + .../kotlin/ru/otus/cookbook/MainActivity.kt | 8 ++ .../ru/otus/cookbook/ui/CookbookFragment.kt | 22 ++++- .../cookbook/ui/DeleteRecipeDialogFragment.kt | 45 ++++++++++ .../ru/otus/cookbook/ui/RecipeAdapter.kt | 86 ++++++++++++++++++ .../ru/otus/cookbook/ui/RecipeFragment.kt | 50 +++++++++-- app/src/main/res/drawable/arrow_back_24px.xml | 11 +++ app/src/main/res/drawable/circle.xml | 9 ++ app/src/main/res/drawable/close_24px.xml | 10 +++ app/src/main/res/drawable/delete_24px.xml | 10 +++ app/src/main/res/layout/activity_main.xml | 14 ++- app/src/main/res/layout/fragment_cookbook.xml | 40 ++++++++- app/src/main/res/layout/fragment_recipe.xml | 71 ++++++++++++++- .../main/res/layout/vh_recipe_category.xml | 17 +++- app/src/main/res/layout/vh_recipe_item.xml | 64 +++++++++++++- app/src/main/res/menu/recipe_app_bar.xml | 10 +++ .../main/res/navigation/cookbook_graph.xml | 49 +++++++++++ app/src/main/res/values/strings.xml | 14 +++ app/src/main/res/values/styles.xml | 88 +++++++++++++++++++ gradle/libs.versions.toml | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 22 files changed, 600 insertions(+), 27 deletions(-) create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/DeleteRecipeDialogFragment.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/RecipeAdapter.kt create mode 100644 app/src/main/res/drawable/arrow_back_24px.xml create mode 100644 app/src/main/res/drawable/circle.xml create mode 100644 app/src/main/res/drawable/close_24px.xml create mode 100644 app/src/main/res/drawable/delete_24px.xml create mode 100644 app/src/main/res/menu/recipe_app_bar.xml create mode 100644 app/src/main/res/navigation/cookbook_graph.xml create mode 100644 app/src/main/res/values/styles.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f3207a6..1b3b0dc 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.github.glide) 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..682aa63 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + // Handle recipe click, e.g., navigate to RecipeFragment + val action = CookbookFragmentDirections.actionToRecipeFragment(recipeId) + findNavController().navigate(action) + } + + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + recyclerView.adapter = adapter } - private fun onRecipeListUpdated(recipeList: List) { + private fun onRecipeListUpdated(recipeList: List) = binding.withBinding { // Handle recipe list + (recyclerView.adapter as RecipeAdapter).submitList(recipeList) } } \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteRecipeDialogFragment.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteRecipeDialogFragment.kt new file mode 100644 index 0000000..f3a37b5 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteRecipeDialogFragment.kt @@ -0,0 +1,45 @@ +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 + +class DeleteRecipeDialogFragment : DialogFragment() { + + companion object { + const val CONFIRMATION_RESULT = "confirmation_result" + } + + private val args: DeleteRecipeDialogFragmentArgs by navArgs() + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val recipeTitle = args.recipeTitle + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.delete)) + .setMessage(getString(R.string.delete_recipe_message, recipeTitle)) + .setPositiveButton(R.string.ok) { _, _ -> + dismiss() + setResult(true) + } + .setNegativeButton(R.string.cancel + ) { _, _ -> + dismiss() + setResult(false) + } + .create() + } + + private fun setResult(result: Boolean) { + val navController = findNavController() + navController.previousBackStackEntry?.savedStateHandle?.set( + CONFIRMATION_RESULT, + result + ) + navController.popBackStack() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeAdapter.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeAdapter.kt new file mode 100644 index 0000000..2d25777 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeAdapter.kt @@ -0,0 +1,86 @@ +package ru.otus.cookbook.ui + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import ru.otus.cookbook.R +import ru.otus.cookbook.data.RecipeListItem +import ru.otus.cookbook.databinding.VhRecipeCategoryBinding +import ru.otus.cookbook.databinding.VhRecipeItemBinding + +class RecipeAdapter(private val context: Context, private val onRecipeClick: (Int) -> Unit): + ListAdapter(RecipeDiffCallback()) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + ITEMTYPE_RECIPE-> { + val binding = + VhRecipeItemBinding.inflate(LayoutInflater.from(context), parent, false) + RecipeViewHolder(binding) + } + + ITEMTYPE_CATEGORY-> { + val binding = + VhRecipeCategoryBinding.inflate(LayoutInflater.from(context), parent, false) + CategoryViewHolder(binding) + } + + else -> throw IllegalArgumentException(context.getString(R.string.unknown_view_type)) + } + } + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is RecipeListItem.RecipeItem -> ITEMTYPE_RECIPE + is RecipeListItem.CategoryItem -> ITEMTYPE_CATEGORY + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (val item = getItem(position)) { + is RecipeListItem.RecipeItem -> (holder as RecipeViewHolder).bind(item) + is RecipeListItem.CategoryItem -> (holder as CategoryViewHolder).bind(item) + } + } + + inner class RecipeViewHolder(private val binding: VhRecipeItemBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: RecipeListItem.RecipeItem) { + binding.firstLetter.text = item.title.firstOrNull()?.toString() ?: "" + binding.title.text = item.title + binding.description.text = item.description + Glide.with(binding.root.context) + .load(item.imageUrl) + .centerCrop() + .into(binding.image) + binding.root.setOnClickListener { onRecipeClick(item.id) } + } + } + + inner class CategoryViewHolder(private val binding: VhRecipeCategoryBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: RecipeListItem.CategoryItem) { + binding.categoryName.text = item.name + } + } + + companion object { + const val ITEMTYPE_RECIPE = 1 + const val ITEMTYPE_CATEGORY = 2 + } +} + +class RecipeDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: RecipeListItem, newItem: RecipeListItem): Boolean { + return when { + oldItem is RecipeListItem.RecipeItem && newItem is RecipeListItem.RecipeItem -> oldItem.id == newItem.id + oldItem is RecipeListItem.CategoryItem && newItem is RecipeListItem.CategoryItem -> oldItem.name == newItem.name + else -> false + } + } + + override fun areContentsTheSame(oldItem: RecipeListItem, newItem: RecipeListItem): Boolean { + return oldItem == newItem + } +} \ 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..e36aaf5 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt @@ -9,14 +9,18 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.MutableCreationExtras +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.bumptech.glide.Glide 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 args: RecipeFragmentArgs by navArgs() + private val recipeId: Int get() = args.recipeId private val binding = FragmentBindingDelegate(this) private val model: RecipeFragmentViewModel by viewModels( extrasProducer = { @@ -38,6 +42,19 @@ class RecipeFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + setupAppBar() + + val navController = findNavController() + navController.currentBackStackEntry?.savedStateHandle?.getLiveData( + DeleteRecipeDialogFragment.CONFIRMATION_RESULT + )?.observe(viewLifecycleOwner) { result-> + if (result) { + model.delete() + navController.navigateUp() + } + } + viewLifecycleOwner.lifecycleScope.launch { model.recipe .flowWithLifecycle(viewLifecycleOwner.lifecycle) @@ -52,11 +69,32 @@ class RecipeFragment : Fragment() { return model.recipe.value.title } - private fun displayRecipe(recipe: Recipe) { - // Display the recipe + private fun setupAppBar() { + binding.withBinding { + recipeToolbar.setNavigationOnClickListener { + findNavController().navigate(R.id.action_to_cookbook) + } + recipeToolbar.setOnMenuItemClickListener { menuItem-> + when (menuItem.itemId) { + R.id.delete -> { + val action = RecipeFragmentDirections.actionToDeleteRecipeDialog(getTitle()) + findNavController().navigate(action) + true + } + + else -> false + } + } + } } - private fun deleteRecipe() { - model.delete() + private fun displayRecipe(recipe: Recipe) = binding.withBinding { + // Display the recipe + recipeTitle.text = recipe.title + recipeShortDescription.text = recipe.category.name + recipeFullDescription.text = recipe.description + Glide.with(this@RecipeFragment) + .load(recipe.imageUrl) + .into(recipeImage) } } \ 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/circle.xml b/app/src/main/res/drawable/circle.xml new file mode 100644 index 0000000..e8ab92e --- /dev/null +++ b/app/src/main/res/drawable/circle.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/close_24px.xml b/app/src/main/res/drawable/close_24px.xml new file mode 100644 index 0000000..7a0ff35 --- /dev/null +++ b/app/src/main/res/drawable/close_24px.xml @@ -0,0 +1,10 @@ + + + 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..bbdeab1 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,13 +7,11 @@ android:layout_height="match_parent" tools:context=".MainActivity"> - + \ 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..0c6495d 100644 --- a/app/src/main/res/layout/fragment_cookbook.xml +++ b/app/src/main/res/layout/fragment_cookbook.xml @@ -1,6 +1,44 @@ + 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..724cb9e 100644 --- a/app/src/main/res/layout/fragment_recipe.xml +++ b/app/src/main/res/layout/fragment_recipe.xml @@ -1,6 +1,73 @@ + 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/vh_recipe_category.xml b/app/src/main/res/layout/vh_recipe_category.xml index 006fd49..a64ac49 100644 --- a/app/src/main/res/layout/vh_recipe_category.xml +++ b/app/src/main/res/layout/vh_recipe_category.xml @@ -1,6 +1,19 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="16dp" + > + + \ 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..b0f99b4 100644 --- a/app/src/main/res/layout/vh_recipe_item.xml +++ b/app/src/main/res/layout/vh_recipe_item.xml @@ -1,6 +1,62 @@ - + - \ No newline at end of file + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/recipe_app_bar.xml b/app/src/main/res/menu/recipe_app_bar.xml new file mode 100644 index 0000000..91be031 --- /dev/null +++ b/app/src/main/res/menu/recipe_app_bar.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/cookbook_graph.xml b/app/src/main/res/navigation/cookbook_graph.xml new file mode 100644 index 0000000..ac291ed --- /dev/null +++ b/app/src/main/res/navigation/cookbook_graph.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + \ 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..60cc69a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,17 @@ Cookbook + Close + Search + Delete + Unknown view type + Recipe image description + Title + Short description + Full description + A + First letter + Recipe image + Are you sure you want to delete %1$s? + Cancel + OK \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..adc4036 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 515d8d1..b5d4836 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.7.3" +agp = "8.9.2" kotlin = "2.1.0" coreKtx = "1.15.0" fragmentKtx = "1.8.5" @@ -15,6 +15,7 @@ junitVersion = "1.2.1" espressoCore = "3.6.1" serialization = "1.7.3" datastore = "1.1.1" +glideVersion = "4.16.0" [libraries] @@ -37,6 +38,7 @@ androidx-activity = { group = "androidx.activity", name = "activity", version.re androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +github-glide = { module = "com.github.bumptech.glide:glide", version.ref = "glideVersion" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c4701d6..b88a568 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Nov 30 19:52:55 CET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 4f5c2e445db57f63585deb980dd430896108ae60 Mon Sep 17 00:00:00 2001 From: Sergei Korotaev Date: Sat, 10 May 2025 11:24:58 +0300 Subject: [PATCH 2/2] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=20=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82=D0=B0=D0=BC=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/ru/otus/cookbook/ui/RecipeAdapter.kt | 13 ++++--------- gradle/libs.versions.toml | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeAdapter.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeAdapter.kt index 2d25777..1b4ee18 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeAdapter.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeAdapter.kt @@ -16,13 +16,13 @@ class RecipeAdapter(private val context: Context, private val onRecipeClick: (In ListAdapter(RecipeDiffCallback()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { - ITEMTYPE_RECIPE-> { + R.layout.vh_recipe_item-> { val binding = VhRecipeItemBinding.inflate(LayoutInflater.from(context), parent, false) RecipeViewHolder(binding) } - ITEMTYPE_CATEGORY-> { + R.layout.vh_recipe_category-> { val binding = VhRecipeCategoryBinding.inflate(LayoutInflater.from(context), parent, false) CategoryViewHolder(binding) @@ -34,8 +34,8 @@ class RecipeAdapter(private val context: Context, private val onRecipeClick: (In override fun getItemViewType(position: Int): Int { return when (getItem(position)) { - is RecipeListItem.RecipeItem -> ITEMTYPE_RECIPE - is RecipeListItem.CategoryItem -> ITEMTYPE_CATEGORY + is RecipeListItem.RecipeItem -> R.layout.vh_recipe_item + is RecipeListItem.CategoryItem -> R.layout.vh_recipe_category } } @@ -64,11 +64,6 @@ class RecipeAdapter(private val context: Context, private val onRecipeClick: (In binding.categoryName.text = item.name } } - - companion object { - const val ITEMTYPE_RECIPE = 1 - const val ITEMTYPE_CATEGORY = 2 - } } class RecipeDiffCallback : DiffUtil.ItemCallback() { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b5d4836..d12c941 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.9.2" +agp = "8.10.0" kotlin = "2.1.0" coreKtx = "1.15.0" fragmentKtx = "1.8.5"