diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f3207a6..dd31abf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.kotlin.dsl.implementation + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -50,4 +52,7 @@ dependencies { implementation(libs.androidx.constraintlayout) testImplementation(libs.junit) testImplementation(libs.kotlin.coroutines.test) + + implementation("io.coil-kt.coil3:coil-network-okhttp:3.3.0") + implementation("io.coil-kt.coil3:coil:3.3.0") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 063f4d1..682aa63 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + setResult(true) + } + .setNegativeButton("Нет") { _, _ -> + setResult(false) + } + .create() + + private fun setResult(result: Boolean) { + findNavController().previousBackStackEntry?.savedStateHandle?.set(RESULT_KEY, result) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/MainActivity.kt b/app/src/main/kotlin/ru/otus/cookbook/MainActivity.kt index 6e524b6..ce6e1ec 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/MainActivity.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/MainActivity.kt @@ -2,6 +2,9 @@ package ru.otus.cookbook import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.setupWithNavController import ru.otus.cookbook.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { @@ -12,5 +15,13 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + + setSupportActionBar(binding.toolbar) + + val navHost = + supportFragmentManager.findFragmentById(R.id.cookbook_fragment_container) as NavHostFragment + val navController = navHost.navController + val appConfiguration = AppBarConfiguration(navController.graph) + binding.toolbar.setupWithNavController(navController, appConfiguration) } } \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt index efe6939..2ad5bd2 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt @@ -8,15 +8,18 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +import androidx.navigation.findNavController import kotlinx.coroutines.launch import ru.otus.cookbook.data.RecipeListItem import ru.otus.cookbook.databinding.FragmentCookbookBinding -class CookbookFragment : Fragment() { +class CookbookFragment : Fragment(), RecipeItemListener { private val binding = FragmentBindingDelegate(this) private val model: CookbookFragmentViewModel by viewModels { CookbookFragmentViewModel.Factory } + private val recipesAdapter by lazy { CookbookRecyclerViewAdapter(this) } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -38,9 +41,16 @@ class CookbookFragment : Fragment() { private fun setupRecyclerView() = binding.withBinding { // Setup RecyclerView + cookbookRecyclerView.adapter = recipesAdapter } private fun onRecipeListUpdated(recipeList: List) { // Handle recipe list + recipesAdapter.setList(recipeList) + } + + override fun onRecipeClick(recipeId: Int) { + val action = CookbookFragmentDirections.actionCookbookFragmentToRecipeFragment(recipeId) + view?.findNavController()?.navigate(action) } } \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookRecyclerViewAdapter.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookRecyclerViewAdapter.kt new file mode 100644 index 0000000..826dc7a --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookRecyclerViewAdapter.kt @@ -0,0 +1,67 @@ +package ru.otus.cookbook.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import ru.otus.cookbook.R +import ru.otus.cookbook.data.RecipeListItem + +class CookbookRecyclerViewAdapter( + val recipeListener: RecipeItemListener +) : RecyclerView.Adapter() { + + private var list = emptyList() + + fun setList(recipeList: List) { + list = recipeList + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + ViewType.CATEGORY.id -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.vh_recipe_category, parent, false) + CategoryItemViewHolder(view) + } + + ViewType.RECIPE.id -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.vh_recipe_item, parent, false) + RecipeItemViewHolder( + view = view, + recipeListener = recipeListener + ) + } + + else -> throw IllegalArgumentException("Unknown view type") + } + } + + override fun getItemViewType(position: Int): Int { + return when (list[position]) { + is RecipeListItem.CategoryItem -> ViewType.CATEGORY.id + is RecipeListItem.RecipeItem -> ViewType.RECIPE.id + } + } + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int + ) { + val item = list[position] + + when (getItemViewType(position)) { + ViewType.CATEGORY.id -> (holder as CategoryItemViewHolder).bind(item as RecipeListItem.CategoryItem) + ViewType.RECIPE.id -> (holder as RecipeItemViewHolder).bind(item as RecipeListItem.RecipeItem) + } + + } + + override fun getItemCount(): Int = list.size + + private enum class ViewType(val id: Int) { + CATEGORY(R.layout.vh_recipe_category), + RECIPE(R.layout.vh_recipe_item) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookViewHolders.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookViewHolders.kt new file mode 100644 index 0000000..0ca26a4 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookViewHolders.kt @@ -0,0 +1,50 @@ +package ru.otus.cookbook.ui + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.cardview.widget.CardView +import androidx.recyclerview.widget.RecyclerView +import ru.otus.cookbook.R +import ru.otus.cookbook.data.RecipeListItem +import coil3.load +import coil3.request.placeholder +import coil3.request.error + +interface RecipeItemListener { + fun onRecipeClick(recipeId: Int) +} + +class CategoryItemViewHolder(view: View) : RecyclerView.ViewHolder(view) { + private val name: TextView by lazy { view.findViewById(R.id.category_item_name) } + + fun bind(categoryItem: RecipeListItem.CategoryItem) { + name.text = categoryItem.name + } +} + +class RecipeItemViewHolder( + view: View, + val recipeListener: RecipeItemListener +) : RecyclerView.ViewHolder(view) { + private val letter: TextView by lazy { view.findViewById(R.id.recipe_letter) } + private val title: TextView by lazy { view.findViewById(R.id.recipe_item_title) } + private val description: TextView by lazy { view.findViewById(R.id.recipe_item_description) } + private val image: ImageView by lazy { view.findViewById(R.id.recipe_image) } + + private val recipeCard: CardView by lazy { view.findViewById(R.id.recipe_card) } + + fun bind(recipeItem: RecipeListItem.RecipeItem) { + letter.text = recipeItem.title[0].toString() + title.text = recipeItem.title + description.text = recipeItem.description + image.load(recipeItem.imageUrl) { + placeholder(R.drawable.ic_android_placeholder) + error(R.drawable.ic_android_placeholder) + } + + recipeCard.setOnClickListener { + recipeListener.onRecipeClick(recipeItem.id) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt index e4460c1..c8c9d25 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt @@ -2,20 +2,35 @@ 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 androidx.core.view.MenuProvider import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.MutableCreationExtras +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import coil3.load +import coil3.request.error +import coil3.request.placeholder import kotlinx.coroutines.launch +import ru.otus.cookbook.R import ru.otus.cookbook.data.Recipe import ru.otus.cookbook.databinding.FragmentRecipeBinding +const val RESULT_KEY = "result_key" + 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(this) private val model: RecipeFragmentViewModel by viewModels( @@ -43,6 +58,56 @@ class RecipeFragment : Fragment() { .flowWithLifecycle(viewLifecycleOwner.lifecycle) .collect(::displayRecipe) } + + requireActivity().addMenuProvider( + object : MenuProvider { + override fun onCreateMenu( + menu: Menu, + menuInflater: MenuInflater + ) { + menuInflater.inflate(R.menu.options_menu, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.delete_btn -> { + val action = + RecipeFragmentDirections.actionRecipeFragmentToConfirmationDialog( + getTitle() + ) + findNavController().navigate(action) + true + } + + else -> false + } + } + }, + viewLifecycleOwner, + Lifecycle.State.RESUMED + ) + + val navBackStackEntry = findNavController().getBackStackEntry(R.id.recipeFragment) + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME && navBackStackEntry.savedStateHandle.contains( + RESULT_KEY + ) + ) { + val result = navBackStackEntry.savedStateHandle.get(RESULT_KEY) + if (result == true) { + deleteRecipe() + findNavController().popBackStack() + } + + } + } + navBackStackEntry.lifecycle.addObserver(observer) + + viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_DESTROY) { + navBackStackEntry.lifecycle.removeObserver(observer) + } + }) } /** @@ -54,6 +119,15 @@ class RecipeFragment : Fragment() { private fun displayRecipe(recipe: Recipe) { // Display the recipe + binding.withBinding { + recipeImage.load(recipe.imageUrl) { + placeholder(R.drawable.ic_android_placeholder) + error(R.drawable.ic_android_placeholder) + } + recipeTitle.text = recipe.title + recipeDescription.text = recipe.description + recipeSteps.text = recipe.steps.toString() + } } private fun deleteRecipe() { diff --git a/app/src/main/res/drawable/ic_android_placeholder.xml b/app/src/main/res/drawable/ic_android_placeholder.xml new file mode 100644 index 0000000..bd8306d --- /dev/null +++ b/app/src/main/res/drawable/ic_android_placeholder.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 86a5d97..8f161e1 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,24 @@ - - + + + app:defaultNavHost="true" + app:navGraph="@navigation/nav_graph" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_confirmation_dialog.xml b/app/src/main/res/layout/fragment_confirmation_dialog.xml new file mode 100644 index 0000000..73ceb25 --- /dev/null +++ b/app/src/main/res/layout/fragment_confirmation_dialog.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_cookbook.xml b/app/src/main/res/layout/fragment_cookbook.xml index 77d9ef6..8559e64 100644 --- a/app/src/main/res/layout/fragment_cookbook.xml +++ b/app/src/main/res/layout/fragment_cookbook.xml @@ -1,6 +1,15 @@ + android:layout_height="match_parent" + android:background="?attr/colorSurface"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_recipe.xml b/app/src/main/res/layout/fragment_recipe.xml index 77d9ef6..ea8caab 100644 --- a/app/src/main/res/layout/fragment_recipe.xml +++ b/app/src/main/res/layout/fragment_recipe.xml @@ -1,6 +1,63 @@ + + + android:layout_height="match_parent" + android:padding="10dp"> + + + + + + + + - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/vh_recipe_category.xml b/app/src/main/res/layout/vh_recipe_category.xml index 006fd49..648d91a 100644 --- a/app/src/main/res/layout/vh_recipe_category.xml +++ b/app/src/main/res/layout/vh_recipe_category.xml @@ -1,6 +1,19 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/vh_recipe_item.xml b/app/src/main/res/layout/vh_recipe_item.xml index 006fd49..8d1e2f4 100644 --- a/app/src/main/res/layout/vh_recipe_item.xml +++ b/app/src/main/res/layout/vh_recipe_item.xml @@ -1,6 +1,55 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="5dp"> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/options_menu.xml b/app/src/main/res/menu/options_menu.xml new file mode 100644 index 0000000..b8633ba --- /dev/null +++ b/app/src/main/res/menu/options_menu.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 0000000..ce6c8a3 --- /dev/null +++ b/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8fce1d1..51cebfd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,6 @@ Cookbook + + Hello blank fragment + Удалить \ No newline at end of file