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)
}
4 changes: 3 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
<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"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package ru.otus.cookbook

import android.os.Bundle
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import ru.otus.cookbook.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
Expand All @@ -12,5 +14,10 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
onBackPressedDispatcher.addCallback(this) {
if (!findNavController(R.id.fragment_main).popBackStack()) {
finish()
}
}
}
}
103 changes: 103 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,103 @@
package ru.otus.cookbook.ui

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


class CookbookAdapter(
private val onRecipeClick: (Int) -> Unit
) :
ListAdapter<RecipeListItem, CookbookAdapter.CookbookViewHolder>(RecipeDiff) {

override fun getItemViewType(position: Int): Int {
return getItem(position).layoutId
}


override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = when (viewType) {
RecipeListItem.CategoryItem.layoutId -> CookbookViewHolder.CategoryHolder(
VhRecipeCategoryBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)

RecipeListItem.RecipeItem.layoutId -> CookbookViewHolder.RecipeHolder(
VhRecipeItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
), onRecipeClick
)

else -> throw IllegalArgumentException("View is not supported")
}

override fun onBindViewHolder(holder: CookbookViewHolder, position: Int) {
when (holder) {
is CookbookViewHolder.CategoryHolder -> holder.bind(getItem(position) as RecipeListItem.CategoryItem)
is CookbookViewHolder.RecipeHolder -> holder.bind(getItem(position) as RecipeListItem.RecipeItem)
}
}

sealed class CookbookViewHolder(view: View) : ViewHolder(view) {
class RecipeHolder(
private val binding: VhRecipeItemBinding,
private val onClick: (Int) -> Unit
) : CookbookViewHolder(binding.root) {
private var id: Int = -1

init {
binding.root.setOnClickListener { onClick(id) }
}

fun bind(recipe: RecipeListItem.RecipeItem) = with(binding) {
this@RecipeHolder.id = recipe.id
txtRecipeAvatar.text = recipe.title[0].uppercase()
txtRecipeName.text = recipe.title
txtRecipeDescription.text = recipe.description
Glide.with(root.context).load(recipe.imageUrl).centerCrop().into(imgRecipeImage)
}

}

class CategoryHolder(
private val binding: VhRecipeCategoryBinding
) :
CookbookViewHolder(binding.root) {

fun bind(category: RecipeListItem.CategoryItem) = with(binding) {
txtCategoryName.text = category.name
}
}
}

}

private object RecipeDiff : 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.category == newItem.category
else -> false
}
}

override fun areContentsTheSame(oldItem: RecipeListItem, newItem: RecipeListItem): Boolean {
return oldItem == newItem
}
}


24 changes: 22 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 @@ -4,10 +4,12 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.launch
import ru.otus.cookbook.data.RecipeListItem
import ru.otus.cookbook.databinding.FragmentCookbookBinding
Expand All @@ -17,6 +19,14 @@ class CookbookFragment : Fragment() {
private val binding = FragmentBindingDelegate<FragmentCookbookBinding>(this)
private val model: CookbookFragmentViewModel by viewModels { CookbookFragmentViewModel.Factory }

private val adapter = CookbookAdapter(
{ id ->
val navController = findNavController()
val action = CookbookFragmentDirections.actionCookbookToRecipe(id)
navController.navigate(action)
}
)

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
Expand All @@ -29,18 +39,28 @@ class CookbookFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
setupAppBar()
viewLifecycleOwner.lifecycleScope.launch {
model.recipeList
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.collect(::onRecipeListUpdated)
}
}

private fun setupAppBar() {
binding.withBinding {
cookbookToolbar.setNavigationOnClickListener {
Toast.makeText(requireContext(), "Close application?", Toast.LENGTH_LONG).show()
}
}
}


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

private fun onRecipeListUpdated(recipeList: List<RecipeListItem>) {
// Handle recipe list
adapter.submitList(recipeList)
}
}
40 changes: 40 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/ui/DialogFragmentDelete.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package ru.otus.cookbook.ui

