Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
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 1 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 2 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 33 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 76 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 80 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 6 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 6 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 6 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 6 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 9 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 9 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 9 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 9 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 15 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
Morty Proxy This is a proxified and sanitized view of the page, visit original site.