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
14 changes: 14 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/data/RecipeListItem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import ru.otus.cookbook.R
* Recipe list items.
*/
sealed class RecipeListItem : WithLayoutId {

// See RecipeAdapter.kt
abstract fun isSame(other: RecipeListItem): Boolean
abstract fun isContentSame(other: RecipeListItem): Boolean

/**
* Recipe item.
*/
Expand All @@ -20,6 +25,12 @@ sealed class RecipeListItem : WithLayoutId {
val title: String get() = recipe.title
val description: String get() = recipe.description
val imageUrl: String get() = recipe.imageUrl

override fun isSame(other: RecipeListItem): Boolean = id == (other as? RecipeItem)?.id
override fun isContentSame(other: RecipeListItem): Boolean {
if (other !is RecipeItem) return false
return title == other.title && description == other.description && imageUrl == other.imageUrl
}
}

/**
Expand All @@ -32,5 +43,8 @@ sealed class RecipeListItem : WithLayoutId {
}

val name: String get() = category.name

override fun isSame(other: RecipeListItem): Boolean = name == (other as? CategoryItem)?.name
override fun isContentSame(other: RecipeListItem): Boolean = name == (other as? CategoryItem)?.name
}
}
17 changes: 15 additions & 2 deletions app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package ru.otus.cookbook.ui

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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,13 @@ class CookbookFragment : Fragment() {
private val binding = FragmentBindingDelegate<FragmentCookbookBinding>(this)
private val model: CookbookFragmentViewModel by viewModels { CookbookFragmentViewModel.Factory }

private val adapter = RecipeAdapter { id ->
Log.d(TAG, "Recipe clicked: $id")
val navController = findNavController()
val action = CookbookFragmentDirections.actionCookbookFragmentToRecipeFragment(id)
navController.navigate(action)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
Expand All @@ -37,10 +46,14 @@ class CookbookFragment : Fragment() {
}

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

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

companion object {
const val TAG = "CookbookFragment"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ru.otus.cookbook.ui

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.navigation.fragment.findNavController
import ru.otus.cookbook.R
import ru.otus.cookbook.databinding.FragmentConfirmationBinding

class DeleteConfirmationFragment : DialogFragment() {
private val binding = FragmentBindingDelegate<FragmentConfirmationBinding>(this)

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

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.withBinding {
text.text = getString(R.string.text_delete, DeleteConfirmationFragmentArgs.fromBundle(requireArguments()).title)
ok.setOnClickListener {
setResult(true)
}
cancel.setOnClickListener {
setResult(false)
}
}
}

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

companion object {
const val CONFIRMATION_RESULT = "confirmation_result"
}
}
68 changes: 68 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,68 @@
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 ru.otus.cookbook.data.RecipeListItem
import ru.otus.cookbook.databinding.VhRecipeCategoryBinding
import ru.otus.cookbook.databinding.VhRecipeItemBinding
import java.util.Locale

/**
* Recipe adapter
*/
class RecipeAdapter(private val onClick: (Int) -> Unit) : ListAdapter<RecipeListItem, RecipeAdapter.RecipeViewHolder>(RecipeDiff) {

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

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when(viewType) {
RecipeListItem.RecipeItem.layoutId -> RecipeViewHolder.RecipeHolder(
VhRecipeItemBinding.inflate(LayoutInflater.from(parent.context), parent, false),
onClick
)
RecipeListItem.CategoryItem.layoutId -> RecipeViewHolder.CategoryHolder(
VhRecipeCategoryBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
else -> throw IllegalArgumentException("Unknown view type: $viewType")
}

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

sealed class RecipeViewHolder(view: View): ViewHolder(view) {

class RecipeHolder(private val binding: VhRecipeItemBinding, private val onClick: (Int) -> Unit) : RecipeViewHolder(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
id.text = String.format(Locale.getDefault(), "%02d", recipe.id)
title.text = recipe.title
}
}

class CategoryHolder(private val binding: VhRecipeCategoryBinding) : RecipeViewHolder(binding.root) {
fun bind(category: RecipeListItem.CategoryItem) = with(binding) {
name.text = category.name
}
}
}
}

private object RecipeDiff : DiffUtil.ItemCallback<RecipeListItem>() {
override fun areItemsTheSame(oldItem: RecipeListItem, newItem: RecipeListItem): Boolean = oldItem.isSame(newItem)
override fun areContentsTheSame(oldItem: RecipeListItem, newItem: RecipeListItem): Boolean = oldItem.isContentSame(newItem)
}
71 changes: 67 additions & 4 deletions app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
package ru.otus.cookbook.ui

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.navigation.fragment.findNavController
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(
Expand All @@ -38,6 +43,8 @@ class RecipeFragment : Fragment() {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupAppBar()
setupAlertResult()
viewLifecycleOwner.lifecycleScope.launch {
model.recipe
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
Expand All @@ -52,11 +59,67 @@ class RecipeFragment : Fragment() {
return model.recipe.value.title
}

private fun displayRecipe(recipe: Recipe) {
// Display the recipe
private fun setupAppBar() = binding.withBinding {
topAppBar.setNavigationOnClickListener {
close()
}
topAppBar.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.action_delete -> {
deleteRecipe()
true
}
else -> false
}
}
}

private fun close() {
findNavController().popBackStack()
}

/**
* Sets up alert dialog for delete result.
* https://developer.android.com/guide/navigation/use-graph/programmatic#returning_a_result
*/
private fun setupAlertResult() {
val navBackStackEntry = findNavController().getBackStackEntry(R.id.recipeFragment)

val observer = object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
if (navBackStackEntry.savedStateHandle.contains(DeleteConfirmationFragment.CONFIRMATION_RESULT)) {
if (true == navBackStackEntry.savedStateHandle.get<Boolean>(DeleteConfirmationFragment.CONFIRMATION_RESULT)) {
Log.d(TAG, "Deleting recipe $recipeId")
model.delete()
close()
}
navBackStackEntry.savedStateHandle.remove<Boolean>(DeleteConfirmationFragment.CONFIRMATION_RESULT)
}
}
}

navBackStackEntry.lifecycle.addObserver(observer)

viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}

private fun displayRecipe(recipe: Recipe) = binding.withBinding {
title.text = recipe.title
steps.text = recipe.steps.joinToString("\n")
}

private fun deleteRecipe() {
model.delete()
Log.d(TAG, "Deleting recipe $recipeId")
findNavController().navigate(
RecipeFragmentDirections.actionRecipeFragmentToDeleteConfirmation(getTitle())
)
}

companion object {
private const val TAG = "RecipeFragment"
}
}
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/ic_add.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="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>

</vector>
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/ic_back.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:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>

</vector>
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/ic_delete.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="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>

</vector>
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/ic_search.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="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>

</vector>
25 changes: 10 additions & 15 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
android:layout_height="match_parent">

<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/navigation"
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>
</FrameLayout>
Loading