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 @@ -48,6 +48,7 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.github.glide)
testImplementation(libs.junit)
testImplementation(libs.kotlin.coroutines.test)
}
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
8 changes: 8 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package ru.otus.cookbook
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import ru.otus.cookbook.databinding.ActivityMainBinding
import androidx.navigation.findNavController
import androidx.activity.addCallback

class MainActivity : AppCompatActivity() {

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

onBackPressedDispatcher.addCallback(this) {
if (!findNavController(R.id.fragment_container_view).popBackStack()) {
finish()
}
}
}
}
22 changes: 20 additions & 2 deletions app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.launch
import ru.otus.cookbook.data.RecipeListItem
import ru.otus.cookbook.databinding.FragmentCookbookBinding
Expand All @@ -29,6 +31,14 @@ class CookbookFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()

binding.withBinding {
topToolbar.setOnClickListener {
@Suppress("DEPRECATION")
requireActivity().onBackPressed()
}
}

viewLifecycleOwner.lifecycleScope.launch {
model.recipeList
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
Expand All @@ -37,10 +47,18 @@ class CookbookFragment : Fragment() {
}

private fun setupRecyclerView() = binding.withBinding {
// Setup RecyclerView
val adapter = RecipeAdapter(requireContext()) { recipeId ->
// Handle recipe click, e.g., navigate to RecipeFragment
val action = CookbookFragmentDirections.actionToRecipeFragment(recipeId)
findNavController().navigate(action)
}

recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = adapter
}

private fun onRecipeListUpdated(recipeList: List<RecipeListItem>) {
private fun onRecipeListUpdated(recipeList: List<RecipeListItem>) = binding.withBinding {
// Handle recipe list
(recyclerView.adapter as RecipeAdapter).submitList(recipeList)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package ru.otus.cookbook.ui

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

class DeleteRecipeDialogFragment : DialogFragment() {

companion object {
const val CONFIRMATION_RESULT = "confirmation_result"
}

private val args: DeleteRecipeDialogFragmentArgs by navArgs()

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val recipeTitle = args.recipeTitle
return MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.delete))
.setMessage(getString(R.string.delete_recipe_message, recipeTitle))
.setPositiveButton(R.string.ok) { _, _ ->
dismiss()
setResult(true)
}
.setNegativeButton(R.string.cancel
) { _, _ ->
dismiss()
setResult(false)
}
.create()
}

private fun setResult(result: Boolean) {
val navController = findNavController()
navController.previousBackStackEntry?.savedStateHandle?.set(
CONFIRMATION_RESULT,
result
)
navController.popBackStack()
}

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

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import ru.otus.cookbook.R
import ru.otus.cookbook.data.RecipeListItem
import ru.otus.cookbook.databinding.VhRecipeCategoryBinding
import ru.otus.cookbook.databinding.VhRecipeItemBinding

class RecipeAdapter(private val context: Context, private val onRecipeClick: (Int) -> Unit):
ListAdapter<RecipeListItem, RecyclerView.ViewHolder>(RecipeDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
R.layout.vh_recipe_item-> {
val binding =
VhRecipeItemBinding.inflate(LayoutInflater.from(context), parent, false)
RecipeViewHolder(binding)
}

R.layout.vh_recipe_category-> {
val binding =
VhRecipeCategoryBinding.inflate(LayoutInflater.from(context), parent, false)
CategoryViewHolder(binding)
}

else -> throw IllegalArgumentException(context.getString(R.string.unknown_view_type))
}
}

override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is RecipeListItem.RecipeItem -> R.layout.vh_recipe_item
is RecipeListItem.CategoryItem -> R.layout.vh_recipe_category
}
}

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

inner class RecipeViewHolder(private val binding: VhRecipeItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: RecipeListItem.RecipeItem) {
binding.firstLetter.text = item.title.firstOrNull()?.toString() ?: ""
binding.title.text = item.title
binding.description.text = item.description
Glide.with(binding.root.context)
.load(item.imageUrl)
.centerCrop()
.into(binding.image)
binding.root.setOnClickListener { onRecipeClick(item.id) }
}
}

inner class CategoryViewHolder(private val binding: VhRecipeCategoryBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: RecipeListItem.CategoryItem) {
binding.categoryName.text = item.name
}
}
}

class RecipeDiffCallback : 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
}
}
50 changes: 44 additions & 6 deletions app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
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 args: RecipeFragmentArgs by navArgs()
private val recipeId: Int get() = args.recipeId
private val binding = FragmentBindingDelegate<FragmentRecipeBinding>(this)
private val model: RecipeFragmentViewModel by viewModels(
extrasProducer = {
Expand All @@ -38,6 +42,19 @@ class RecipeFragment : Fragment() {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

setupAppBar()

val navController = findNavController()
navController.currentBackStackEntry?.savedStateHandle?.getLiveData<Boolean>(
DeleteRecipeDialogFragment.CONFIRMATION_RESULT
)?.observe(viewLifecycleOwner) { result->
if (result) {
model.delete()
navController.navigateUp()
}
}

viewLifecycleOwner.lifecycleScope.launch {
model.recipe
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
Expand All @@ -52,11 +69,32 @@ class RecipeFragment : Fragment() {
return model.recipe.value.title
}

private fun displayRecipe(recipe: Recipe) {
// Display the recipe
private fun setupAppBar() {
binding.withBinding {
recipeToolbar.setNavigationOnClickListener {
findNavController().navigate(R.id.action_to_cookbook)
}
recipeToolbar.setOnMenuItemClickListener { menuItem->
when (menuItem.itemId) {
R.id.delete -> {
val action = RecipeFragmentDirections.actionToDeleteRecipeDialog(getTitle())
findNavController().navigate(action)
true
}

else -> false
}
}
}
}

private fun deleteRecipe() {
model.delete()
private fun displayRecipe(recipe: Recipe) = binding.withBinding {
// Display the recipe
recipeTitle.text = recipe.title
recipeShortDescription.text = recipe.category.name
recipeFullDescription.text = recipe.description
Glide.with(this@RecipeFragment)
.load(recipe.imageUrl)
.into(recipeImage)
}
}
11 changes: 11 additions & 0 deletions app/src/main/res/drawable/arrow_back_24px.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z"/>
</vector>
9 changes: 9 additions & 0 deletions app/src/main/res/drawable/circle.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="?attr/colorPrimaryContainer" />

<size
android:width="40dp"
android:height="40dp" />
</shape>
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/close_24px.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z"/>
</vector>
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/delete_24px.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M280,840Q247,840 223.5,816.5Q200,793 200,760L200,240L160,240L160,160L360,160L360,120L600,120L600,160L800,160L800,240L760,240L760,760Q760,793 736.5,816.5Q713,840 680,840L280,840ZM680,240L280,240L280,760Q280,760 280,760Q280,760 280,760L680,760Q680,760 680,760Q680,760 680,760L680,240ZM360,680L440,680L440,320L360,320L360,680ZM520,680L600,680L600,320L520,320L520,680ZM280,240L280,240L280,760Q280,760 280,760Q280,760 280,760L280,760Q280,760 280,760Q280,760 280,760L280,240Z"/>
</vector>
14 changes: 6 additions & 8 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:layout_width="wrap_content"
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" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/cookbook_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>
Loading