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
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import org.gradle.kotlin.dsl.implementation

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
Expand Down Expand Up @@ -50,4 +52,7 @@ dependencies {
implementation(libs.androidx.constraintlayout)
testImplementation(libs.junit)
testImplementation(libs.kotlin.coroutines.test)

implementation("io.coil-kt.coil3:coil-network-okhttp:3.3.0")
implementation("io.coil-kt.coil3:coil:3.3.0")
}
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

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

<application
android:allowBackup="true"
android:name=".App"
Expand Down
31 changes: 31 additions & 0 deletions app/src/main/java/ru/otus/cookbook/ConfirmationDialog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ru.otus.cookbook

import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import ru.otus.cookbook.ui.RESULT_KEY
import kotlin.getValue

class ConfirmationDialog : DialogFragment() {

private val args: ConfirmationDialogArgs by navArgs()
private val recipeTitle: String get() = args.recipeTitle

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
AlertDialog.Builder(requireContext())
.setMessage("Удалить рецепт $recipeTitle?")
.setPositiveButton("Да") { _, _ ->
setResult(true)
}
.setNegativeButton("Нет") { _, _ ->
setResult(false)
}
.create()

private fun setResult(result: Boolean) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(RESULT_KEY, result)
}
}
11 changes: 11 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,6 +2,9 @@ package ru.otus.cookbook

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController
import ru.otus.cookbook.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
Expand All @@ -12,5 +15,13 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

setSupportActionBar(binding.toolbar)

val navHost =
supportFragmentManager.findFragmentById(R.id.cookbook_fragment_container) as NavHostFragment
val navController = navHost.navController
val appConfiguration = AppBarConfiguration(navController.graph)
binding.toolbar.setupWithNavController(navController, appConfiguration)
}
}
12 changes: 11 additions & 1 deletion app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import kotlinx.coroutines.launch
import ru.otus.cookbook.data.RecipeListItem
import ru.otus.cookbook.databinding.FragmentCookbookBinding

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

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

private val recipesAdapter by lazy { CookbookRecyclerViewAdapter(this) }

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
Expand All @@ -38,9 +41,16 @@ class CookbookFragment : Fragment() {

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

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

override fun onRecipeClick(recipeId: Int) {
val action = CookbookFragmentDirections.actionCookbookFragmentToRecipeFragment(recipeId)
view?.findNavController()?.navigate(action)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package ru.otus.cookbook.ui

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

class CookbookRecyclerViewAdapter(
val recipeListener: RecipeItemListener
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

private var list = emptyList<RecipeListItem>()

fun setList(recipeList: List<RecipeListItem>) {
list = recipeList
notifyDataSetChanged()
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ViewType.CATEGORY.id -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.vh_recipe_category, parent, false)
CategoryItemViewHolder(view)
}

ViewType.RECIPE.id -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.vh_recipe_item, parent, false)
RecipeItemViewHolder(
view = view,
recipeListener = recipeListener
)
}

else -> throw IllegalArgumentException("Unknown view type")
}
}

override fun getItemViewType(position: Int): Int {
return when (list[position]) {
is RecipeListItem.CategoryItem -> ViewType.CATEGORY.id
is RecipeListItem.RecipeItem -> ViewType.RECIPE.id
}
}

override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int
) {
val item = list[position]

when (getItemViewType(position)) {
ViewType.CATEGORY.id -> (holder as CategoryItemViewHolder).bind(item as RecipeListItem.CategoryItem)
ViewType.RECIPE.id -> (holder as RecipeItemViewHolder).bind(item as RecipeListItem.RecipeItem)
}

}

override fun getItemCount(): Int = list.size

private enum class ViewType(val id: Int) {
CATEGORY(R.layout.vh_recipe_category),
RECIPE(R.layout.vh_recipe_item)
}
}
50 changes: 50 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/ui/CookbookViewHolders.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ru.otus.cookbook.ui

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView
import ru.otus.cookbook.R
import ru.otus.cookbook.data.RecipeListItem
import coil3.load
import coil3.request.placeholder
import coil3.request.error

interface RecipeItemListener {
fun onRecipeClick(recipeId: Int)
}

class CategoryItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val name: TextView by lazy { view.findViewById<TextView>(R.id.category_item_name) }

fun bind(categoryItem: RecipeListItem.CategoryItem) {
name.text = categoryItem.name
}
}