import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.navigation.fragment.findNavController
import ru.otus.cookbook.R

class DialogFragmentDelete : DialogFragment() {
private val title: String get() = DialogFragmentDeleteArgs.fromBundle(requireArguments()).title

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
requireContext().let {
val navController = findNavController()
AlertDialog.Builder(it)

.setTitle(getString(R.string.delete_dialog_title, title))
.setMessage(getString(R.string.delete_dialog_message, title))
.setPositiveButton(getString(R.string.delete_dialog_positive_button)) { _, _ ->
navController.previousBackStackEntry?.savedStateHandle?.set(
DIALOG_RESULT_KEY,
true
)
}
.setNegativeButton(getString(R.string.delete_dialog_negative_button)) { _, _ ->
navController.previousBackStackEntry?.savedStateHandle?.set(
DIALOG_RESULT_KEY,
false
)
}
.create()
}

companion object {
const val DIALOG_RESULT_KEY = "deleteResult"
}

}

50 changes: 47 additions & 3 deletions app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +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 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 recipeId: Int get() = RecipeFragmentArgs.fromBundle(requireArguments()).recipeId
private val binding = FragmentBindingDelegate<FragmentRecipeBinding>(this)

private val model: RecipeFragmentViewModel by viewModels(
extrasProducer = {
MutableCreationExtras(defaultViewModelCreationExtras).apply {
Expand All @@ -27,6 +30,7 @@ class RecipeFragment : Fragment() {
factoryProducer = { RecipeFragmentViewModel.Factory }
)


override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
Expand All @@ -38,25 +42,65 @@ class RecipeFragment : Fragment() {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupAppBar()
setupDialogObserver()
viewLifecycleOwner.lifecycleScope.launch {
model.recipe
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.collect(::displayRecipe)
}
}

private fun setupDialogObserver() {
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<Boolean>(
DialogFragmentDelete.DIALOG_RESULT_KEY
)?.observe(viewLifecycleOwner) { result ->
if (result) {
deleteRecipe()
}
}
}

/**
* Use to get recipe title and pass to confirmation dialog
*/
private fun getTitle(): String {
return model.recipe.value.title
}

private fun setupAppBar() {
binding.withBinding {
recipeToolbar.setNavigationOnClickListener {
findNavController().navigate(R.id.action_recipe_to_cookbook)
}
recipeToolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.delete_recipe -> {
val navController = findNavController()
val action = RecipeFragmentDirections.actionRecipeToDialogDelete(getTitle())
navController.navigate(action)
true
}

else -> false
}
}
}
}

private fun displayRecipe(recipe: Recipe) {
// Display the recipe
binding.withBinding {
txtRecipeName.text = recipe.title
txtRecipeDescription.text = recipe.description
txtRecipeSteps.text =
recipe.steps.mapIndexed { index, step -> "${index + 1}. $step" }.joinToString("\n")
Glide.with(root.context).load(recipe.imageUrl).into(imgRecipeImageBig)
recipeToolbar.title = recipe.title
}
}

private fun deleteRecipe() {
model.delete()
findNavController().navigate(R.id.action_dialog_delete_to_cookbook)
}
}
7 changes: 7 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,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_shortAnimTime"
android:fromXDelta="-100%"
android:toXDelta="0%" />
</set>
7 changes: 7 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,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_shortAnimTime"
android:fromXDelta="100%"
android:toXDelta="0%" />
</set>
7 changes: 7 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,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_shortAnimTime"
android:fromXDelta="0%"
android:toXDelta="-100%" />
</set>
7 changes: 7 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,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_shortAnimTime"
android:fromXDelta="100%"
android:toXDelta="0%" />
</set>
9 changes: 9 additions & 0 deletions app/src/main/res/drawable/arrow_back_24dp.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="960"
android:viewportHeight="960">
<path
android:pathData="m313,520 l224,224 -57,56 -320,-320 320,-320 57,56 -224,224h487v80L313,520Z"
android:fillColor="?attr/colorOnSurface"/>
</vector>
Loading