From c6e71ba768bcc7eca603b373af926e97959a976f Mon Sep 17 00:00:00 2001 From: "adil.ziganshin" Date: Fri, 23 Jan 2026 13:56:50 +0300 Subject: [PATCH] Move screens on compose --- app/build.gradle | 22 +++ .../main/java/ru/otus/marketsample/Theme.kt | 1 + .../details/feature/DetailsFragment.kt | 89 +------------ .../details/feature/compose/DetailsScreen.kt | 126 ++++++++++++++++++ .../products/feature/ProductListFragment.kt | 117 ---------------- .../products/feature/adapter/ProductHolder.kt | 2 +- .../feature/adapter/ProductsAdapter.kt | 2 +- .../feature/di/ProductListComponent.kt | 2 +- .../feature/{ => ui}/ProductListViewModel.kt | 3 +- .../{ => ui}/ProductListViewModelFactory.kt | 5 +- .../products/feature/{ => ui}/ProductState.kt | 2 +- .../feature/{ => ui}/ProductStateFactory.kt | 4 +- .../feature/ui/compose/ProductListScreen.kt | 109 +++++++++++++++ .../ui/compose/components/DiscountWidget.kt | 47 +++++++ .../ui/compose/components/ProductCard.kt | 107 +++++++++++++++ .../feature/ui/view/ProductListFragment.kt | 59 ++++++++ .../promo/feature/PromoListFragment.kt | 106 --------------- .../promo/feature/adapter/PromoAdapter.kt | 2 +- .../promo/feature/adapter/PromoHolder.kt | 2 +- .../promo/feature/di/PromoComponent.kt | 2 +- .../promo/feature/ui/PromoListFragment.kt | 40 ++++++ .../feature/{ => ui}/PromoListViewModel.kt | 2 +- .../{ => ui}/PromoListViewModelFactory.kt | 2 +- .../promo/feature/{ => ui}/PromoState.kt | 2 +- .../feature/{ => ui}/PromoStateFactory.kt | 2 +- .../promo/feature/ui/compose/PromoCard.kt | 84 ++++++++++++ .../promo/feature/ui/compose/PromoScreen.kt | 95 +++++++++++++ .../navigation/main_fragment_navigation.xml | 4 +- gradle/libs.versions.toml | 9 +- 29 files changed, 722 insertions(+), 327 deletions(-) create mode 100644 app/src/main/java/ru/otus/marketsample/Theme.kt create mode 100644 app/src/main/java/ru/otus/marketsample/details/feature/compose/DetailsScreen.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/products/feature/ProductListFragment.kt rename app/src/main/java/ru/otus/marketsample/products/feature/{ => ui}/ProductListViewModel.kt (95%) rename app/src/main/java/ru/otus/marketsample/products/feature/{ => ui}/ProductListViewModelFactory.kt (92%) rename app/src/main/java/ru/otus/marketsample/products/feature/{ => ui}/ProductState.kt (90%) rename app/src/main/java/ru/otus/marketsample/products/feature/{ => ui}/ProductStateFactory.kt (94%) create mode 100644 app/src/main/java/ru/otus/marketsample/products/feature/ui/compose/ProductListScreen.kt create mode 100644 app/src/main/java/ru/otus/marketsample/products/feature/ui/compose/components/DiscountWidget.kt create mode 100644 app/src/main/java/ru/otus/marketsample/products/feature/ui/compose/components/ProductCard.kt create mode 100644 app/src/main/java/ru/otus/marketsample/products/feature/ui/view/ProductListFragment.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/promo/feature/PromoListFragment.kt create mode 100644 app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoListFragment.kt rename app/src/main/java/ru/otus/marketsample/promo/feature/{ => ui}/PromoListViewModel.kt (97%) rename app/src/main/java/ru/otus/marketsample/promo/feature/{ => ui}/PromoListViewModelFactory.kt (95%) rename app/src/main/java/ru/otus/marketsample/promo/feature/{ => ui}/PromoState.kt (89%) rename app/src/main/java/ru/otus/marketsample/promo/feature/{ => ui}/PromoStateFactory.kt (89%) create mode 100644 app/src/main/java/ru/otus/marketsample/promo/feature/ui/compose/PromoCard.kt create mode 100644 app/src/main/java/ru/otus/marketsample/promo/feature/ui/compose/PromoScreen.kt diff --git a/app/build.gradle b/app/build.gradle index 1cdd96f..c09d3b1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,6 +3,7 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.kotlinxSerialization) alias(libs.plugins.kapt) + alias(libs.plugins.compose.compiler) } android { @@ -34,7 +35,16 @@ android { } buildFeatures { viewBinding true + compose true } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.15" + } +} + +composeCompiler { + reportsDestination = layout.buildDirectory.dir("compose_compiler") + metricsDestination = layout.buildDirectory.dir("compose_compiler") } dependencies { @@ -64,8 +74,20 @@ dependencies { implementation libs.androidx.datastore.preferences implementation libs.dagger + implementation libs.androidx.compose.material3 kapt libs.daggerCompiler + // Compose + def composeBom = libs.androidx.compose.bom + implementation composeBom + androidTestImplementation composeBom + implementation libs.androidx.runtime + implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' + implementation("io.coil-kt.coil3:coil-compose:3.3.0") + implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.0") + testImplementation libs.junit androidTestImplementation libs.androidx.test.ext.junit androidTestImplementation libs.espresso.core diff --git a/app/src/main/java/ru/otus/marketsample/Theme.kt b/app/src/main/java/ru/otus/marketsample/Theme.kt new file mode 100644 index 0000000..16d47bc --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/Theme.kt @@ -0,0 +1 @@ +package ru.otus.marketsample diff --git a/app/src/main/java/ru/otus/marketsample/details/feature/DetailsFragment.kt b/app/src/main/java/ru/otus/marketsample/details/feature/DetailsFragment.kt index e23c57e..ede2758 100644 --- a/app/src/main/java/ru/otus/marketsample/details/feature/DetailsFragment.kt +++ b/app/src/main/java/ru/otus/marketsample/details/feature/DetailsFragment.kt @@ -3,27 +3,17 @@ package ru.otus.marketsample.details.feature import android.content.Context import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.Toast +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import coil.load -import kotlinx.coroutines.launch import ru.otus.common.di.findDependencies +import ru.otus.marketsample.details.feature.compose.DetailsScreenRoute import ru.otus.marketsample.details.feature.di.DaggerDetailsComponent -import ru.otus.marketsample.R -import ru.otus.marketsample.databinding.FragmentDetailsBinding import javax.inject.Inject class DetailsFragment : Fragment() { - private var _binding: FragmentDetailsBinding? = null - private val binding get() = _binding!! - @Inject lateinit var factory: DetailsViewModelFactory @@ -48,78 +38,11 @@ class DetailsFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View { - _binding = FragmentDetailsBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - subscribeUI() - } - - private fun subscribeUI() { - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.state.collect { state -> - when { - state.isLoading -> showLoading() - state.hasError -> { - Toast.makeText( - requireContext(), - "Error wile loading data", - Toast.LENGTH_SHORT - ).show() - - viewModel.errorHasShown() - } - - else -> showProduct(detailsState = state.detailsState) - } - } - } + ): ComposeView { + return ComposeView(context = requireContext()).apply { + setContent { + DetailsScreenRoute(viewModel = viewModel) } } } - - private fun showLoading() { - hideAll() - binding.progress.visibility = View.VISIBLE - } - - private fun showProduct(detailsState: DetailsState) { - hideAll() - binding.image.load(detailsState.image) - binding.image.visibility = View.VISIBLE - - binding.name.text = detailsState.name - binding.name.visibility = View.VISIBLE - - binding.price.text = getString(R.string.price_with_arg, detailsState.price) - binding.price.visibility = View.VISIBLE - - if (detailsState.hasDiscount) { - binding.promo.visibility = View.VISIBLE - binding.promo.text = detailsState.discount - } else { - binding.promo.visibility = View.GONE - } - - binding.addToCart.visibility = View.VISIBLE - } - - private fun hideAll() { - binding.progress.visibility = View.GONE - binding.image.visibility = View.GONE - binding.name.visibility = View.GONE - binding.price.visibility = View.GONE - binding.progress.visibility = View.GONE - binding.addToCart.visibility = View.GONE - } } diff --git a/app/src/main/java/ru/otus/marketsample/details/feature/compose/DetailsScreen.kt b/app/src/main/java/ru/otus/marketsample/details/feature/compose/DetailsScreen.kt new file mode 100644 index 0000000..400d3f7 --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/details/feature/compose/DetailsScreen.kt @@ -0,0 +1,126 @@ +package ru.otus.marketsample.details.feature.compose + +import android.widget.Toast +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil3.compose.AsyncImage +import ru.otus.marketsample.details.feature.DetailsScreenState +import ru.otus.marketsample.details.feature.DetailsState +import ru.otus.marketsample.details.feature.DetailsViewModel +import ru.otus.marketsample.products.feature.ui.ProductState +import ru.otus.marketsample.products.feature.ui.compose.components.DiscountWidget + +@Composable +fun DetailsScreenRoute( + viewModel: DetailsViewModel +) { + val state by viewModel.state.collectAsStateWithLifecycle() + DetailsScreen( + state = state, + onError = viewModel::errorHasShown + ) +} + +@Composable +private fun DetailsScreen( + state: DetailsScreenState, + onError: () -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + if (state.hasError) { + Toast.makeText( + context, + "Error wile loading data", + Toast.LENGTH_SHORT + ).show() + onError() + } + Column( + modifier = modifier + ) { + AsyncImage( + model = state.detailsState.image, + contentDescription = state.detailsState.name, + modifier = Modifier + .height(300.dp) + .fillMaxWidth() + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.End + ) { + Text( + text = state.detailsState.name, + fontSize = 24.sp, + color = Color.Black, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Start + ) + if (state.detailsState.hasDiscount) { + DiscountWidget(discount = state.detailsState.discount) + } + Text( + text = state.detailsState.price, + fontSize = 18.sp, + color = Color(0xFF6200EE), + modifier = Modifier.padding(vertical = 14.dp) + ) + + Button( + onClick = { /* TODO */ }, + modifier = Modifier.padding(top = 10.dp) + ) { + Text(text = "Add to cart", fontSize = 18.sp) + } + } + } + +} + + +@Preview +@Composable +private fun DetailsScreenPreview() { + val product = ProductState( + id = "1", + name = "Вафли с жидким шоколадом", + image = "", + price = "250.00 руб", + hasDiscount = true, + discount = "7%" + ) + val state = DetailsScreenState( + isLoading = false, + detailsState = DetailsState( + name = product.name, + image = product.image, + price = product.price, + discount = product.discount, + hasDiscount = product.hasDiscount + ), + hasError = false, + errorProvider = { "" } + ) + DetailsScreen( + state = state, + onError = {} + ) +} diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/ProductListFragment.kt b/app/src/main/java/ru/otus/marketsample/products/feature/ProductListFragment.kt deleted file mode 100644 index 88f7ec0..0000000 --- a/app/src/main/java/ru/otus/marketsample/products/feature/ProductListFragment.kt +++ /dev/null @@ -1,117 +0,0 @@ -package ru.otus.marketsample.products.feature - -import android.content.Context -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.findNavController -import androidx.recyclerview.widget.LinearLayoutManager -import kotlinx.coroutines.launch -import ru.otus.marketsample.MarketSampleApp -import ru.otus.marketsample.R -import ru.otus.marketsample.databinding.FragmentProductListBinding -import ru.otus.marketsample.products.feature.adapter.ProductsAdapter -import ru.otus.marketsample.products.feature.di.DaggerProductListComponent -import javax.inject.Inject - -class ProductListFragment : Fragment() { - - private var _binding: FragmentProductListBinding? = null - private val binding get() = _binding!! - - @Inject - lateinit var factory: ProductListViewModelFactory - - private val viewModel: ProductListViewModel by viewModels { factory } - - override fun onAttach(context: Context) { - super.onAttach(context) - - val appComponent = (activity?.applicationContext as MarketSampleApp).appComponent - - DaggerProductListComponent.factory() - .create(appComponent) - .inject(this) - } - - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentProductListBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.recyclerView.adapter = ProductsAdapter( - onItemClicked = { productId -> - requireActivity().findNavController(R.id.nav_host_activity_main) - .navigate( - resId = R.id.action_main_to_details, - args = bundleOf("productId" to productId), - ) - } - ) - binding.recyclerView.layoutManager = LinearLayoutManager(context) - - binding.swipeRefreshLayout.setOnRefreshListener { - viewModel.refresh() - } - - subscribeUI() - } - - private fun subscribeUI() { - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.state.collect { state -> - when { - state.isLoading -> showLoading() - state.hasError -> { - Toast.makeText( - requireContext(), - "Error wile loading data", - Toast.LENGTH_SHORT - ).show() - - viewModel.errorHasShown() - } - - else -> showProductList(productListState = state.productListState) - } - } - } - } - } - } - - private fun showProductList(productListState: List) { - binding.progress.visibility = View.GONE - binding.recyclerView.visibility = View.VISIBLE - (binding.recyclerView.adapter as ProductsAdapter).submitList(productListState) - binding.swipeRefreshLayout.isRefreshing = false - } - - private fun showLoading() { - binding.progress.visibility = View.VISIBLE - binding.recyclerView.visibility = View.GONE - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/adapter/ProductHolder.kt b/app/src/main/java/ru/otus/marketsample/products/feature/adapter/ProductHolder.kt index f216b25..9dc0da2 100644 --- a/app/src/main/java/ru/otus/marketsample/products/feature/adapter/ProductHolder.kt +++ b/app/src/main/java/ru/otus/marketsample/products/feature/adapter/ProductHolder.kt @@ -6,7 +6,7 @@ import androidx.recyclerview.widget.RecyclerView import coil.load import ru.otus.marketsample.R import ru.otus.marketsample.databinding.ItemProductBinding -import ru.otus.marketsample.products.feature.ProductState +import ru.otus.marketsample.products.feature.ui.ProductState class ProductHolder( private val binding: ItemProductBinding, diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/adapter/ProductsAdapter.kt b/app/src/main/java/ru/otus/marketsample/products/feature/adapter/ProductsAdapter.kt index 18354a2..857dbe4 100644 --- a/app/src/main/java/ru/otus/marketsample/products/feature/adapter/ProductsAdapter.kt +++ b/app/src/main/java/ru/otus/marketsample/products/feature/adapter/ProductsAdapter.kt @@ -6,7 +6,7 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import ru.otus.common.di.FeatureScope import ru.otus.marketsample.databinding.ItemProductBinding -import ru.otus.marketsample.products.feature.ProductState +import ru.otus.marketsample.products.feature.ui.ProductState import javax.inject.Inject @FeatureScope diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/di/ProductListComponent.kt b/app/src/main/java/ru/otus/marketsample/products/feature/di/ProductListComponent.kt index 2b4c9fd..4ca6ea3 100644 --- a/app/src/main/java/ru/otus/marketsample/products/feature/di/ProductListComponent.kt +++ b/app/src/main/java/ru/otus/marketsample/products/feature/di/ProductListComponent.kt @@ -4,7 +4,7 @@ import dagger.Component import ru.otus.common.data.products.ProductRepository import ru.otus.common.data.promo.PromoRepository import ru.otus.common.di.FeatureScope -import ru.otus.marketsample.products.feature.ProductListFragment +import ru.otus.marketsample.products.feature.ui.view.ProductListFragment @FeatureScope @Component(dependencies = [ProductListComponentDependencies::class]) diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/ProductListViewModel.kt b/app/src/main/java/ru/otus/marketsample/products/feature/ui/ProductListViewModel.kt similarity index 95% rename from app/src/main/java/ru/otus/marketsample/products/feature/ProductListViewModel.kt rename to app/src/main/java/ru/otus/marketsample/products/feature/ui/ProductListViewModel.kt index ce33e63..7525fc1 100644 --- a/app/src/main/java/ru/otus/marketsample/products/feature/ProductListViewModel.kt +++ b/app/src/main/java/ru/otus/marketsample/products/feature/ui/ProductListViewModel.kt @@ -1,4 +1,4 @@ -package ru.otus.marketsample.products.feature +package ru.otus.marketsample.products.feature.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -6,7 +6,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/ProductListViewModelFactory.kt b/app/src/main/java/ru/otus/marketsample/products/feature/ui/ProductListViewModelFactory.kt similarity index 92% rename from app/src/main/java/ru/otus/marketsample/products/feature/ProductListViewModelFactory.kt rename to app/src/main/java/ru/otus/marketsample/products/feature/ui/ProductListViewModelFactory.kt index 55eaeb7..652bc45 100644 --- a/app/src/main/java/ru/otus/marketsample/products/feature/ProductListViewModelFactory.kt +++ b/app/src/main/java/ru/otus/marketsample/products/feature/ui/ProductListViewModelFactory.kt @@ -1,4 +1,4 @@ -package ru.otus.marketsample.products.feature +package ru.otus.marketsample.products.feature.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -11,8 +11,7 @@ import javax.inject.Inject class ProductListViewModelFactory @Inject constructor( private val consumeProductsUseCase: ConsumeProductsUseCase, private val productStateFactory: ProductStateFactory, -) : - ViewModelProvider.Factory { +) : ViewModelProvider.Factory { override fun create( modelClass: Class, diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/ProductState.kt b/app/src/main/java/ru/otus/marketsample/products/feature/ui/ProductState.kt similarity index 90% rename from app/src/main/java/ru/otus/marketsample/products/feature/ProductState.kt rename to app/src/main/java/ru/otus/marketsample/products/feature/ui/ProductState.kt index b500b08..253989b 100644 --- a/app/src/main/java/ru/otus/marketsample/products/feature/ProductState.kt +++ b/app/src/main/java/ru/otus/marketsample/products/feature/ui/ProductState.kt @@ -1,4 +1,4 @@ -package ru.otus.marketsample.products.feature +package ru.otus.marketsample.products.feature.ui import android.content.Context diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/ProductStateFactory.kt b/app/src/main/java/ru/otus/marketsample/products/feature/ui/ProductStateFactory.kt similarity index 94% rename from app/src/main/java/ru/otus/marketsample/products/feature/ProductStateFactory.kt rename to app/src/main/java/ru/otus/marketsample/products/feature/ui/ProductStateFactory.kt index 51f0490..4af86e8 100644 --- a/app/src/main/java/ru/otus/marketsample/products/feature/ProductStateFactory.kt +++ b/app/src/main/java/ru/otus/marketsample/products/feature/ui/ProductStateFactory.kt @@ -1,9 +1,9 @@ -package ru.otus.marketsample.products.feature +package ru.otus.marketsample.products.feature.ui import ru.otus.common.di.FeatureScope -import ru.otus.marketsample.products.domain.Product import ru.otus.common.formatters.DiscountFormatter import ru.otus.common.formatters.PriceFormatter +import ru.otus.marketsample.products.domain.Product import javax.inject.Inject @FeatureScope diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/ui/compose/ProductListScreen.kt b/app/src/main/java/ru/otus/marketsample/products/feature/ui/compose/ProductListScreen.kt new file mode 100644 index 0000000..20a7db5 --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/products/feature/ui/compose/ProductListScreen.kt @@ -0,0 +1,109 @@ +package ru.otus.marketsample.products.feature.ui.compose + +import android.widget.Toast +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ru.otus.marketsample.products.feature.ui.ProductListViewModel +import ru.otus.marketsample.products.feature.ui.ProductState +import ru.otus.marketsample.products.feature.ui.ProductsScreenState +import ru.otus.marketsample.products.feature.ui.compose.components.ProductCard + +@Composable +fun ProductListRoute( + viewModel: ProductListViewModel, + navigateToDetails: (String) -> Unit +) { + val state by viewModel.state.collectAsStateWithLifecycle() + ProductListScreen( + state = state, + navigateToDetails = navigateToDetails, + onRefresh = viewModel::refresh + ) +} + +@Composable +private fun ProductListScreen( + state: ProductsScreenState, + navigateToDetails: (String) -> Unit, + onRefresh: () -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + if (state.hasError) { + Toast.makeText( + context, + "Error wile loading data", + Toast.LENGTH_SHORT + ).show() + } + PullToRefreshBox( + isRefreshing = state.isLoading, + onRefresh = onRefresh, + ) { + LazyColumn( + modifier = modifier + .fillMaxSize() + ) { + items(state.productListState.size) { index -> + ProductCard( + state = state.productListState[index], + navigateToDetails = navigateToDetails + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ProductsScreenPreview() { + val previewState = ProductsScreenState( + isLoading = false, + productListState = listOf( + ProductState( + id = "1", + name = "Персиковый кранч", + image = "https://example.com/food_peach_crunch.jpg", + price = "320,00 руб", + hasDiscount = true, + discount = "10%" + ), + ProductState( + id = "2", + name = "Черничные блинчики", + image = "https://example.com/food_blueberry_pancakes.jpg", + price = "280,00 руб", + hasDiscount = false, + discount = "" + ), + ProductState( + id = "3", + name = "Вафли с жидким шоколадом", + image = "https://example.com/food_choco_waffles.jpg", + price = "250,00 руб", + hasDiscount = true, + discount = "7%" + ), + ProductState( + id = "4", + name = "Фруктовый мусс", + image = "https://example.com/food_fruit_mousse.jpg", + price = "300,00 руб", + hasDiscount = false, + discount = "" + ) + ), + hasError = false, + errorProvider = { "" } + ) + + ProductListScreen(state = previewState, onRefresh = {}, navigateToDetails = {}) +} diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/ui/compose/components/DiscountWidget.kt b/app/src/main/java/ru/otus/marketsample/products/feature/ui/compose/components/DiscountWidget.kt new file mode 100644 index 0000000..da6d905 --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/products/feature/ui/compose/components/DiscountWidget.kt @@ -0,0 +1,47 @@ +package ru.otus.marketsample.products.feature.ui.compose.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + + + +@Composable +fun DiscountWidget( + discount: String, + modifier: Modifier = Modifier +) { + val brush = Brush.linearGradient( + colors = listOf( + colorResource(ru.otus.common.ui.R.color.purple_200), + colorResource(ru.otus.common.ui.R.color.purple_500) + ) + ) + val shape = RoundedCornerShape( + topStart = 40.dp, + bottomStart = 40.dp, + topEnd = 10.dp, + bottomEnd = 40.dp + ) + Text( + text = discount, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + modifier = modifier + .padding(8.dp) + .background(brush = brush, shape = shape) + .border(width = 2.dp, color = Color.White, shape = shape) + .padding(horizontal = 10.dp, vertical = 4.dp) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/ui/compose/components/ProductCard.kt b/app/src/main/java/ru/otus/marketsample/products/feature/ui/compose/components/ProductCard.kt new file mode 100644 index 0000000..8a09f95 --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/products/feature/ui/compose/components/ProductCard.kt @@ -0,0 +1,107 @@ +package ru.otus.marketsample.products.feature.ui.compose.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import ru.otus.marketsample.products.feature.ui.ProductState + +@Composable +fun ProductCard( + state: ProductState, + navigateToDetails: (String) -> Unit, + modifier: Modifier = Modifier +) { + val priceBackground = Color(0xFFFFF3E0) + Row( + modifier = modifier + .padding(horizontal = 16.dp, vertical = 24.dp) + .height(130.dp) + .fillMaxWidth() + .clickable { + navigateToDetails(state.id) + } + ) { + Box( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(12.dp)) // get from res + ) { + AsyncImage( + model = state.image, + contentDescription = state.name, + modifier = Modifier + .fillMaxSize() + ) + if (state.hasDiscount) { + DiscountWidget( + discount = state.discount, + modifier = Modifier.align(Alignment.TopEnd) + ) + } + } + Spacer(modifier = Modifier.width(16.dp)) + Box( + modifier = Modifier + .fillMaxSize() + .weight(1f) + ) { + Text( + text = state.name, + fontSize = 18.sp, + modifier = Modifier + .align(Alignment.TopStart) + ) + Text( + text = state.price, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = colorResource(ru.otus.common.ui.R.color.purple_500), + modifier = Modifier + .align(Alignment.BottomEnd) + .background( + color = priceBackground, + shape = RoundedCornerShape(8.dp) + ) + .padding(vertical = 12.dp, horizontal = 8.dp) + ) + } + } + +} + +@Preview +@Composable +private fun ProductCardPreview() { + + val state = ProductState( + id = "1", + name = "Вафли с жидким шоколадом", + image = "https://images.unsplash.com/photo-1721078917681-40f74d16d7b7?q=80&w=1023&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + price = "250,00 руб", + hasDiscount = true, + discount = "7%" + ) + + ProductCard(state = state, navigateToDetails = {}) + +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/ui/view/ProductListFragment.kt b/app/src/main/java/ru/otus/marketsample/products/feature/ui/view/ProductListFragment.kt new file mode 100644 index 0000000..67e6b60 --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/products/feature/ui/view/ProductListFragment.kt @@ -0,0 +1,59 @@ +package ru.otus.marketsample.products.feature.ui.view + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.findNavController +import ru.otus.marketsample.MarketSampleApp +import ru.otus.marketsample.products.feature.di.DaggerProductListComponent +import ru.otus.marketsample.products.feature.ui.ProductListViewModel +import ru.otus.marketsample.products.feature.ui.ProductListViewModelFactory +import ru.otus.marketsample.products.feature.ui.compose.ProductListRoute +import javax.inject.Inject +import ru.otus.marketsample.R + +class ProductListFragment : Fragment() { + + @Inject + lateinit var factory: ProductListViewModelFactory + + private val viewModel: ProductListViewModel by viewModels { factory } + + override fun onAttach(context: Context) { + super.onAttach(context) + + val appComponent = (activity?.applicationContext as MarketSampleApp).appComponent + + DaggerProductListComponent.factory() + .create(appComponent) + .inject(this) + } + + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).apply { + // Set the Compose content + setContent { + ProductListRoute(viewModel = viewModel, navigateToDetails = ::navigateToDetails) + } + } + } + + private fun navigateToDetails(productId: String) { + requireActivity().findNavController(R.id.nav_host_activity_main) + .navigate( + resId = R.id.action_main_to_details, + args = bundleOf("productId" to productId), + ) + } +} diff --git a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoListFragment.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/PromoListFragment.kt deleted file mode 100644 index 2e4f533..0000000 --- a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoListFragment.kt +++ /dev/null @@ -1,106 +0,0 @@ -package ru.otus.marketsample.promo.feature - -import android.content.Context -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.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.LinearLayoutManager -import kotlinx.coroutines.launch -import ru.otus.common.di.findDependencies -import ru.otus.marketsample.databinding.FragmentPromoListBinding -import ru.otus.marketsample.promo.feature.adapter.PromoAdapter -import ru.otus.marketsample.promo.feature.di.DaggerPromoComponent -import javax.inject.Inject - -class PromoListFragment : Fragment() { - - private var _binding: FragmentPromoListBinding? = null - private val binding get() = _binding!! - - @Inject - lateinit var adapter: PromoAdapter - - @Inject - lateinit var factory: PromoListViewModelFactory - - private val viewModel: PromoListViewModel by viewModels { factory } - - override fun onAttach(context: Context) { - super.onAttach(context) - - DaggerPromoComponent.factory() - .create(dependencies = findDependencies()) - .inject(this) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentPromoListBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.recyclerView.adapter = adapter - binding.recyclerView.layoutManager = LinearLayoutManager(context) - - binding.swipeRefreshLayout.setOnRefreshListener { - viewModel.refresh() - } - - subscribeUI() - } - - private fun subscribeUI() { - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.state.collect { state -> - when { - state.isLoading -> showLoading() - state.hasError -> { - Toast.makeText( - requireContext(), - "Error wile loading data", - Toast.LENGTH_SHORT - ).show() - - viewModel.errorHasShown() - } - - else -> showPromoList(promoListState = state.promoListState) - } - } - } - } - } - } - - private fun showPromoList(promoListState: List) { - binding.progress.visibility = View.GONE - binding.recyclerView.visibility = View.VISIBLE - adapter.submitList(promoListState) - binding.swipeRefreshLayout.isRefreshing = false - } - - private fun showLoading() { - binding.progress.visibility = View.VISIBLE - binding.recyclerView.visibility = View.GONE - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/app/src/main/java/ru/otus/marketsample/promo/feature/adapter/PromoAdapter.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/adapter/PromoAdapter.kt index 0f6b562..3312920 100644 --- a/app/src/main/java/ru/otus/marketsample/promo/feature/adapter/PromoAdapter.kt +++ b/app/src/main/java/ru/otus/marketsample/promo/feature/adapter/PromoAdapter.kt @@ -6,7 +6,7 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import ru.otus.common.di.FeatureScope import ru.otus.marketsample.databinding.ItemPromoBinding -import ru.otus.marketsample.promo.feature.PromoState +import ru.otus.marketsample.promo.feature.ui.PromoState import javax.inject.Inject @FeatureScope diff --git a/app/src/main/java/ru/otus/marketsample/promo/feature/adapter/PromoHolder.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/adapter/PromoHolder.kt index 5d08f5d..16e74cf 100644 --- a/app/src/main/java/ru/otus/marketsample/promo/feature/adapter/PromoHolder.kt +++ b/app/src/main/java/ru/otus/marketsample/promo/feature/adapter/PromoHolder.kt @@ -3,7 +3,7 @@ package ru.otus.marketsample.promo.feature.adapter import androidx.recyclerview.widget.RecyclerView import coil.load import ru.otus.marketsample.databinding.ItemPromoBinding -import ru.otus.marketsample.promo.feature.PromoState +import ru.otus.marketsample.promo.feature.ui.PromoState class PromoHolder( private val binding: ItemPromoBinding, diff --git a/app/src/main/java/ru/otus/marketsample/promo/feature/di/PromoComponent.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/di/PromoComponent.kt index b1ad582..ecb2140 100644 --- a/app/src/main/java/ru/otus/marketsample/promo/feature/di/PromoComponent.kt +++ b/app/src/main/java/ru/otus/marketsample/promo/feature/di/PromoComponent.kt @@ -3,7 +3,7 @@ package ru.otus.marketsample.promo.feature.di import dagger.Component import ru.otus.common.data.promo.PromoRepository import ru.otus.common.di.FeatureScope -import ru.otus.marketsample.promo.feature.PromoListFragment +import ru.otus.marketsample.promo.feature.ui.PromoListFragment @FeatureScope @Component(dependencies = [PromoComponentDependencies::class]) diff --git a/app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoListFragment.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoListFragment.kt new file mode 100644 index 0000000..1d50721 --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoListFragment.kt @@ -0,0 +1,40 @@ +package ru.otus.marketsample.promo.feature.ui + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import ru.otus.common.di.findDependencies +import ru.otus.marketsample.promo.feature.di.DaggerPromoComponent +import ru.otus.marketsample.promo.feature.ui.compose.PromoScreenRoute +import javax.inject.Inject + +class PromoListFragment : Fragment() { + + @Inject + lateinit var factory: PromoListViewModelFactory + private val viewModel: PromoListViewModel by viewModels { factory } + + override fun onAttach(context: Context) { + super.onAttach(context) + + DaggerPromoComponent.factory() + .create(dependencies = findDependencies()) + .inject(this) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): ComposeView { + return ComposeView(context = requireContext()).apply { + setContent { + PromoScreenRoute(viewModel = viewModel) + } + } + } +} diff --git a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoListViewModel.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoListViewModel.kt similarity index 97% rename from app/src/main/java/ru/otus/marketsample/promo/feature/PromoListViewModel.kt rename to app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoListViewModel.kt index 6343012..826c123 100644 --- a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoListViewModel.kt +++ b/app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoListViewModel.kt @@ -1,4 +1,4 @@ -package ru.otus.marketsample.promo.feature +package ru.otus.marketsample.promo.feature.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoListViewModelFactory.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoListViewModelFactory.kt similarity index 95% rename from app/src/main/java/ru/otus/marketsample/promo/feature/PromoListViewModelFactory.kt rename to app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoListViewModelFactory.kt index f397c06..1b8c012 100644 --- a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoListViewModelFactory.kt +++ b/app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoListViewModelFactory.kt @@ -1,4 +1,4 @@ -package ru.otus.marketsample.promo.feature +package ru.otus.marketsample.promo.feature.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider diff --git a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoState.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoState.kt similarity index 89% rename from app/src/main/java/ru/otus/marketsample/promo/feature/PromoState.kt rename to app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoState.kt index 8a45b7d..cbab71b 100644 --- a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoState.kt +++ b/app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoState.kt @@ -1,4 +1,4 @@ -package ru.otus.marketsample.promo.feature +package ru.otus.marketsample.promo.feature.ui import android.content.Context diff --git a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoStateFactory.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoStateFactory.kt similarity index 89% rename from app/src/main/java/ru/otus/marketsample/promo/feature/PromoStateFactory.kt rename to app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoStateFactory.kt index 6d350a6..e7182af 100644 --- a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoStateFactory.kt +++ b/app/src/main/java/ru/otus/marketsample/promo/feature/ui/PromoStateFactory.kt @@ -1,4 +1,4 @@ -package ru.otus.marketsample.promo.feature +package ru.otus.marketsample.promo.feature.ui import ru.otus.common.di.FeatureScope import ru.otus.marketsample.promo.domain.Promo diff --git a/app/src/main/java/ru/otus/marketsample/promo/feature/ui/compose/PromoCard.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/ui/compose/PromoCard.kt new file mode 100644 index 0000000..7f20eeb --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/promo/feature/ui/compose/PromoCard.kt @@ -0,0 +1,84 @@ +package ru.otus.marketsample.promo.feature.ui.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import ru.otus.marketsample.promo.feature.ui.PromoState + + +@Composable +fun PromoCard(state: PromoState) { + Box( + modifier = Modifier + .padding(10.dp) + .fillMaxWidth() + .height(250.dp) + ) { + AsyncImage( + model = state.image, + contentDescription = state.name, + modifier = Modifier + .fillMaxSize() + .clip(RectangleShape), + contentScale = ContentScale.Crop + ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(100.dp) + .align(Alignment.BottomCenter) + .background( + brush = Brush.verticalGradient( + colors = listOf(Color.Transparent, Color.Black) + ) + ) + ) + Column( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(10.dp) + ) { + Text( + text = state.name, + fontSize = 24.sp, + color = Color.White + ) + Text( + text = state.description, + fontSize = 14.sp, + color = Color.White + ) + } + } +} + +@Preview +@Composable +fun PromoCardPreview() { + PromoCard( + state = PromoState( + id = "1", + name = "Шоколадное настроение", + description = "7% скидка на всё, что создано для сладкой жизни!\n" + + "Шоколадные десерты, хрустящее печенье, воздушные торты - идеально для семейных чаепитий или подарка себе,", + image = "https://media.istockphoto.com/id/522735736/photo/chocolate-background.jpg?s=612x612&w=0&k=20&c=fmBbjHi5zXpcbzAQWGy3xtPgcIJkc7eHXjZiYsi396A=" + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/marketsample/promo/feature/ui/compose/PromoScreen.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/ui/compose/PromoScreen.kt new file mode 100644 index 0000000..acbb5c6 --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/promo/feature/ui/compose/PromoScreen.kt @@ -0,0 +1,95 @@ +package ru.otus.marketsample.promo.feature.ui.compose + +import android.widget.Toast +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ru.otus.marketsample.promo.feature.ui.PromoListViewModel +import ru.otus.marketsample.promo.feature.ui.PromoScreenState +import ru.otus.marketsample.promo.feature.ui.PromoState + +@Composable +fun PromoScreenRoute( + viewModel: PromoListViewModel +) { + val state by viewModel.state.collectAsStateWithLifecycle() + PromoScreen( + state = state, + onRefresh = viewModel::refresh, + onError = viewModel::errorHasShown + ) +} + +@Composable +private fun PromoScreen( + state: PromoScreenState, + onRefresh: () -> Unit, + onError: () -> Unit +) { + val context = LocalContext.current + if (state.hasError) { + Toast.makeText( + context, + "Error wile loading data", + Toast.LENGTH_SHORT + ).show() + onError() + } + PullToRefreshBox( + isRefreshing = state.isLoading, + onRefresh = onRefresh + ) { + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + items(state.promoListState.size) { index -> + PromoCard(state.promoListState[index]) + } + } + } +} + +@Preview +@Composable +private fun PromoScreenPreview() { + val promos = listOf( + PromoState( + id = "1", + name = "Летний Sale", + description = "Скидки до 50% на все солнцезащитные очки только до конца недели.", + image = "https://example.com/promo1.jpg" + ), + PromoState( + id = "2", + name = "Кешбэк 15%", + description = "Возвращаем часть средств при оплате картами партнеров в категории «Еда».", + image = "https://example.com/promo2.jpg" + ), + PromoState( + id = "3", + name = "Новая коллекция", + description = "Посмотрите наши последние поступления осеннего сезона уже сегодня.", + image = "https://example.com/promo3.jpg" + ), + PromoState( + id = "4", + name = "Бесплатная доставка", + description = "При заказе от 3000 рублей доставим ваш товар прямо до двери бесплатно.", + image = "https://example.com/promo4.jpg" + ) + ) + val state = PromoScreenState( + promoListState = promos + ) + PromoScreen( + state = state, + onRefresh = {}, + onError = {} + ) +} \ No newline at end of file diff --git a/app/src/main/res/navigation/main_fragment_navigation.xml b/app/src/main/res/navigation/main_fragment_navigation.xml index 5ed484e..7c1940c 100644 --- a/app/src/main/res/navigation/main_fragment_navigation.xml +++ b/app/src/main/res/navigation/main_fragment_navigation.xml @@ -7,13 +7,13 @@ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ad9bf92..cd8f161 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] agp = "8.11.1" +composeBom = "2025.11.00" kotlin = "2.2.10" core-ktx = "1.17.0" junit = "4.13.2" @@ -25,8 +26,11 @@ serializationJson = "1.9.0" androidXDatastore = "1.1.7" kotlinx-coroutines-rx2 = "1.7.3" dagger = "2.57.1" +runtime = "1.9.4" +material3 = "1.4.0" [libraries] +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } @@ -54,6 +58,8 @@ kotlinx-serializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serializat kotlinx-coroutines-rx2 = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-rx2", version.ref = "kotlinx-coroutines-rx2" } dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } daggerCompiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } +androidx-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } [bundles] network = ["okhttp", "okhttp-logging-interceptor", "retrofit", "retrofit-converter-gson"] @@ -64,4 +70,5 @@ kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } androidLibrary = { id = "com.android.library", version.ref = "agp" } - +org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }