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 @@ -50,4 +50,5 @@ dependencies {
implementation(libs.androidx.constraintlayout)
testImplementation(libs.junit)
testImplementation(libs.kotlin.coroutines.test)
implementation("io.coil-kt:coil:2.4.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
33 changes: 24 additions & 9 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 @@ -17,30 +19,43 @@ class CookbookFragment : Fragment() {
private val binding = FragmentBindingDelegate<FragmentCookbookBinding>(this)
private val model: CookbookFragmentViewModel by viewModels { CookbookFragmentViewModel.Factory }

private var adapter: RecipeAdapter? = null

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View = binding.bind(
container,
FragmentCookbookBinding::inflate
container, FragmentCookbookBinding::inflate
)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.withBinding {
btnClose.setOnClickListener {
requireActivity().finish()
}
}
setupRecyclerView()
viewLifecycleOwner.lifecycleScope.launch {
model.recipeList
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
model.recipeList.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.collect(::onRecipeListUpdated)
}
}

private fun setupRecyclerView() = binding.withBinding {
// Setup RecyclerView
adapter = RecipeAdapter { recipeId ->
val action = CookbookFragmentDirections.actionCookbookFragmentToRecipeFragment(recipeId)
findNavController().navigate(action)
}
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = adapter
}

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

override fun onDestroyView() {
super.onDestroyView()
adapter = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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() {

private val args: DeleteRecipeDialogFragmentArgs by navArgs()

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext()).setTitle("Удалить рецепт?")
.setMessage("Вы уверены, что хотите удалить рецепт «${args.recipeTitle}»?")
.setPositiveButton("Удалить") { _, _ ->
findNavController().getBackStackEntry(R.id.recipeFragment).savedStateHandle["DELETE_CONFIRMED"] =
true
}.setNegativeButton("Отмена", null).create()
}
}
76 changes: 76 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,76 @@
package ru.otus.cookbook.ui

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
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 onRecipeClick: (Int) -> Unit) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {

var items: List<RecipeListItem> = emptyList()
set(value) {
field = value
notifyDataSetChanged()
}

companion object {
private const val TYPE_CATEGORY = 0
private const val TYPE_ITEM = 1
}

override fun getItemViewType(position: Int): Int {
return when (items[position]) {
is RecipeListItem.CategoryItem -> TYPE_CATEGORY
is RecipeListItem.RecipeItem -> TYPE_ITEM
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return if (viewType == TYPE_CATEGORY) {
CategoryViewHolder(VhRecipeCategoryBinding.inflate(inflater, parent, false))
} else {
RecipeViewHolder(VhRecipeItemBinding.inflate(inflater, parent, false))
}
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = items[position]
when (holder) {
is CategoryViewHolder -> holder.bind(item as RecipeListItem.CategoryItem)
is RecipeViewHolder -> {
val recipeItem = item as RecipeListItem.RecipeItem
holder.bind(recipeItem)
holder.itemView.setOnClickListener { onRecipeClick(recipeItem.id) }
}
}
}

override fun getItemCount(): Int = items.size

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

class RecipeViewHolder(private val binding: VhRecipeItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: RecipeListItem.RecipeItem) {
binding.recipeTitle.text = item.title
binding.recipeDescription.text = item.description
binding.recipeImageThumbnail.load(item.imageUrl) {
transformations()
crossfade(true)
placeholder(R.drawable.ic_placeholder_shapes)
error(R.drawable.ic_placeholder_shapes)
}
}
}
}
80 changes: 50 additions & 30 deletions app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,54 +9,74 @@ 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 kotlinx.coroutines.launch
import ru.otus.cookbook.data.Recipe
import ru.otus.cookbook.databinding.FragmentRecipeBinding
import coil.load
import ru.otus.cookbook.R

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 = {
MutableCreationExtras(defaultViewModelCreationExtras).apply {
set(RecipeFragmentViewModel.RECIPE_ID_KEY, recipeId)
}
},
factoryProducer = { RecipeFragmentViewModel.Factory }
)
private val model: RecipeFragmentViewModel by viewModels(extrasProducer = {
MutableCreationExtras(defaultViewModelCreationExtras).apply {
set(RecipeFragmentViewModel.RECIPE_ID_KEY, recipeId)
}
}, factoryProducer = { RecipeFragmentViewModel.Factory })

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = binding.bind(
container,
FragmentRecipeBinding::inflate
)
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View = binding.bind(container, FragmentRecipeBinding::inflate)

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

viewLifecycleOwner.lifecycleScope.launch {
model.recipe
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.collect(::displayRecipe)
model.recipe.flowWithLifecycle(viewLifecycleOwner.lifecycle).collect(::displayRecipe)
}
}

/**
* Use to get recipe title and pass to confirmation dialog
*/
private fun getTitle(): String {
return model.recipe.value.title
}
val navBackStackEntry = findNavController().getBackStackEntry(R.id.recipeFragment)
navBackStackEntry.savedStateHandle.getLiveData<Boolean>("DELETE_CONFIRMED")
.observe(viewLifecycleOwner) { isConfirmed ->
if (isConfirmed == true) {
navBackStackEntry.savedStateHandle.remove<Boolean>("DELETE_CONFIRMED")
model.delete()
if (!findNavController().popBackStack(R.id.cookbookFragment, false)) {
findNavController().popBackStack()
}
}
}

private fun displayRecipe(recipe: Recipe) {
// Display the recipe
binding.withBinding {
btnDelete.setOnClickListener {
val action =
RecipeFragmentDirections.actionRecipeFragmentToDeleteRecipeDialogFragment(
getTitle()
)
findNavController().navigate(action)
}
btnBack.setOnClickListener {
findNavController().navigateUp()
}
}
}

private fun deleteRecipe() {
model.delete()
private fun displayRecipe(recipe: Recipe) = binding.withBinding {
toolbarRecipeTitle.text = recipe.title
recipeTitle.text = recipe.title
recipeSubhead.text = recipe.description
recipeDescription.text = recipe.steps.joinToString("\n\n") { "• $it" }
recipeImage.load(recipe.imageUrl) {
crossfade(true)
placeholder(R.drawable.ic_placeholder_cookbook)
error(R.drawable.ic_placeholder_cookbook)
}
}

private fun getTitle(): String = model.recipe.value.title
}
6 changes: 6 additions & 0 deletions app/src/main/res/anim/slide_in_left.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="-100%"
android:toXDelta="0%" />
</set>
6 changes: 6 additions & 0 deletions app/src/main/res/anim/slide_in_right.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="100%"
android:toXDelta="0%" />
</set>
6 changes: 6 additions & 0 deletions app/src/main/res/anim/slide_out_left.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="0%"
android:toXDelta="-100%" />
</set>
6 changes: 6 additions & 0 deletions app/src/main/res/anim/slide_out_right.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="0%"
android:toXDelta="100%" />
</set>
9 changes: 9 additions & 0 deletions app/src/main/res/drawable/ic_back.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.825,13L13.425,18.6L12,20L4,12L12,4L13.425,5.4L7.825,11H20V13H7.825Z"
android:fillColor="#1D1B20"/>
</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="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#1D1B20"
android:pathData="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/>
</vector>
9 changes: 9 additions & 0 deletions app/src/main/res/drawable/ic_delete.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="#49454F"/>
</vector>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions app/src/main/res/drawable/ic_search.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="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M16.6,18L10.3,11.7C9.8,12.1 9.225,12.417 8.575,12.65C7.925,12.883 7.233,13 6.5,13C4.683,13 3.146,12.371 1.888,11.113C0.629,9.854 0,8.317 0,6.5C0,4.683 0.629,3.146 1.888,1.888C3.146,0.629 4.683,0 6.5,0C8.317,0 9.854,0.629 11.113,1.888C12.371,3.146 13,4.683 13,6.5C13,7.233 12.883,7.925 12.65,8.575C12.417,9.225 12.1,9.8 11.7,10.3L18,16.6L16.6,18ZM6.5,11C7.75,11 8.813,10.563 9.688,9.688C10.563,8.813 11,7.75 11,6.5C11,5.25 10.563,4.188 9.688,3.313C8.813,2.438 7.75,2 6.5,2C5.25,2 4.188,2.438 3.313,3.313C2.438,4.188 2,5.25 2,6.5C2,7.75 2.438,8.813 3.313,9.688C4.188,10.563 5.25,11 6.5,11Z"
android:fillColor="#49454F"/>
</vector>
15 changes: 7 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,12 @@
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/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation" />

</androidx.constraintlayout.widget.ConstraintLayout>
Loading