diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f3207a6..d449e29 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,4 +50,7 @@ dependencies { implementation(libs.androidx.constraintlayout) testImplementation(libs.junit) testImplementation(libs.kotlin.coroutines.test) + implementation(libs.androidx.recyclerview) + implementation(libs.glide) + annotationProcessor(libs.compiler) } \ 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 adapter = RecipesAdapter { launchRecipeFragment(it) } override fun onCreateView( inflater: LayoutInflater, @@ -28,6 +31,13 @@ class CookbookFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.withBinding { + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + (activity as? AppCompatActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(true) + toolbar.setNavigationOnClickListener { + findNavController().navigateUp() + } + } setupRecyclerView() viewLifecycleOwner.lifecycleScope.launch { model.recipeList @@ -36,11 +46,15 @@ class CookbookFragment : Fragment() { } } + private fun launchRecipeFragment(recipeId: Int) { + findNavController().navigate(CookbookFragmentDirections.actionCookbookFragmentToRecipeFragment(recipeId)) + } + private fun setupRecyclerView() = binding.withBinding { - // Setup RecyclerView + recyclerViewCategory.adapter = adapter } private fun onRecipeListUpdated(recipeList: List) { - // Handle recipe list + adapter.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..8184fe2 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteRecipeDialogFragment.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.navArgs +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import ru.otus.cookbook.R +import androidx.navigation.fragment.findNavController +import kotlin.getValue + +class DeleteRecipeDialogFragment: DialogFragment() { + + private val args by navArgs() + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val recipeTitle = args.recipeTitle + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.delete)) + .setMessage(getString(R.string.are_you_sure_want_to_delete, recipeTitle)) + .setPositiveButton(getString(R.string.ok)) { _, _ -> + dismiss() + setResult(true) + } + .setNegativeButton(getString(R.string.cancel)) { _, _ -> + dismiss() + setResult(false) + } + .create() + } + + private fun setResult(result: Boolean) { + val navController = findNavController() + navController.previousBackStackEntry?.savedStateHandle?.set( + DIALOG_RESULT_KEY, + result + ) + } + + companion object { + const val DIALOG_RESULT_KEY = "delete" + } +} 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..20671f7 --- /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) { +} diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDiffCallback.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDiffCallback.kt new file mode 100644 index 0000000..1183962 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDiffCallback.kt @@ -0,0 +1,20 @@ +package ru.otus.cookbook.ui + +import androidx.recyclerview.widget.DiffUtil +import ru.otus.cookbook.data.RecipeListItem + +class RecipeDiffCallback: DiffUtil.ItemCallback() { + override fun areItemsTheSame(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.id == newItem.id + else -> false + } + } + + override fun areContentsTheSame(oldItem: RecipeListItem, newItem: RecipeListItem): Boolean { + return oldItem == newItem + } +} 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..5a5ac8a 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt @@ -4,18 +4,24 @@ import android.os.Bundle 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.fragment.navArgs import kotlinx.coroutines.launch import ru.otus.cookbook.data.Recipe import ru.otus.cookbook.databinding.FragmentRecipeBinding +import androidx.navigation.fragment.findNavController +import android.os.Handler +import android.os.Looper +import com.bumptech.glide.Glide 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 by navArgs() + private val recipeId: Int get() = args.recipeId private val binding = FragmentBindingDelegate(this) private val model: RecipeFragmentViewModel by viewModels( @@ -38,6 +44,14 @@ class RecipeFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + binding.withBinding { + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + (activity as? AppCompatActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + setupToolBar() + observeDeleteResult() + viewLifecycleOwner.lifecycleScope.launch { model.recipe .flowWithLifecycle(viewLifecycleOwner.lifecycle) @@ -45,6 +59,37 @@ class RecipeFragment : Fragment() { } } + private fun setupToolBar() { + binding.withBinding { + (activity as? AppCompatActivity)?.supportActionBar?.title = getTitle() + toolbar.setNavigationOnClickListener { + findNavController().navigateUp() + } + imageViewDelete.setOnClickListener { + findNavController().navigate( + RecipeFragmentDirections.actionRecipeFragmentToDeleteRecipeDialogFragment( + getTitle() + ) + ) + } + } + } + + private fun observeDeleteResult() { + val navController = findNavController() + navController.currentBackStackEntry + ?.savedStateHandle + ?.getLiveData(DeleteRecipeDialogFragment.DIALOG_RESULT_KEY) + ?.observe(viewLifecycleOwner) { result -> + if (result) { + deleteRecipe() + Handler(Looper.getMainLooper()).post { + navController.navigateUp() + } + } + } + } + /** * Use to get recipe title and pass to confirmation dialog */ @@ -53,7 +98,16 @@ class RecipeFragment : Fragment() { } private fun displayRecipe(recipe: Recipe) { - // Display the recipe + binding.withBinding { + textViewTitle.text = recipe.title + textViewDescription.text = recipe.description + textViewRecipe.text = recipe.steps.mapIndexed { index, step -> "${index + 1}. $step"} + .joinToString("\n") + Glide.with(root) + .load(recipe.imageUrl) + .centerCrop() + .into(imageViewRecipe) + } } private fun deleteRecipe() { 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..c383f1e --- /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) { +} 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..e227b43 --- /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(RecipeDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + + return when(viewType){ + VIEW_TYPE_CATEGORY -> { + val binding = VhRecipeCategoryBinding.inflate(inflater, parent, false) + RecipeCategoryViewHolder(binding) + } + VIEW_TYPE_RECIPE -> { + val binding = VhRecipeItemBinding.inflate(inflater, parent, false) + RecipeItemViewHolder(binding) + } + else -> throw RuntimeException("RuntimeException") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = getItem(position) + when(holder){ + is RecipeCategoryViewHolder -> { + val category = item as RecipeListItem.CategoryItem + holder.binding.textViewCategory.text = category.category.name + } + is RecipeItemViewHolder -> { + val recipeItem = item as RecipeListItem.RecipeItem + with(holder){ + binding.textBackground.text = recipeItem.title.first().toString() + binding.title.text = recipeItem.title + binding.description.text = recipeItem.description + Glide.with(binding.root.context) + .load(recipeItem.imageUrl) + .centerCrop() + .into(binding.imageViewRecipe) + binding.root.setOnClickListener { onRecipeClick(item.id) } + } + } + } + } + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is RecipeListItem.CategoryItem -> VIEW_TYPE_CATEGORY + is RecipeListItem.RecipeItem -> VIEW_TYPE_RECIPE + } + } + + companion object{ + private const val VIEW_TYPE_CATEGORY = 1 + private const val VIEW_TYPE_RECIPE = 2 + } +} 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..5fb9556 --- /dev/null +++ b/app/src/main/res/drawable/arrow_back.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/close.xml b/app/src/main/res/drawable/close.xml new file mode 100644 index 0000000..dd3c867 --- /dev/null +++ b/app/src/main/res/drawable/close.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/image_view_recipe.xml b/app/src/main/res/drawable/image_view_recipe.xml new file mode 100644 index 0000000..c41fc68 --- /dev/null +++ b/app/src/main/res/drawable/image_view_recipe.xml @@ -0,0 +1,4 @@ + + + + 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..9b124b6 --- /dev/null +++ b/app/src/main/res/drawable/trash_icon.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 86a5d97..9b50803 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 + + diff --git a/app/src/main/res/layout/fragment_cookbook.xml b/app/src/main/res/layout/fragment_cookbook.xml index 77d9ef6..b8206db 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 + + + + + diff --git a/app/src/main/res/layout/fragment_recipe.xml b/app/src/main/res/layout/fragment_recipe.xml index 77d9ef6..2390006 100644 --- a/app/src/main/res/layout/fragment_recipe.xml +++ b/app/src/main/res/layout/fragment_recipe.xml @@ -1,6 +1,77 @@ - + 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..a3256e1 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..54141b3 100644 --- a/app/src/main/res/layout/vh_recipe_item.xml +++ b/app/src/main/res/layout/vh_recipe_item.xml @@ -1,6 +1,73 @@ + android:layout_width="match_parent" + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools" + app:cardCornerRadius="10dp" + android:background="#CAC4D0" + android:layout_marginBottom="10dp" + android:layout_marginHorizontal="12dp" + 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..135354c --- /dev/null +++ b/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8fce1d1..7c86a38 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,7 @@ Cookbook + Delete + Are you sure want to delete %1$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..9ccbfc4 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -5,5 +5,13 @@ + + +