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.coil)
testImplementation(libs.junit)
testImplementation(libs.kotlin.coroutines.test)
}
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-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
15 changes: 15 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/CategoryViewHolder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ru.otus.cookbook

import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import ru.otus.cookbook.data.RecipeListItem

class CategoryViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {

private val name: TextView by lazy { view.findViewById(R.id.textView) }

fun bind(item: RecipeListItem.CategoryItem) {
name.text = item.name
}
}
55 changes: 55 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/CookbookAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package ru.otus.cookbook

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import ru.otus.cookbook.data.RecipeListItem

class CookbookAdapter(private val listener: Listener): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var list = listOf<RecipeListItem>()

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ViewTypes.CATEGORY.id -> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.vh_recipe_category, parent, false)
CategoryViewHolder(view)
}
ViewTypes.RECIPE.id -> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.vh_recipe_item, parent, false)
RecipeViewHolder(view, listener)
}
else -> throw IllegalArgumentException("Not found view type for chat adapter")
}
}

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

override fun getItemCount(): Int = list.size

override fun getItemViewType(position: Int): Int {
return when (list[position]) {
is RecipeListItem.CategoryItem -> ViewTypes.CATEGORY.id
is RecipeListItem.RecipeItem -> ViewTypes.RECIPE.id
}
}

fun setItems(items: List<RecipeListItem>) {
list = items
notifyDataSetChanged()
}

enum class ViewTypes(val id: Int) {
CATEGORY(R.layout.vh_recipe_category),
RECIPE(R.layout.vh_recipe_item)
}
}
5 changes: 5 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/Listener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ru.otus.cookbook

interface Listener {
fun onItemClicked(id: Int)
}
39 changes: 39 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,55 @@
package ru.otus.cookbook

import android.os.Bundle
import android.view.Menu
import android.widget.ImageButton
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import com.google.android.material.appbar.MaterialToolbar
import ru.otus.cookbook.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

val fragmentsWithButton = setOf(
R.id.fragment_recipe,
)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

val toolbar = findViewById<MaterialToolbar>(R.id.topAppBar)
setSupportActionBar(toolbar)

val navHostFragment = supportFragmentManager
.findFragmentById(R.id.fragment_container_view) as NavHostFragment
val navController = navHostFragment.navController

val appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)

onBackPressedDispatcher.addCallback(this) {
if (!findNavController(R.id.fragment_container_view).popBackStack()) {
finish()
}
}

navController.addOnDestinationChangedListener { _, destination, _ ->
val shouldShow = fragmentsWithButton.contains(destination.id)
toolbar.findViewById<ImageButton>(R.id.deleteButton)?.visibility =
if (shouldShow) ImageButton.VISIBLE else ImageButton.GONE
}
}

override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.fragment_container_view)
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
23 changes: 23 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/RecipeViewHolder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ru.otus.cookbook

import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import ru.otus.cookbook.data.RecipeListItem

class RecipeViewHolder(
private val view: View,
private val listener: Listener): RecyclerView.ViewHolder(view) {

private val twName: TextView by lazy { view.findViewById(R.id.tw_name) }
private val twDescription: TextView by lazy { view.findViewById(R.id.tw_description) }

fun bind(item: RecipeListItem.RecipeItem) {
twName.text = item.title
twDescription.text = item.description

view.setOnClickListener {
listener.onItemClicked(item.id)
}
}
}
23 changes: 20 additions & 3 deletions app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,23 @@ 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.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.appbar.MaterialToolbar
import kotlinx.coroutines.launch
import ru.otus.cookbook.CookbookAdapter
import ru.otus.cookbook.Listener
import ru.otus.cookbook.R
import ru.otus.cookbook.data.RecipeListItem
import ru.otus.cookbook.databinding.FragmentCookbookBinding

class CookbookFragment : Fragment() {
class CookbookFragment : Fragment(), Listener {

private val binding = FragmentBindingDelegate<FragmentCookbookBinding>(this)
private val model: CookbookFragmentViewModel by viewModels { CookbookFragmentViewModel.Factory }
private val cookbookAdapter = CookbookAdapter(this@CookbookFragment)


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

private fun setupRecyclerView() = binding.withBinding {
// Setup RecyclerView
val dividerItemDecoration = DividerItemDecoration(context, LinearLayoutManager.VERTICAL)
recyclerView.addItemDecoration(dividerItemDecoration)
recyclerView.adapter = cookbookAdapter
}

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

override fun onItemClicked(id: Int) {
findNavController().navigate(CookbookFragmentDirections.actionCookbookToRecipe(
recipeID = id
))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package ru.otus.cookbook.ui

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

class RecipeDeleteDialogFragment : DialogFragment() {
private val title get() = RecipeDeleteDialogFragmentArgs.fromBundle(requireArguments()).title

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
requireContext().let {
AlertDialog.Builder(it, R.style.RoundedDialog)
.setTitle(R.string.rdf_title)
.setMessage(getString(R.string.rdf_message, title))
.setPositiveButton(R.string.rdf_ok) { _, _ ->
findNavController().previousBackStackEntry?.savedStateHandle?.set(RESULT, 1)
}
.setNeutralButton(R.string.rdf_cancel) { dialog, _ ->
dialog.dismiss()
}
.create()
}

companion object {
const val RESULT = "result"
}
}
48 changes: 42 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 @@ -2,20 +2,27 @@ package ru.otus.cookbook.ui

import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
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.data.Recipe
import ru.otus.cookbook.databinding.FragmentRecipeBinding
import coil.load
import com.google.android.material.appbar.MaterialToolbar
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 recipeId: Int get() = RecipeFragmentArgs.fromBundle(requireArguments()).recipeID

private val binding = FragmentBindingDelegate<FragmentRecipeBinding>(this)
private val model: RecipeFragmentViewModel by viewModels(
Expand All @@ -26,6 +33,14 @@ class RecipeFragment : Fragment() {
},
factoryProducer = { RecipeFragmentViewModel.Factory }
)
private val navigationController by lazy { findNavController() }
private val toolbar by lazy { activity?.findViewById<MaterialToolbar>(R.id.topAppBar) }
private val deleteButton by lazy { toolbar?.findViewById<ImageButton>(R.id.deleteButton) }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}

override fun onCreateView(
inflater: LayoutInflater,
Expand All @@ -43,20 +58,41 @@ class RecipeFragment : Fragment() {
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.collect(::displayRecipe)
}

displayRecipe(model.recipe.value)

navigationController.currentBackStackEntry?.savedStateHandle?.getLiveData<Int>(
RecipeDeleteDialogFragment.RESULT)
?.observe(viewLifecycleOwner) {
if (it == 1) {
deleteRecipe()
}
}


toolbar?.title = getTitle()
deleteButton?.setOnClickListener {
navigationController.navigate(RecipeFragmentDirections.actionRecipeToDialog(
getTitle()))
}
}

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

private fun displayRecipe(recipe: Recipe) {
// Display the recipe
binding.withBinding {
twName.text = recipe.title
twDescription.text = recipe.description
imageView.load(recipe.imageUrl) {
crossfade(true)
}
}
}

private fun deleteRecipe() {
model.delete()
findNavController().popBackStack(R.id.fragment_cookbook, false)
}
}
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="300"
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="300"
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="300"
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="300"
android:fromXDelta="0%"
android:toXDelta="100%"/>
</set>
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/dialog_background.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/design_default_color_background"/>
<corners android:radius="28dp"/>
</shape>
Loading