From 9efc086572f12169b4a2f8056d1d073d841b4d21 Mon Sep 17 00:00:00 2001 From: "kosteckiy.daniil" Date: Thu, 19 Mar 2026 12:14:06 +0300 Subject: [PATCH] hw --- app/build.gradle | 13 + .../java/ru/otus/marketsample/MainActivity.kt | 139 +++++++- .../java/ru/otus/marketsample/MainFragment.kt | 49 --- .../products/ProductListScreen.kt | 307 ++++++++++++++++++ .../products/feature/ProductListFragment.kt | 117 ------- .../products/feature/ProductState.kt | 1 + .../feature/di/ProductListComponent.kt | 5 +- .../marketsample/promo/PromoListScreen.kt | 95 ++++++ .../promo/feature/PromoListFragment.kt | 106 ------ .../promo/feature/di/PromoComponent.kt | 4 +- gradle.properties | 12 +- gradle/libs.versions.toml | 19 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 13 files changed, 573 insertions(+), 296 deletions(-) delete mode 100644 app/src/main/java/ru/otus/marketsample/MainFragment.kt create mode 100644 app/src/main/java/ru/otus/marketsample/products/ProductListScreen.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/products/feature/ProductListFragment.kt create mode 100644 app/src/main/java/ru/otus/marketsample/promo/PromoListScreen.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/promo/feature/PromoListFragment.kt diff --git a/app/build.gradle b/app/build.gradle index 1cdd96f..84fae65 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) } android { @@ -34,6 +35,11 @@ android { } buildFeatures { viewBinding true + compose = true + } + composeCompiler { + reportsDestination = layout.buildDirectory.dir("compose_compiler") + metricsDestination = layout.buildDirectory.dir("compose_compiler") } } @@ -55,6 +61,7 @@ dependencies { implementation libs.lifecycle.runtime.ktx implementation libs.navigation.fragment.ktx implementation libs.navigation.ui.ktx + implementation libs.navigation.compose implementation libs.coil implementation libs.gson implementation libs.bundles.network @@ -62,8 +69,14 @@ dependencies { implementation libs.kotlinx.serializationJson implementation libs.androidx.datastore implementation libs.androidx.datastore.preferences + implementation libs.compose.ui + implementation libs.androidx.material3 + implementation libs.compose.foundation + implementation libs.coil.compose implementation libs.dagger + implementation libs.androidx.runtime + implementation libs.androidx.foundation.layout kapt libs.daggerCompiler testImplementation libs.junit diff --git a/app/src/main/java/ru/otus/marketsample/MainActivity.kt b/app/src/main/java/ru/otus/marketsample/MainActivity.kt index 7e34aaf..bc7f277 100644 --- a/app/src/main/java/ru/otus/marketsample/MainActivity.kt +++ b/app/src/main/java/ru/otus/marketsample/MainActivity.kt @@ -1,26 +1,135 @@ package ru.otus.marketsample import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import ru.otus.marketsample.databinding.ActivityMainBinding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import ru.otus.marketsample.products.ProductListScreen +import ru.otus.marketsample.products.feature.ProductListViewModel +import ru.otus.marketsample.products.feature.di.DaggerProductListComponent +import ru.otus.marketsample.promo.PromoListScreen +import ru.otus.marketsample.promo.feature.PromoListViewModel +import ru.otus.marketsample.promo.feature.di.DaggerPromoComponent -class MainActivity : AppCompatActivity() { - - private lateinit var binding: ActivityMainBinding +class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - - ViewCompat.setOnApplyWindowInsetsListener(binding.container) { view, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) - insets + + setContent { + MaterialTheme { + MainScreen() + } + } + } +} + +@Composable +private fun MainScreen() { + val navController = rememberNavController() + val context = LocalContext.current + val appComponent = (context.applicationContext as MarketSampleApp).appComponent + + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { BottomNavigationBar(navController) } + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = "Products", + modifier = Modifier.padding(innerPadding) + ) { + composable("Products") { + val factory = remember { + DaggerProductListComponent.factory() + .create(appComponent) + .getViewModelFactory() + } + + val viewModel: ProductListViewModel = viewModel(factory = factory) + + ProductListScreen( + viewModel = viewModel + ) + } + composable("Promo") { + val factory = remember { + DaggerPromoComponent.factory() + .create(appComponent) + .getViewModelFactory() + } + + val viewModel: PromoListViewModel = viewModel(factory = factory) + PromoListScreen( + viewModel = viewModel + ) + + } + } + } +} + +@Composable +private fun BottomNavigationBar(navController: NavController) { + val items = listOf( + BottomNavItem( + "Products", + "Products", + painterResource(ru.otus.common.ui.R.drawable.ic_list) + ), + BottomNavItem("Promo", "Promo", painterResource(ru.otus.common.ui.R.drawable.ic_discount)) + ) + + NavigationBar { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route + + items.forEach { item -> + NavigationBarItem( + icon = { + Icon( + painter = item.icon, + contentDescription = item.title, + Modifier.size(24.dp) + ) + }, + label = { Text(item.title) }, + selected = currentRoute == item.route, + onClick = { + if (currentRoute != item.route) { + navController.navigate(item.route) { + popUpTo(navController.graph.startDestinationId) + launchSingleTop = true + } + } + } + ) } } -} \ No newline at end of file +} + +data class BottomNavItem(val route: String, val title: String, val icon: Painter) \ No newline at end of file diff --git a/app/src/main/java/ru/otus/marketsample/MainFragment.kt b/app/src/main/java/ru/otus/marketsample/MainFragment.kt deleted file mode 100644 index d03161e..0000000 --- a/app/src/main/java/ru/otus/marketsample/MainFragment.kt +++ /dev/null @@ -1,49 +0,0 @@ -package ru.otus.marketsample - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.fragment.app.Fragment -import androidx.navigation.Navigation.findNavController -import com.google.android.material.bottomnavigation.BottomNavigationView -import ru.otus.marketsample.databinding.FragmentMainBinding - -class MainFragment : Fragment() { - - private var _binding: FragmentMainBinding? = null - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentMainBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val navView: BottomNavigationView = binding.navView - - navView.setOnItemSelectedListener { - findNavController(binding.navHostFragmentMain).navigate(it.itemId) - true - } - - ViewCompat.setOnApplyWindowInsetsListener(binding.container) { view, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - view.setPadding(systemBars.left, 0, systemBars.right, 0) - insets - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/marketsample/products/ProductListScreen.kt b/app/src/main/java/ru/otus/marketsample/products/ProductListScreen.kt new file mode 100644 index 0000000..9308cfe --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/products/ProductListScreen.kt @@ -0,0 +1,307 @@ +package ru.otus.marketsample.products + + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.layout.ContentScale +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import ru.otus.marketsample.products.feature.ProductListViewModel +import ru.otus.marketsample.products.feature.ProductState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProductListScreen( + viewModel: ProductListViewModel +) { + val viewState by viewModel.state.collectAsState() + val pullState = rememberPullToRefreshState() + var productState: ProductState? by remember { + mutableStateOf(null) + } + SharedTransitionLayout { + AnimatedContent( + productState, + label = "basic_transition" + ) { state -> + if (state != null) { + ProductItemDetail( + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout, + item = state, + onBack = { + productState = null + } + ) + } else { + PullToRefreshBox( + isRefreshing = viewState.isLoading, + onRefresh = { viewModel.refresh() }, + state = pullState, + modifier = Modifier.fillMaxSize() + ) { + LazyColumn( + contentPadding = PaddingValues(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + items( + items = viewState.productListState, + key = { it.id } + ) { + ProductItemShort( + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout, + item = it, + onItemClick = { pr -> + productState = pr + } + ) + } + } + } + } + } + } +} + +@Composable +private fun ProductItemShort( + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope, + item: ProductState, + onItemClick: (ProductState) -> Unit +) { + with(sharedTransitionScope) { + Row( + modifier = Modifier + .sharedElement( + rememberSharedContentState(key = item.id), + animatedVisibilityScope = animatedVisibilityScope + ) + .fillMaxWidth() + .height(130.dp) + .clickable { onItemClick(item) } + ) { + Box( + modifier = Modifier + .weight(1f) + ) { + AsyncImage( + modifier = Modifier.clip(RoundedCornerShape(12.dp)), + model = item.image, + contentDescription = "Изображение товара", + contentScale = ContentScale.Crop + ) + if (item.hasDiscount) { + Text( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(8.dp) + .clip( + RoundedCornerShape( + topStart = 16.dp, + topEnd = 0.dp, + bottomStart = 16.dp, + bottomEnd = 16.dp, + ) + ) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(0xFF64B5F6), + Color.Blue + )// от темно-синего к светло-синему + // start = Offset(0f, 0f), end = Offset(0f, Float.POSITIVE_INFINITY) // вертикальный градиент (сверху вниз) + // по умолчанию градиент идёт слева направо (горизонтальный) + ) + ) + .border( + 2.dp, + Color.White, + RoundedCornerShape( + topStart = 16.dp, + topEnd = 0.dp, + bottomStart = 16.dp, + bottomEnd = 16.dp, + ) + ) + .padding( + horizontal = 10.dp, + vertical = 4.dp + ), + text = item.discount, + fontSize = 14.sp, + color = Color.White + ) + } + } + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize() + .padding(horizontal = 12.dp) + ) { + Text( + modifier = Modifier + .align(Alignment.TopStart), + text = item.name, + fontSize = 18.sp, + fontFamily = FontFamily.SansSerif, + maxLines = 2, + fontWeight = FontWeight.Medium + ) + + Text( + modifier = Modifier + .align(Alignment.BottomEnd) + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFFEDE8D0)) + .padding(8.dp) + .align(Alignment.TopStart), + text = item.price + "руб", + fontSize = 18.sp, + color = Color(0xFF6200EE), + fontWeight = FontWeight.Bold + ) + } + } + } +} + +@Composable +private fun ProductItemDetail( + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope, + item: ProductState, + onBack: () -> Unit +) { + BackHandler { + onBack() + } + with(sharedTransitionScope) { + Column( + modifier = Modifier + .sharedElement( + rememberSharedContentState(key = item.id), + animatedVisibilityScope = animatedVisibilityScope + ) + .fillMaxWidth(), + horizontalAlignment = Alignment.End + ) { + AsyncImage( + modifier = Modifier + .fillMaxWidth() + .height(300.dp), + model = item.image, + contentDescription = "Изображение товара", + contentScale = ContentScale.Crop + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = item.name, + fontSize = 24.sp, + textAlign = TextAlign.Start + ) + if (item.hasDiscount) { + Text( + modifier = Modifier + .padding(8.dp) + .clip( + RoundedCornerShape( + topStart = 16.dp, + topEnd = 0.dp, + bottomStart = 16.dp, + bottomEnd = 16.dp, + ) + ) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(0xFF64B5F6), + Color.Blue + ) + ) + ) + .border( + 2.dp, + Color.White, + RoundedCornerShape( + topStart = 16.dp, + topEnd = 0.dp, + bottomStart = 16.dp, + bottomEnd = 16.dp, + ) + ) + .padding( + horizontal = 10.dp, + vertical = 4.dp + ), + text = item.discount, + fontSize = 20.sp, + color = Color.White + ) + } + Text( + modifier = Modifier.padding(10.dp), + text = item.price + "руб", + fontSize = 18.sp, + color = Color(0xFF6200EE) + ) + Button( + shape = RoundedCornerShape(6.dp), + colors = ButtonColors( + containerColor = Color.Blue, contentColor = Color.White, + disabledContainerColor = Color.Gray, + disabledContentColor = Color.White + ), + modifier = Modifier.padding(10.dp), + onClick = {} + ) { + Text( + modifier = Modifier.padding(vertical = 10.dp), + text = "ADD TO CART", + fontSize = 18.sp + ) + } + } + } +} \ No newline at end of file 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/ProductState.kt b/app/src/main/java/ru/otus/marketsample/products/feature/ProductState.kt index b500b08..adbe912 100644 --- a/app/src/main/java/ru/otus/marketsample/products/feature/ProductState.kt +++ b/app/src/main/java/ru/otus/marketsample/products/feature/ProductState.kt @@ -7,6 +7,7 @@ typealias ErrorProvider = (Context) -> String data class ProductsScreenState( val isLoading: Boolean = false, val productListState: List = emptyList(), + val isOpenDetailInfo: ProductState? = null, val hasError: Boolean = false, val errorProvider: ErrorProvider = { "" }, ) 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..048363f 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.ProductListViewModelFactory @FeatureScope @Component(dependencies = [ProductListComponentDependencies::class]) @@ -16,8 +16,7 @@ interface ProductListComponent { dependencies: ProductListComponentDependencies, ): ProductListComponent } - - fun inject(productListFragment: ProductListFragment) + fun getViewModelFactory(): ProductListViewModelFactory } interface ProductListComponentDependencies { diff --git a/app/src/main/java/ru/otus/marketsample/promo/PromoListScreen.kt b/app/src/main/java/ru/otus/marketsample/promo/PromoListScreen.kt new file mode 100644 index 0000000..e53e398 --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/promo/PromoListScreen.kt @@ -0,0 +1,95 @@ +package ru.otus.marketsample.promo + +import androidx.compose.foundation.layout.Arrangement +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.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +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.layout.ContentScale +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import ru.otus.marketsample.promo.feature.PromoListViewModel +import ru.otus.marketsample.promo.feature.PromoState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PromoListScreen( + viewModel: PromoListViewModel +) { + val viewState by viewModel.state.collectAsState() + val pullState = rememberPullToRefreshState() + + PullToRefreshBox( + isRefreshing = viewState.isLoading, + onRefresh = { viewModel.refresh() }, + state = pullState, + modifier = Modifier.fillMaxSize() + ) { + Column ( + modifier = Modifier + .padding(horizontal = 8.dp) + .verticalScroll(rememberScrollState()) + , + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + viewState.promoListState.forEach { + PromoItem(item = it) + } + } + } +} + +@Composable +private fun PromoItem( + item: PromoState +) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(250.dp) + ) { + AsyncImage( + modifier = Modifier.fillMaxSize(), + model = item.image, + contentDescription = "Изображение товара", + contentScale = ContentScale.Crop + ) + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(100.dp) + .padding(10.dp) + , + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = item.name, + fontSize = 24.sp, + color = Color.White + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = item.description, + fontSize = 14.sp, + color = Color.White + ) + } + } +} \ No newline at end of file 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/di/PromoComponent.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/di/PromoComponent.kt index b1ad582..14354cf 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.PromoListViewModelFactory @FeatureScope @Component(dependencies = [PromoComponentDependencies::class]) @@ -13,8 +13,8 @@ interface PromoComponent { interface Factory { fun create(dependencies: PromoComponentDependencies): PromoComponent } + fun getViewModelFactory(): PromoListViewModelFactory - fun inject(productFragment: PromoListFragment) } interface PromoComponentDependencies { diff --git a/gradle.properties b/gradle.properties index 3c5031e..a626f61 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,14 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +android.defaults.buildfeatures.resvalues=true +android.sdk.defaultTargetSdkToCompileSdkIfUnset=false +android.enableAppCompileTimeRClass=false +android.usesSdkInManifest.disallowed=false +android.uniquePackageNames=false +android.dependency.useConstraints=true +android.r8.strictFullModeForKeepRules=false +android.r8.optimizedResourceShrinking=false +android.builtInKotlin=false +android.newDsl=false \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ad9bf92..0b1f627 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ fragment-ktx = "1.8.9" lifecycle-runtime-ktx = "2.9.3" navigation-fragment-ktx = "2.9.3" navigation-ui-ktx = "2.9.3" +nav_version = "2.9.3" coil = "2.7.0" gson = "2.13.1" okhttp = "4.12.0" @@ -25,6 +26,12 @@ serializationJson = "1.9.0" androidXDatastore = "1.1.7" kotlinx-coroutines-rx2 = "1.7.3" dagger = "2.57.1" +runtime = "1.10.3" +foundationLayout = "1.10.3" +compose = "2.2.10" +material3 = "1.3.1" +coil-compose = "2.7.0" + [libraries] core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } @@ -39,6 +46,7 @@ lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-view fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragment-ktx" } lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" } navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation-fragment-ktx" } +navigation-compose = {group = "androidx.navigation", name = "navigation-compose", version.ref = "nav_version" } navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation-ui-ktx" } coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } @@ -51,9 +59,16 @@ androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "a androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidXDatastore" } kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlinSerialization" } kotlinx-serializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serializationJson" } -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-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" } +compose-ui = { group = "androidx.compose.ui", name = "ui"} +androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3"} +compose-foundation = { group = "androidx.compose.foundation", name = "foundation"} +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil-compose" } + + [bundles] network = ["okhttp", "okhttp-logging-interceptor", "retrofit", "retrofit-converter-gson"] @@ -64,4 +79,4 @@ 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" } - +compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 793c13a..3d4c6f7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Aug 30 19:44:41 TRT 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists