Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.navigation.fragment)
implementation(libs.androidx.navigation.ui)
implementation(libs.glide)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/ui/CategoryViewHolder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ru.otus.cookbook.ui

import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import ru.otus.cookbook.R
import ru.otus.cookbook.data.RecipeListItem

class CategoryViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val categoryName = view.findViewById<TextView>(R.id.category_name_tv)

fun bind(item: RecipeListItem.CategoryItem) {
categoryName.text = item.name
}
}
66 changes: 66 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/ui/CookBookAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package ru.otus.cookbook.ui

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import ru.otus.cookbook.R
import ru.otus.cookbook.data.RecipeListItem

class CookBookAdapter(private val itemClickListener: ItemClickListener) :
ListAdapter<RecipeListItem, RecyclerView.ViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ViewTypes.RECIPE.ordinal -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.vh_recipe_item, parent, false)
RecipeViewHolder(view, itemClickListener)
}

ViewTypes.CATEGORY.ordinal -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.vh_recipe_category, parent, false)
CategoryViewHolder(view)
}

else -> throw IllegalArgumentException("View type is not supported")
}
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is RecipeViewHolder -> holder.bind(getItem(position) as RecipeListItem.RecipeItem)
is CategoryViewHolder -> holder.bind(getItem(position) as RecipeListItem.CategoryItem)
}
}

override fun getItemViewType(position: Int): Int {
return when (currentList[position]) {
is RecipeListItem.CategoryItem -> ViewTypes.CATEGORY.ordinal
is RecipeListItem.RecipeItem -> ViewTypes.RECIPE.ordinal
}
}
}

enum class ViewTypes {
CATEGORY,
RECIPE
}

class DiffCallback : DiffUtil.ItemCallback<RecipeListItem>() {
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
}

}
28 changes: 24 additions & 4 deletions app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +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 com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
import ru.otus.cookbook.R
import ru.otus.cookbook.data.RecipeListItem
import ru.otus.cookbook.databinding.FragmentCookbookBinding

class CookbookFragment : Fragment() {
class CookbookFragment : Fragment(), ItemClickListener {

private val binding = FragmentBindingDelegate<FragmentCookbookBinding>(this)
private val model: CookbookFragmentViewModel by viewModels { CookbookFragmentViewModel.Factory }
private val adapter: CookBookAdapter by lazy { CookBookAdapter(this) }

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
savedInstanceState: Bundle?,
): View = binding.bind(
container,
FragmentCookbookBinding::inflate
Expand All @@ -34,13 +38,29 @@ class CookbookFragment : Fragment() {
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.collect(::onRecipeListUpdated)
}
binding.withBinding {
recipeListMt.setNavigationOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setMessage(getString(R.string.close_app_message))
.setPositiveButton(R.string.yes_btn) { _, _ ->
requireActivity().finishAndRemoveTask()
}
.setNegativeButton(R.string.no_btn, null)
.show()
}
}
}

private fun setupRecyclerView() = binding.withBinding {
// Setup RecyclerView
recipeListRv.adapter = adapter
}

private fun onRecipeListUpdated(recipeList: List<RecipeListItem>) {
// Handle recipe list
adapter.submitList(recipeList)
}

override fun itemClicked(id: Int) {
val action = CookbookFragmentDirections.actionCookbookFragmentToRecipeFragment(id)
findNavController().navigate(action)
}
}
41 changes: 41 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/ui/DeleteDialogFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package ru.otus.cookbook.ui

import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import ru.otus.cookbook.R

class DeleteDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.delete_dialog_title))
.setMessage(
getString(
R.string.delete_dialog_message,
DeleteDialogFragmentArgs.fromBundle(requireArguments()).recipeTitle
)
)
.setPositiveButton(R.string.ok_btn) { _, _ ->
dismiss()
setConfirmationResult(true)
}
.setNegativeButton(R.string.cancel_btn) { _, _ ->
dismiss()
setConfirmationResult(false)
}
.create()
}

private fun setConfirmationResult(result: Boolean) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(
DELETE_CONFIRMATION_RESULT, result
)
}

companion object {
const val DELETE_CONFIRMATION_RESULT = "result"
}

}
5 changes: 5 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/ui/ItemClickListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ru.otus.cookbook.ui

interface ItemClickListener {
fun itemClicked(id: Int)
}
52 changes: 49 additions & 3 deletions app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.navigation.fragment.findNavController
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 recipeId: Int get() = RecipeFragmentArgs.fromBundle(requireArguments()).recipeId

