diff --git a/app/src/main/kotlin/ru/otus/cookbook/data/RecipeListItem.kt b/app/src/main/kotlin/ru/otus/cookbook/data/RecipeListItem.kt index ba9bbe1..d62008a 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/data/RecipeListItem.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/data/RecipeListItem.kt @@ -7,6 +7,11 @@ import ru.otus.cookbook.R * Recipe list items. */ sealed class RecipeListItem : WithLayoutId { + + // See RecipeAdapter.kt + abstract fun isSame(other: RecipeListItem): Boolean + abstract fun isContentSame(other: RecipeListItem): Boolean + /** * Recipe item. */ @@ -20,6 +25,12 @@ sealed class RecipeListItem : WithLayoutId { val title: String get() = recipe.title val description: String get() = recipe.description val imageUrl: String get() = recipe.imageUrl + + override fun isSame(other: RecipeListItem): Boolean = id == (other as? RecipeItem)?.id + override fun isContentSame(other: RecipeListItem): Boolean { + if (other !is RecipeItem) return false + return title == other.title && description == other.description && imageUrl == other.imageUrl + } } /** @@ -32,5 +43,8 @@ sealed class RecipeListItem : WithLayoutId { } val name: String get() = category.name + + override fun isSame(other: RecipeListItem): Boolean = name == (other as? CategoryItem)?.name + override fun isContentSame(other: RecipeListItem): Boolean = name == (other as? CategoryItem)?.name } } \ 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..d49e404 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,6 +9,7 @@ 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.data.RecipeListItem import ru.otus.cookbook.databinding.FragmentCookbookBinding @@ -17,6 +19,13 @@ class CookbookFragment : Fragment() { private val binding = FragmentBindingDelegate(this) private val model: CookbookFragmentViewModel by viewModels { CookbookFragmentViewModel.Factory } + private val adapter = RecipeAdapter { id -> + Log.d(TAG, "Recipe clicked: $id") + val navController = findNavController() + val action = CookbookFragmentDirections.actionCookbookFragmentToRecipeFragment(id) + navController.navigate(action) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -37,10 +46,14 @@ class CookbookFragment : Fragment() { } private fun setupRecyclerView() = binding.withBinding { - // Setup RecyclerView + recipes.adapter = adapter } private fun onRecipeListUpdated(recipeList: List) { - // Handle recipe list + adapter.submitList(recipeList) + } + + companion object { + const val TAG = "CookbookFragment" } } \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteConfirmationFragment.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteConfirmationFragment.kt new file mode 100644 index 0000000..8382371 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteConfirmationFragment.kt @@ -0,0 +1,48 @@ +package ru.otus.cookbook.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.navigation.fragment.findNavController +import ru.otus.cookbook.R +import ru.otus.cookbook.databinding.FragmentConfirmationBinding + +class DeleteConfirmationFragment : DialogFragment() { + private val binding = FragmentBindingDelegate(this) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = binding.bind( + container, + FragmentConfirmationBinding::inflate + ) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.withBinding { + text.text = getString(R.string.text_delete, DeleteConfirmationFragmentArgs.fromBundle(requireArguments()).title) + ok.setOnClickListener { + setResult(true) + } + cancel.setOnClickListener { + setResult(false) + } + } + } + + private fun setResult(result: Boolean) { + findNavController().previousBackStackEntry?.savedStateHandle?.set( + CONFIRMATION_RESULT, + result + ) + findNavController().popBackStack() + } + + companion object { + const val CONFIRMATION_RESULT = "confirmation_result" + } +} \ 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..82d5e83 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeAdapter.kt @@ -0,0 +1,68 @@ +package ru.otus.cookbook.ui + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import ru.otus.cookbook.data.RecipeListItem +import ru.otus.cookbook.databinding.VhRecipeCategoryBinding +import ru.otus.cookbook.databinding.VhRecipeItemBinding +import java.util.Locale + +/** + * Recipe adapter + */ +class RecipeAdapter(private val onClick: (Int) -> Unit) : ListAdapter(RecipeDiff) { + + override fun getItemViewType(position: Int): Int { + return getItem(position).layoutId + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when(viewType) { + RecipeListItem.RecipeItem.layoutId -> RecipeViewHolder.RecipeHolder( + VhRecipeItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), + onClick + ) + RecipeListItem.CategoryItem.layoutId -> RecipeViewHolder.CategoryHolder( + VhRecipeCategoryBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + else -> throw IllegalArgumentException("Unknown view type: $viewType") + } + + override fun onBindViewHolder(holder: RecipeViewHolder, position: Int) { + when(holder) { + is RecipeViewHolder.CategoryHolder -> holder.bind(getItem(position) as RecipeListItem.CategoryItem) + is RecipeViewHolder.RecipeHolder -> holder.bind(getItem(position) as RecipeListItem.RecipeItem) + } + } + + sealed class RecipeViewHolder(view: View): ViewHolder(view) { + + class RecipeHolder(private val binding: VhRecipeItemBinding, private val onClick: (Int) -> Unit) : RecipeViewHolder(binding.root) { + private var id: Int = -1 + + init { + binding.root.setOnClickListener { onClick(id) } + } + + fun bind(recipe: RecipeListItem.RecipeItem) = with(binding) { + this@RecipeHolder.id = recipe.id + id.text = String.format(Locale.getDefault(), "%02d", recipe.id) + title.text = recipe.title + } + } + + class CategoryHolder(private val binding: VhRecipeCategoryBinding) : RecipeViewHolder(binding.root) { + fun bind(category: RecipeListItem.CategoryItem) = with(binding) { + name.text = category.name + } + } + } +} + +private object RecipeDiff : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: RecipeListItem, newItem: RecipeListItem): Boolean = oldItem.isSame(newItem) + override fun areContentsTheSame(oldItem: RecipeListItem, newItem: RecipeListItem): Boolean = oldItem.isContentSame(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..0380cf0 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt @@ -1,21 +1,26 @@ 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 import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.DefaultLifecycleObserver +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( @@ -38,6 +43,8 @@ class RecipeFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setupAppBar() + setupAlertResult() viewLifecycleOwner.lifecycleScope.launch { model.recipe .flowWithLifecycle(viewLifecycleOwner.lifecycle) @@ -52,11 +59,67 @@ class RecipeFragment : Fragment() { return model.recipe.value.title } - private fun displayRecipe(recipe: Recipe) { - // Display the recipe + private fun setupAppBar() = binding.withBinding { + topAppBar.setNavigationOnClickListener { + close() + } + topAppBar.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.action_delete -> { + deleteRecipe() + true + } + else -> false + } + } + } + + private fun close() { + findNavController().popBackStack() + } + + /** + * Sets up alert dialog for delete result. + * https://developer.android.com/guide/navigation/use-graph/programmatic#returning_a_result + */ + private fun setupAlertResult() { + val navBackStackEntry = findNavController().getBackStackEntry(R.id.recipeFragment) + + val observer = object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + if (navBackStackEntry.savedStateHandle.contains(DeleteConfirmationFragment.CONFIRMATION_RESULT)) { + if (true == navBackStackEntry.savedStateHandle.get(DeleteConfirmationFragment.CONFIRMATION_RESULT)) { + Log.d(TAG, "Deleting recipe $recipeId") + model.delete() + close() + } + navBackStackEntry.savedStateHandle.remove(DeleteConfirmationFragment.CONFIRMATION_RESULT) + } + } + } + + navBackStackEntry.lifecycle.addObserver(observer) + + viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + navBackStackEntry.lifecycle.removeObserver(observer) + } + }) + } + + private fun displayRecipe(recipe: Recipe) = binding.withBinding { + title.text = recipe.title + steps.text = recipe.steps.joinToString("\n") } private fun deleteRecipe() { - model.delete() + Log.d(TAG, "Deleting recipe $recipeId") + findNavController().navigate( + RecipeFragmentDirections.actionRecipeFragmentToDeleteConfirmation(getTitle()) + ) + } + + companion object { + private const val TAG = "RecipeFragment" } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..9f83b8f --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000..075e95d --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000..883bcaa --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..d29c6ea --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 86a5d97..8b540e6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,14 @@ - - + android:layout_height="match_parent"> - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_confirmation.xml b/app/src/main/res/layout/fragment_confirmation.xml new file mode 100644 index 0000000..382626f --- /dev/null +++ b/app/src/main/res/layout/fragment_confirmation.xml @@ -0,0 +1,56 @@ + + + + + + + +