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
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.recyclerview)
implementation(libs.glide)
testImplementation(libs.junit)
testImplementation(libs.kotlin.coroutines.test)
}
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
18 changes: 16 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 @@ -8,6 +8,7 @@ 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 +18,13 @@ class CookbookFragment : Fragment() {
private val binding = FragmentBindingDelegate<FragmentCookbookBinding>(this)
private val model: CookbookFragmentViewModel by viewModels { CookbookFragmentViewModel.Factory }

// Адаптер для списка рецептов
private val adapter = RecipeListAdapter { recipeId ->
// Навигация к экрану детальной информации о рецепте с использованием Safe Args
val action = CookbookFragmentDirections.actionCookbookToRecipe(recipeId)
findNavController().navigate(action)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
Expand All @@ -36,11 +44,17 @@ class CookbookFragment : Fragment() {
}
}

/**
* Настройка RecyclerView: подключение адаптера и LayoutManager
*/
private fun setupRecyclerView() = binding.withBinding {
// Setup RecyclerView
recipes.adapter = adapter
}

/**
* Обновление списка рецептов в адаптере при изменении данных
*/
private fun onRecipeListUpdated(recipeList: List<RecipeListItem>) {
// Handle recipe list
adapter.submitList(recipeList)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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 DeleteRecipeDialogFragment : DialogFragment() {

companion object {
const val CONFIRMATION_RESULT = "confirmation_result"
}

// Получение названия рецепта через Safe Args
private val recipeTitle: String get() = DeleteRecipeDialogFragmentArgs.fromBundle(requireArguments()).recipeTitle

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
AlertDialog.Builder(requireContext())
.setTitle(R.string.delete)
.setMessage(getString(R.string.delete_recipe_confirmation, recipeTitle))
.setPositiveButton(android.R.string.ok) { _, _ ->
dismiss()
setResult(true)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
dismiss()
setResult(false)
}
.create()

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

152 changes: 142 additions & 10 deletions app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
package ru.otus.cookbook.ui

import android.annotation.SuppressLint
import android.os.Bundle
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 ru.otus.cookbook.R
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import kotlinx.coroutines.launch
import ru.otus.cookbook.data.Recipe
import ru.otus.cookbook.databinding.FragmentRecipeBinding
import androidx.activity.OnBackPressedCallback

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")
// Получение ID рецепта через SafeArgs
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 {
set(RecipeFragmentViewModel.RECIPE_ID_KEY, recipeId)
}
androidx.lifecycle.viewmodel.MutableCreationExtras(defaultViewModelCreationExtras)
.apply {
set(RecipeFragmentViewModel.RECIPE_ID_KEY, recipeId)
}
},
factoryProducer = { RecipeFragmentViewModel.Factory }
)
Expand All @@ -38,25 +49,146 @@ class RecipeFragment : Fragment() {

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

/**
* Use to get recipe title and pass to confirmation dialog
*/
private fun setupToolbar() = binding.withBinding {
toolbar.setupWithNavController(findNavController())
}

private fun setupDeleteButton() = binding.withBinding {
deleteButton.setOnClickListener {
// Навигация в диалог подтверждения удаления с передачей названия рецепта
val action = RecipeFragmentDirections.actionRecipeToDeleteDialog(getTitle())
findNavController().navigate(action)
}
}

private fun setupDeleteResult() {
val navBackStackEntry = findNavController().getBackStackEntry(R.id.recipeFragment)

val observer = object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
// Проверка результата подтверждения удаления
if (navBackStackEntry.savedStateHandle.contains(DeleteRecipeDialogFragment.CONFIRMATION_RESULT)) {
val confirmed =
navBackStackEntry.savedStateHandle.get<Boolean>(DeleteRecipeDialogFragment.CONFIRMATION_RESULT) == true

if (confirmed) {
deleteRecipe()
// Возврат к списку рецептов после удаления
findNavController().popBackStack()
}

// Удаление результата из SavedStateHandle
navBackStackEntry.savedStateHandle.remove<Boolean>(
DeleteRecipeDialogFragment.CONFIRMATION_RESULT
)
}
}
}

navBackStackEntry.lifecycle.addObserver(observer)

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

private fun setupBackPressHandler() {
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Возвращаемся к списку рецептов
findNavController().popBackStack()
}
})
}

private fun getTitle(): String {
return model.recipe.value.title
}

private fun displayRecipe(recipe: Recipe) {
// Display the recipe
private fun displayRecipe(recipe: Recipe) = binding.withBinding {
// Заголовок рецепта
recipeTitle.text = recipe.title

// Категория рецепта
recipeCategory.text = recipe.category.name

// Загрузка изображения через Glide
Glide.with(this@RecipeFragment)
.load(recipe.imageUrl)
.into(recipeImage)

recipeImage.setImageResource(android.R.drawable.ic_menu_gallery)

// Описание рецепта
recipeDescription.text = recipe.description

// Настройка списка шагов приготовления
stepsLabel.text = getString(R.string.title_recipe_steps)
stepsRecycler.layoutManager = LinearLayoutManager(requireContext())
stepsRecycler.adapter = StepsAdapter(recipe.steps)
}

private fun deleteRecipe() {
model.delete()
}
}

private class StepsAdapter(private val steps: List<String>) :
RecyclerView.Adapter<StepsAdapter.StepViewHolder>() {

@SuppressLint("ResourceType")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StepViewHolder {
val view = com.google.android.material.textview.MaterialTextView(parent.context).apply {
layoutParams = ViewGroup.MarginLayoutParams(
ViewGroup.MarginLayoutParams.MATCH_PARENT,
ViewGroup.MarginLayoutParams.WRAP_CONTENT
).apply {
val margin = parent.context.resources.getDimensionPixelSize(
ru.otus.cookbook.R.dimen.margin_small
)
setMargins(margin, margin, margin, margin)
}

setTextAppearance(
parent.context,
com.google.android.material.R.attr.textAppearanceBody1
)
setPadding(
parent.context.resources.getDimensionPixelSize(ru.otus.cookbook.R.dimen.margin),
0,
parent.context.resources.getDimensionPixelSize(ru.otus.cookbook.R.dimen.margin),
0
)
}
return StepViewHolder(view)
}

override fun onBindViewHolder(holder: StepViewHolder, position: Int) {
holder.bind(steps[position], position + 1)
}

override fun getItemCount(): Int = steps.size

class StepViewHolder(private val textView: com.google.android.material.textview.MaterialTextView) :
RecyclerView.ViewHolder(textView) {
@SuppressLint("SetTextI18n")
fun bind(step: String, number: Int) {
textView.text = "$number. $step"
}
}
}
Loading