private val binding = FragmentBindingDelegate<FragmentRecipeBinding>(this)
private val model: RecipeFragmentViewModel by viewModels(
Expand All @@ -30,7 +35,7 @@ class RecipeFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
savedInstanceState: Bundle?,
): View = binding.bind(
container,
FragmentRecipeBinding::inflate
Expand All @@ -43,6 +48,38 @@ class RecipeFragment : Fragment() {
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.collect(::displayRecipe)
}
binding.withBinding {
detailRecipeMt.setNavigationOnClickListener {
findNavController().popBackStack()
}
detailRecipeMt.setOnMenuItemClickListener {
val action = RecipeFragmentDirections.actionRecipeFragmentToDeleteDialogFragment(getTitle())
findNavController().navigate(action)
true
}
}

val navBackStackEntry = findNavController().getBackStackEntry(R.id.recipeFragment)

val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME &&
navBackStackEntry.savedStateHandle.contains(DeleteDialogFragment.DELETE_CONFIRMATION_RESULT)
) {
if (navBackStackEntry
.savedStateHandle.get<Boolean>(DeleteDialogFragment.DELETE_CONFIRMATION_RESULT) == true
) {
deleteRecipe()
findNavController().popBackStack()
}
}
}

navBackStackEntry.lifecycle.addObserver(observer)
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}

/**
Expand All @@ -53,7 +90,16 @@ class RecipeFragment : Fragment() {
}

private fun displayRecipe(recipe: Recipe) {
// Display the recipe
binding.withBinding {
detailRecipeMt.title = recipe.title
detailRecipeTitleTv.text = recipe.title
detailRecipeDescriptionTv.text = recipe.description
detailRecipeStepsTv.text = recipe.steps.joinToString(".")
Glide.with(this@RecipeFragment)
.load(recipe.imageUrl)
.centerCrop()
.into(detailRecipeImageIv)
}
}

private fun deleteRecipe() {
Expand Down
31 changes: 31 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/ui/RecipeViewHolder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ru.otus.cookbook.ui

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import ru.otus.cookbook.R
import ru.otus.cookbook.data.RecipeListItem

class RecipeViewHolder(view: View, private val itemClickListener: ItemClickListener) : RecyclerView.ViewHolder(view) {
private val root = view.findViewById<ConstraintLayout>(R.id.recipe_item_cl)
private val letter = view.findViewById<TextView>(R.id.letter_tv)
private val title = view.findViewById<TextView>(R.id.title_tv)
private val description = view.findViewById<TextView>(R.id.description_tv)
private val image = view.findViewById<ImageView>(R.id.item_iv)

fun bind(item: RecipeListItem.RecipeItem) {

letter.text = item.title.first().toString()
title.text = item.title
description.text = item.description
Glide.with(root.context)
.load(item.imageUrl)
.centerCrop()
.circleCrop()
.into(image)
root.setOnClickListener { itemClickListener.itemClicked(item.id) }
}
}
16 changes: 16 additions & 0 deletions app/src/main/res/drawable/bg_list_item_rounded_corners.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />

<stroke
android:width="1dp"
android:color="?attr/colorOutlineVariant"
/>

<corners
android:bottomLeftRadius="@dimen/standard"
android:bottomRightRadius="@dimen/standard"
android:topLeftRadius="@dimen/standard"
android:topRightRadius="@dimen/standard"
/>
</shape>
11 changes: 11 additions & 0 deletions app/src/main/res/drawable/circle_shape.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="?attr/colorPrimaryFixed" />

<size
android:height="40dp"
android:width="40dp" />

</shape>
13 changes: 13 additions & 0 deletions app/src/main/res/drawable/ic_arrow.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<group>
<clip-path
android:pathData="M20,0L20,0A20,20 0,0 1,40 20L40,20A20,20 0,0 1,20 40L20,40A20,20 0,0 1,0 20L0,20A20,20 0,0 1,20 0z"/>
<path
android:pathData="M15.825,21L21.425,26.6L20,28L12,20L20,12L21.425,13.4L15.825,19H28V21H15.825Z"
android:fillColor="?attr/colorOnBackground"/>
</group>
</vector>
9 changes: 9 additions & 0 deletions app/src/main/res/drawable/ic_close.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M14.4,27L13,25.6L18.6,20L13,14.4L14.4,13L20,18.6L25.6,13L27,14.4L21.4,20L27,25.6L25.6,27L20,21.4L14.4,27Z"
android:fillColor="?attr/colorOnBackground"/>
</vector>
9 changes: 9 additions & 0 deletions app/src/main/res/drawable/ic_trash.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M7,21C6.45,21 5.979,20.804 5.588,20.413C5.196,20.021 5,19.55 5,19V6H4V4H9V3H15V4H20V6H19V19C19,19.55 18.804,20.021 18.413,20.413C18.021,20.804 17.55,21 17,21H7ZM17,6H7V19H17V6ZM9,17H11V8H9V17ZM13,17H15V8H13V17Z"
android:fillColor="?attr/colorOnBackground"/>
</vector>
Loading