class RecipeItemViewHolder(
view: View,
val recipeListener: RecipeItemListener
) : RecyclerView.ViewHolder(view) {
private val letter: TextView by lazy { view.findViewById(R.id.recipe_letter) }
private val title: TextView by lazy { view.findViewById(R.id.recipe_item_title) }
private val description: TextView by lazy { view.findViewById(R.id.recipe_item_description) }
private val image: ImageView by lazy { view.findViewById(R.id.recipe_image) }

private val recipeCard: CardView by lazy { view.findViewById(R.id.recipe_card) }

fun bind(recipeItem: RecipeListItem.RecipeItem) {
letter.text = recipeItem.title[0].toString()
title.text = recipeItem.title
description.text = recipeItem.description
image.load(recipeItem.imageUrl) {
placeholder(R.drawable.ic_android_placeholder)
error(R.drawable.ic_android_placeholder)
}

recipeCard.setOnClickListener {
recipeListener.onRecipeClick(recipeItem.id)
}
}
}
76 changes: 75 additions & 1 deletion app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,35 @@ package ru.otus.cookbook.ui

import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.view.MenuProvider
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 androidx.navigation.fragment.navArgs
import coil3.load
import coil3.request.error
import coil3.request.placeholder
import kotlinx.coroutines.launch
import ru.otus.cookbook.R
import ru.otus.cookbook.data.Recipe
import ru.otus.cookbook.databinding.FragmentRecipeBinding

const val RESULT_KEY = "result_key"

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<FragmentRecipeBinding>(this)
private val model: RecipeFragmentViewModel by viewModels(
Expand Down Expand Up @@ -43,6 +58,56 @@ class RecipeFragment : Fragment() {
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.collect(::displayRecipe)
}

requireActivity().addMenuProvider(
object : MenuProvider {
override fun onCreateMenu(
menu: Menu,
menuInflater: MenuInflater
) {
menuInflater.inflate(R.menu.options_menu, menu)
}

override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.delete_btn -> {
val action =
RecipeFragmentDirections.actionRecipeFragmentToConfirmationDialog(
getTitle()
)
findNavController().navigate(action)
true
}

else -> false
}
}
},
viewLifecycleOwner,
Lifecycle.State.RESUMED
)

val navBackStackEntry = findNavController().getBackStackEntry(R.id.recipeFragment)
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME && navBackStackEntry.savedStateHandle.contains(
RESULT_KEY
)
) {
val result = navBackStackEntry.savedStateHandle.get<Boolean>(RESULT_KEY)
if (result == true) {
deleteRecipe()
findNavController().popBackStack()
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

После получения результата из диалога ключ RESULT_KEY не удаляется из SavedStateHandle. Это может привести к повторной обработке результата при возврате на экран. Давайтедобавим удаление ключа после обработки


}
}
navBackStackEntry.lifecycle.addObserver(observer)

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

/**
Expand All @@ -54,6 +119,15 @@ class RecipeFragment : Fragment() {

private fun displayRecipe(recipe: Recipe) {
// Display the recipe
binding.withBinding {
recipeImage.load(recipe.imageUrl) {
placeholder(R.drawable.ic_android_placeholder)
error(R.drawable.ic_android_placeholder)
}
recipeTitle.text = recipe.title
recipeDescription.text = recipe.description
recipeSteps.text = recipe.steps.toString()
}
}

private fun deleteRecipe() {
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/ic_android_placeholder.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M40,720Q49,613 105.5,523Q162,433 256,380L182,252Q176,243 179,233Q182,223 192,218Q200,213 210,216Q220,219 226,228L300,356Q386,320 480,320Q574,320 660,356L734,228Q740,219 750,216Q760,213 768,218Q778,223 781,233Q784,243 778,252L704,380Q798,433 854.5,523Q911,613 920,720L40,720ZM280,610Q301,610 315.5,595.5Q330,581 330,560Q330,539 315.5,524.5Q301,510 280,510Q259,510 244.5,524.5Q230,539 230,560Q230,581 244.5,595.5Q259,610 280,610ZM680,610Q701,610 715.5,595.5Q730,581 730,560Q730,539 715.5,524.5Q701,510 680,510Q659,510 644.5,524.5Q630,539 630,560Q630,581 644.5,595.5Q659,610 680,610Z"/>

</vector>
23 changes: 14 additions & 9 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:layout_width="wrap_content"
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<androidx.fragment.app.FragmentContainerView
android:id="@+id/cookbook_fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
Loading