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
8 changes: 7 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,14 @@ dependencies {
implementation(libs.androidx.navigation.fragment)
implementation(libs.androidx.navigation.ui)
implementation(libs.material)
implementation(libs.glide)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
testImplementation(libs.junit)
testImplementation(libs.kotlin.coroutines.test)
}
annotationProcessor(libs.compiler)
}



3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<application
android:allowBackup="true"
android:name=".App"
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package ru.otus.cookbook

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.AppGlideModule
import ru.otus.cookbook.databinding.ActivityMainBinding

@GlideModule
class MyAppGlideModule : AppGlideModule()
class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding
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.vh_category_name)

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
}

}
26 changes: 23 additions & 3 deletions app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import ru.otus.cookbook.R
import ru.otus.cookbook.data.RecipeListItem
import ru.otus.cookbook.databinding.FragmentCookbookBinding
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder

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,
Expand All @@ -34,13 +38,29 @@ class CookbookFragment : Fragment() {
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.collect(::onRecipeListUpdated)
}
binding.withBinding {
cookbookToolbar.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
cookbookList.adapter = adapter
}

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

override fun itemClicked(id: Int) {
val action = CookbookFragmentDirections.actionCookbookToRecipe(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)
}
51 changes: 49 additions & 2 deletions app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.MutableCreationExtras
import kotlinx.coroutines.launch
import ru.otus.cookbook.R
import ru.otus.cookbook.data.Recipe
import ru.otus.cookbook.databinding.FragmentRecipeBinding
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.navigation.fragment.findNavController
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 recipeId: Int get() = RecipeFragmentArgs.fromBundle(requireArguments()).recipeId

private val binding = FragmentBindingDelegate<FragmentRecipeBinding>(this)
private val model: RecipeFragmentViewModel by viewModels(
Expand Down Expand Up @@ -43,6 +49,38 @@ class RecipeFragment : Fragment() {
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.collect(::displayRecipe)
}
binding.withBinding {
recipeToolbar.setNavigationOnClickListener {
findNavController().popBackStack()
}
recipeToolbar.setOnMenuItemClickListener {
val action = RecipeFragmentDirections.actionRecipeToDeleteDialog(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 +91,16 @@ class RecipeFragment : Fragment() {
}

private fun displayRecipe(recipe: Recipe) {
// Display the recipe
binding.withBinding {
recipeToolbar.title = recipe.title
recipeTitle.text = recipe.title
recipeDescription.text = recipe.description
recipeSteps.text = recipe.steps.joinToString(".")
Glide.with(root)
.load(recipe.imageUrl)
.centerCrop()
.into(recipeImage)
}
}

private fun deleteRecipe() {
Expand Down
32 changes: 32 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,32 @@
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 ru.otus.cookbook.R
import ru.otus.cookbook.data.RecipeListItem
import com.bumptech.glide.Glide

class RecipeViewHolder(view: View, private val itemClickListener: ItemClickListener) : RecyclerView.ViewHolder(view) {
private val root = view.findViewById<ConstraintLayout>(R.id.vh_recipe_item)
private val letter = view.findViewById<TextView>(R.id.vh_recipe_letter)
private val title = view.findViewById<TextView>(R.id.vh_recipe_title)
private val description = view.findViewById<TextView>(R.id.vh_recipe_description)
private val image = view.findViewById<ImageView>(R.id.vh_recipe_image)

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) }
}
}
17 changes: 17 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,17 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
14 changes: 14 additions & 0 deletions app/src/main/res/drawable/ic_arrow.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/ic_close.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
Loading