From 9323c89ccec8737adb29ec7473d36d8fb4a3dd04 Mon Sep 17 00:00:00 2001 From: GeorgeBozon Date: Sun, 22 Feb 2026 01:38:24 +0300 Subject: [PATCH] =?UTF-8?q?-=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D0=B4?= =?UTF-8?q?=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D1=83=D0=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 28 +++ .../java/ru/otus/marketsample/MainActivity.kt | 20 +-- .../java/ru/otus/marketsample/MainFragment.kt | 49 ------ .../java/ru/otus/marketsample/MainScreen.kt | 146 ++++++++++++++++ .../ru/otus/marketsample/MarketSampleApp.kt | 13 +- .../ru/otus/marketsample/ViewModelsFactory.kt | 14 ++ .../details/feature/DetailsFragment.kt | 125 -------------- .../details/feature/DetailsStateFactory.kt | 2 - .../feature/DetailsViewModelFactory.kt | 36 ---- .../details/feature/compose/DetailsContent.kt | 8 + .../details/feature/di/DetailsComponent.kt | 27 --- .../ru/otus/marketsample/di/AppComponent.kt | 19 +- .../otus/marketsample/di/ViewModelsModule.kt | 34 ++++ .../products/feature/ProductListFragment.kt | 117 ------------- .../products/feature/ProductListViewModel.kt | 7 +- .../feature/ProductListViewModelFactory.kt | 32 ---- .../products/feature/ProductStateFactory.kt | 2 - .../products/feature/adapter/ProductHolder.kt | 32 ---- .../feature/adapter/ProductsAdapter.kt | 44 ----- .../feature/compose/ProductsContent.kt | 163 ++++++++++++++++++ .../feature/di/ProductListComponent.kt | 26 --- .../promo/feature/PromoListFragment.kt | 106 ------------ .../promo/feature/PromoListViewModel.kt | 6 +- .../feature/PromoListViewModelFactory.kt | 32 ---- .../promo/feature/PromoStateFactory.kt | 2 - .../promo/feature/adapter/PromoAdapter.kt | 40 ----- .../promo/feature/adapter/PromoHolder.kt | 17 -- .../promo/feature/compose/PromoContent.kt | 94 ++++++++++ .../promo/feature/di/PromoComponent.kt | 22 --- app/src/main/res/values/strings.xml | 1 + gradle/libs.versions.toml | 41 ++++- 31 files changed, 561 insertions(+), 744 deletions(-) delete mode 100644 app/src/main/java/ru/otus/marketsample/MainFragment.kt create mode 100644 app/src/main/java/ru/otus/marketsample/MainScreen.kt create mode 100644 app/src/main/java/ru/otus/marketsample/ViewModelsFactory.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/details/feature/DetailsFragment.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/details/feature/DetailsViewModelFactory.kt create mode 100644 app/src/main/java/ru/otus/marketsample/details/feature/compose/DetailsContent.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/details/feature/di/DetailsComponent.kt create mode 100644 app/src/main/java/ru/otus/marketsample/di/ViewModelsModule.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/products/feature/ProductListFragment.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/products/feature/ProductListViewModelFactory.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/products/feature/adapter/ProductHolder.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/products/feature/adapter/ProductsAdapter.kt create mode 100644 app/src/main/java/ru/otus/marketsample/products/feature/compose/ProductsContent.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/products/feature/di/ProductListComponent.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/promo/feature/PromoListFragment.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/promo/feature/PromoListViewModelFactory.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/promo/feature/adapter/PromoAdapter.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/promo/feature/adapter/PromoHolder.kt create mode 100644 app/src/main/java/ru/otus/marketsample/promo/feature/compose/PromoContent.kt delete mode 100644 app/src/main/java/ru/otus/marketsample/promo/feature/di/PromoComponent.kt diff --git a/app/build.gradle b/app/build.gradle index 1cdd96f..5490d6a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,6 +3,12 @@ plugins { alias(libs.plugins.kotlinAndroid) alias(libs.plugins.kotlinxSerialization) alias(libs.plugins.kapt) + alias(libs.plugins.composeCompiler) +} + +composeCompiler { + reportsDestination.set(layout.buildDirectory.dir("compose_compiler/reports")) + metricsDestination.set(layout.buildDirectory.dir("compose_compiler/metrics")) } android { @@ -33,17 +39,39 @@ android { jvmTarget = '17' } buildFeatures { + compose true viewBinding true } } dependencies { + implementation project(":common:di") implementation project(":common:formatters") implementation project(":common:ui") implementation project(":common:data:products") implementation project(":common:data:promo") + + implementation(platform(libs.androidx.compose.bom)) + + implementation(libs.compose.activity) + implementation(libs.compose.foundation) + implementation(libs.compose.lifecicle.viewmodel) + implementation(libs.compose.material3) + implementation(libs.compose.ui) + implementation(libs.compose.ui.tooling) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.compose.coil) + implementation(libs.compose.coil.http) + + implementation(libs.androidx.navigation3.ui) + implementation(libs.androidx.navigation3.runtime) + implementation(libs.androidx.lifecycle.viewmodel.navigation3) + implementation(libs.androidx.material3.adaptive.navigation3) + + implementation(libs.kotlinx.serialization.core) + implementation libs.core.ktx implementation libs.appcompat implementation libs.material diff --git a/app/src/main/java/ru/otus/marketsample/MainActivity.kt b/app/src/main/java/ru/otus/marketsample/MainActivity.kt index 7e34aaf..60d15b8 100644 --- a/app/src/main/java/ru/otus/marketsample/MainActivity.kt +++ b/app/src/main/java/ru/otus/marketsample/MainActivity.kt @@ -1,26 +1,24 @@ package ru.otus.marketsample import android.os.Bundle +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 javax.inject.Inject class MainActivity : AppCompatActivity() { - private lateinit var binding: ActivityMainBinding + @Inject + lateinit var viewModelsFactory: ViewModelsFactory 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 + + application.getAppComponent()?.inject(this) + + setContent { + MainScreen(viewModelsFactory) } } } \ 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/MainScreen.kt b/app/src/main/java/ru/otus/marketsample/MainScreen.kt new file mode 100644 index 0000000..b3f7164 --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/MainScreen.kt @@ -0,0 +1,146 @@ +package ru.otus.marketsample + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModelProvider +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay +import ru.otus.marketsample.details.feature.compose.DetailsContent +import ru.otus.marketsample.products.feature.ProductListViewModel +import ru.otus.marketsample.products.feature.compose.ProductsContent +import ru.otus.marketsample.promo.feature.PromoListViewModel +import ru.otus.marketsample.promo.feature.compose.PromoContent +import ru.otus.common.ui.R + +private const val PRODUCTS_NAV_INDEX = 0 +private const val PROMO_NAV_INDEX = 1 + +private const val UNKNOWN_INDEX = 2 + +private const val NAV_ICON_SIZE = 24 + +@Composable +fun MainScreen(viewModelsFactory: ViewModelProvider.Factory) { + val backStack = remember { mutableStateListOf(ProductsScreen) } + + Scaffold( + modifier = Modifier + .fillMaxSize() + .background(color = Color.White), + bottomBar = { + NavBar( + selectedItem = when (backStack.lastOrNull()) { + ProductsScreen -> PRODUCTS_NAV_INDEX + PromoScreen -> PROMO_NAV_INDEX + else -> UNKNOWN_INDEX + }, + onPromoListClick = { + if (backStack.lastOrNull() != PromoScreen) backStack.add(PromoScreen) + }, + onProductListClick = { + if (backStack.lastOrNull() != ProductsScreen) backStack.add(ProductsScreen) + } + ) + }) { paddingValues -> + + NavDisplay( + modifier = Modifier.padding(paddingValues), + onBack = { backStack.removeLastOrNull() }, + backStack = backStack, + entryProvider = entryProvider { + entry { + ProductsContent( + viewModelsFactory.create(ProductListViewModel::class.java) + ) + } + entry { + PromoContent( + viewModelsFactory.create(PromoListViewModel::class.java) + ) + } + entry { DetailsContent(it.id) } + } + ) + } +} + +@Composable +private fun NavBar( + selectedItem: Int, + onProductListClick: () -> Unit, + onPromoListClick: () -> Unit +) { + + NavigationBar( + modifier = Modifier.fillMaxWidth(), + windowInsets = NavigationBarDefaults.windowInsets + ) { + NavigationBarItem( + selected = selectedItem == PRODUCTS_NAV_INDEX, + onClick = { + onProductListClick() + }, + label = { + Text( + text = stringResource(ru.otus.marketsample.R.string.title_products), + color = colorResource(R.color.purple_500) + ) + }, + icon = { + Icon( + modifier = Modifier.size(NAV_ICON_SIZE.dp), + painter = painterResource(R.drawable.ic_list), + tint = colorResource(R.color.purple_500), + contentDescription = null + ) + }) + NavigationBarItem( + selected = selectedItem == PROMO_NAV_INDEX, + onClick = { + onPromoListClick() + + }, + label = { + Text( + text = stringResource(ru.otus.marketsample.R.string.title_promo), + color = colorResource(R.color.purple_500) + ) + }, + icon = { + Icon( + modifier = Modifier.size(NAV_ICON_SIZE.dp), + painter = painterResource(R.drawable.ic_discount), + tint = colorResource(R.color.purple_500), + contentDescription = null + ) + }) + } +} + +sealed interface ScreenKey + +data object PromoScreen : ScreenKey + +data object ProductsScreen : ScreenKey + +data class DetailsScreen(val id: String) : ScreenKey + + diff --git a/app/src/main/java/ru/otus/marketsample/MarketSampleApp.kt b/app/src/main/java/ru/otus/marketsample/MarketSampleApp.kt index 0ebd60f..2837f79 100644 --- a/app/src/main/java/ru/otus/marketsample/MarketSampleApp.kt +++ b/app/src/main/java/ru/otus/marketsample/MarketSampleApp.kt @@ -3,13 +3,14 @@ package ru.otus.marketsample import android.app.Application import ru.otus.marketsample.di.AppComponent import ru.otus.marketsample.di.DaggerAppComponent -import ru.otus.common.di.Dependencies -import ru.otus.common.di.DependenciesProvider -class MarketSampleApp: Application(), DependenciesProvider { - val appComponent: AppComponent = DaggerAppComponent.factory().create(this) +class MarketSampleApp: Application() { + lateinit var appComponent: AppComponent - override fun getDependencies(): Dependencies { - return appComponent + override fun onCreate() { + super.onCreate() + appComponent = DaggerAppComponent.factory().create(this) } } + +fun Application.getAppComponent() = (this as? MarketSampleApp)?.appComponent diff --git a/app/src/main/java/ru/otus/marketsample/ViewModelsFactory.kt b/app/src/main/java/ru/otus/marketsample/ViewModelsFactory.kt new file mode 100644 index 0000000..e2eff15 --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/ViewModelsFactory.kt @@ -0,0 +1,14 @@ +package ru.otus.marketsample + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import javax.inject.Inject +import javax.inject.Provider + +class ViewModelsFactory @Inject constructor(private val vmMap: Map, @JvmSuppressWildcards Provider>) : + ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + return vmMap.getValue(modelClass).get() as T + } +} \ No newline at end of file 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 deleted file mode 100644 index e23c57e..0000000 --- a/app/src/main/java/ru/otus/marketsample/details/feature/DetailsFragment.kt +++ /dev/null @@ -1,125 +0,0 @@ -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.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.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 - - private val viewModel: DetailsViewModel by viewModels( - factoryProducer = { factory } - ) - - private val productId by lazy { arguments?.getString("productId")!! } - - override fun onAttach(context: Context) { - super.onAttach(context) - - DaggerDetailsComponent.factory() - .create( - dependencies = findDependencies(), - productId = productId, - ) - .inject(this) - } - - override fun onCreateView( - 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) - } - } - } - } - } - } - - 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/DetailsStateFactory.kt b/app/src/main/java/ru/otus/marketsample/details/feature/DetailsStateFactory.kt index 94053f5..f008903 100644 --- a/app/src/main/java/ru/otus/marketsample/details/feature/DetailsStateFactory.kt +++ b/app/src/main/java/ru/otus/marketsample/details/feature/DetailsStateFactory.kt @@ -1,11 +1,9 @@ package ru.otus.marketsample.details.feature import ru.otus.marketsample.details.domain.ProductDetails -import ru.otus.common.di.FeatureScope import ru.otus.common.formatters.PriceFormatter import javax.inject.Inject -@FeatureScope class DetailsStateFactory @Inject constructor( private val priceFormatter: PriceFormatter, ) { diff --git a/app/src/main/java/ru/otus/marketsample/details/feature/DetailsViewModelFactory.kt b/app/src/main/java/ru/otus/marketsample/details/feature/DetailsViewModelFactory.kt deleted file mode 100644 index 1465975..0000000 --- a/app/src/main/java/ru/otus/marketsample/details/feature/DetailsViewModelFactory.kt +++ /dev/null @@ -1,36 +0,0 @@ -package ru.otus.marketsample.details.feature - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.CreationExtras -import ru.otus.marketsample.details.domain.ConsumeProductDetailsUseCase -import ru.otus.common.di.FeatureScope -import javax.inject.Inject -import javax.inject.Named - -@FeatureScope -class DetailsViewModelFactory @Inject constructor( - private val consumeProductDetailsUseCase: ConsumeProductDetailsUseCase, - private val detailsStateFactory: DetailsStateFactory, - @Named("productId") - private val productId: String, -) : - ViewModelProvider.Factory { - - override fun create( - modelClass: Class, - extras: CreationExtras, - ): T { - when { - modelClass.isAssignableFrom(DetailsViewModel::class.java) -> { - @Suppress("UNCHECKED_CAST") - return DetailsViewModel( - consumeProductDetailsUseCase = consumeProductDetailsUseCase, - detailsStateFactory = detailsStateFactory, - productId = productId, - ) as T - } - } - throw IllegalArgumentException("Unknown ViewModel class") - } -} diff --git a/app/src/main/java/ru/otus/marketsample/details/feature/compose/DetailsContent.kt b/app/src/main/java/ru/otus/marketsample/details/feature/compose/DetailsContent.kt new file mode 100644 index 0000000..ceb949a --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/details/feature/compose/DetailsContent.kt @@ -0,0 +1,8 @@ +package ru.otus.marketsample.details.feature.compose + +import androidx.compose.runtime.Composable + +@Composable +fun DetailsContent(id: String){ + TODO() //Здесь типо реализована деталка :) +} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/marketsample/details/feature/di/DetailsComponent.kt b/app/src/main/java/ru/otus/marketsample/details/feature/di/DetailsComponent.kt deleted file mode 100644 index 8eecd0b..0000000 --- a/app/src/main/java/ru/otus/marketsample/details/feature/di/DetailsComponent.kt +++ /dev/null @@ -1,27 +0,0 @@ -package ru.otus.marketsample.details.feature.di - -import dagger.BindsInstance -import dagger.Component -import ru.otus.common.data.products.ProductRepository -import ru.otus.marketsample.details.feature.DetailsFragment -import ru.otus.common.di.FeatureScope -import javax.inject.Named - -@FeatureScope -@Component(dependencies = [DetailsComponentDependencies::class]) -interface DetailsComponent { - - @Component.Factory - interface Factory { - fun create( - dependencies: DetailsComponentDependencies, - @BindsInstance @Named("productId") productId: String, - ): DetailsComponent - } - - fun inject(detailsFragment: DetailsFragment) -} - -interface DetailsComponentDependencies { - fun getProductRepository(): ProductRepository -} diff --git a/app/src/main/java/ru/otus/marketsample/di/AppComponent.kt b/app/src/main/java/ru/otus/marketsample/di/AppComponent.kt index 2b95681..e4f076c 100644 --- a/app/src/main/java/ru/otus/marketsample/di/AppComponent.kt +++ b/app/src/main/java/ru/otus/marketsample/di/AppComponent.kt @@ -1,29 +1,28 @@ package ru.otus.marketsample.di import android.content.Context +import androidx.lifecycle.ViewModelProvider import dagger.BindsInstance import dagger.Component -import ru.otus.marketsample.details.feature.di.DetailsComponentDependencies -import ru.otus.marketsample.products.feature.di.ProductListComponentDependencies -import ru.otus.marketsample.promo.feature.di.PromoComponentDependencies -import ru.otus.common.di.Dependencies +import ru.otus.marketsample.MainActivity import javax.inject.Singleton -@Singleton @Component( modules = [ NetworkModule::class, DataModule::class, + ViewModelsModule::class ] ) -interface AppComponent: - Dependencies, - DetailsComponentDependencies, - PromoComponentDependencies, - ProductListComponentDependencies +@Singleton +interface AppComponent { + fun provideViewModelFactory(): ViewModelProvider.Factory + @Component.Factory interface Factory { fun create(@BindsInstance applicationContext: Context): AppComponent } + + fun inject(mainActivity: MainActivity) } \ No newline at end of file diff --git a/app/src/main/java/ru/otus/marketsample/di/ViewModelsModule.kt b/app/src/main/java/ru/otus/marketsample/di/ViewModelsModule.kt new file mode 100644 index 0000000..54dc173 --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/di/ViewModelsModule.kt @@ -0,0 +1,34 @@ +package ru.otus.marketsample.di + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.Binds +import dagger.MapKey +import dagger.Module +import dagger.multibindings.IntoMap +import ru.otus.marketsample.ViewModelsFactory +import ru.otus.marketsample.products.feature.ProductListViewModel +import ru.otus.marketsample.promo.feature.PromoListViewModel +import kotlin.reflect.KClass + +@Module +interface ViewModelsModule { + + @Binds + fun provideViewModelFactory(factory: ViewModelsFactory): ViewModelProvider.Factory + + @Binds + @IntoMap + @ViewModelKey(PromoListViewModel::class) + fun bindPromoListViewModel(viewModel: PromoListViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ProductListViewModel::class) + fun bindProductViewModel(viewModel: ProductListViewModel): ViewModel +} + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MapKey +annotation class ViewModelKey(val value: KClass) \ 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/ProductListViewModel.kt b/app/src/main/java/ru/otus/marketsample/products/feature/ProductListViewModel.kt index ce33e63..b0e1bdc 100644 --- a/app/src/main/java/ru/otus/marketsample/products/feature/ProductListViewModel.kt +++ b/app/src/main/java/ru/otus/marketsample/products/feature/ProductListViewModel.kt @@ -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 @@ -14,8 +13,9 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.update import ru.otus.marketsample.products.domain.ConsumeProductsUseCase import ru.otus.marketsample.R +import javax.inject.Inject -class ProductListViewModel( +class ProductListViewModel @Inject constructor( private val consumeProductsUseCase: ConsumeProductsUseCase, private val productStateFactory: ProductStateFactory, ) : ViewModel() { @@ -23,7 +23,7 @@ class ProductListViewModel( private val _state = MutableStateFlow(ProductsScreenState()) val state: StateFlow = _state.asStateFlow() - init { + fun initViewModel() { requestProducts() } @@ -45,6 +45,7 @@ class ProductListViewModel( .catch { _state.update { screenState -> screenState.copy( + isLoading = false, hasError = true, errorProvider = { context -> context.getString(R.string.error_wile_loading_data) } ) diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/ProductListViewModelFactory.kt b/app/src/main/java/ru/otus/marketsample/products/feature/ProductListViewModelFactory.kt deleted file mode 100644 index 55eaeb7..0000000 --- a/app/src/main/java/ru/otus/marketsample/products/feature/ProductListViewModelFactory.kt +++ /dev/null @@ -1,32 +0,0 @@ -package ru.otus.marketsample.products.feature - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.CreationExtras -import ru.otus.common.di.FeatureScope -import ru.otus.marketsample.products.domain.ConsumeProductsUseCase -import javax.inject.Inject - -@FeatureScope -class ProductListViewModelFactory @Inject constructor( - private val consumeProductsUseCase: ConsumeProductsUseCase, - private val productStateFactory: ProductStateFactory, -) : - ViewModelProvider.Factory { - - override fun create( - modelClass: Class, - extras: CreationExtras, - ): T { - when { - modelClass.isAssignableFrom(ProductListViewModel::class.java) -> { - @Suppress("UNCHECKED_CAST") - return ProductListViewModel( - consumeProductsUseCase = consumeProductsUseCase, - productStateFactory = productStateFactory, - ) as T - } - } - throw IllegalArgumentException("Unknown ViewModel class") - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/ProductStateFactory.kt b/app/src/main/java/ru/otus/marketsample/products/feature/ProductStateFactory.kt index 51f0490..34af45f 100644 --- a/app/src/main/java/ru/otus/marketsample/products/feature/ProductStateFactory.kt +++ b/app/src/main/java/ru/otus/marketsample/products/feature/ProductStateFactory.kt @@ -1,12 +1,10 @@ package ru.otus.marketsample.products.feature -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 javax.inject.Inject -@FeatureScope class ProductStateFactory @Inject constructor( private val discountFormatter: DiscountFormatter, private val priceFormatter: PriceFormatter, 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 deleted file mode 100644 index f216b25..0000000 --- a/app/src/main/java/ru/otus/marketsample/products/feature/adapter/ProductHolder.kt +++ /dev/null @@ -1,32 +0,0 @@ -package ru.otus.marketsample.products.feature.adapter - -import android.view.View.GONE -import android.view.View.VISIBLE -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 - -class ProductHolder( - private val binding: ItemProductBinding, - private val onItemClicked: (String) -> Unit, -) : RecyclerView.ViewHolder(binding.root) { - - fun bind(productState: ProductState) { - binding.image.load(productState.image) - binding.name.text = productState.name - binding.price.text = - binding.root.resources.getString(R.string.price_with_arg, productState.price) - if (productState.hasDiscount) { - binding.promo.visibility = VISIBLE - binding.promo.text = productState.discount - } else { - binding.promo.visibility = GONE - } - - binding.root.setOnClickListener { - onItemClicked(productState.id) - } - } -} 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 deleted file mode 100644 index 18354a2..0000000 --- a/app/src/main/java/ru/otus/marketsample/products/feature/adapter/ProductsAdapter.kt +++ /dev/null @@ -1,44 +0,0 @@ -package ru.otus.marketsample.products.feature.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -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 javax.inject.Inject - -@FeatureScope -class ProductsAdapter @Inject constructor( - private val onItemClicked: (String) -> Unit, -) : - ListAdapter(DiffCallback()) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductHolder { - return ProductHolder( - binding = ItemProductBinding.inflate( - LayoutInflater.from(parent.context), parent, false - ), - onItemClicked = onItemClicked, - ) - } - - override fun onBindViewHolder(holder: ProductHolder, position: Int) { - val entity = getItem(position) - entity?.let { - holder.bind(entity) - } - } -} - -private class DiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: ProductState, newItem: ProductState): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame(oldItem: ProductState, newItem: ProductState): Boolean { - return oldItem == newItem - } -} diff --git a/app/src/main/java/ru/otus/marketsample/products/feature/compose/ProductsContent.kt b/app/src/main/java/ru/otus/marketsample/products/feature/compose/ProductsContent.kt new file mode 100644 index 0000000..7ba897e --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/products/feature/compose/ProductsContent.kt @@ -0,0 +1,163 @@ +package ru.otus.marketsample.products.feature.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +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.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +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.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import ru.otus.common.ui.R +import ru.otus.marketsample.products.feature.ProductListViewModel +import ru.otus.marketsample.products.feature.ProductState + +@Composable +fun ProductsContent(viewModel: ProductListViewModel, modifier: Modifier = Modifier) { + val context = LocalContext.current + val state by viewModel.state.collectAsState() + + LaunchedEffect(Unit) { + viewModel.initViewModel() + } + + Box(modifier = modifier.fillMaxSize()) { + when { + state.isLoading -> CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + + state.hasError -> Text( + modifier = Modifier.align(Alignment.Center), + text = state.errorProvider(context) + ) + + state.productListState.isNotEmpty() -> LazyColumn(modifier = Modifier.fillMaxWidth()) { + items(state.productListState) { + ProductCard(modifier = Modifier.padding(16.dp), product = it) + } + } + } + } +} + +@Composable +private fun ProductCard(modifier: Modifier = Modifier, product: ProductState) { + Row( + modifier + .fillMaxWidth() + .height(IntrinsicSize.Min), + verticalAlignment = Alignment.Top + ) { + Box(modifier = Modifier.weight(1f)) { + AsyncImage( + modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(12.dp)), + contentScale = ContentScale.Crop, + model = product.image, + contentDescription = null + ) + if (product.discount.isNotBlank()) { + DiscountBadge( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(end = 8.dp, top = 8.dp), + text = product.discount + ) + } + } + + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .padding(start = 8.dp) + + ) { + val rubStr = stringResource(ru.otus.marketsample.R.string.price_rub) + Text( + modifier = Modifier.align(Alignment.TopStart), + fontFamily = FontFamily.SansSerif, + text = product.name, + fontSize = TextUnit( + 18f, + TextUnitType.Sp + ) + ) + Text( + modifier = Modifier + .align(Alignment.BottomEnd) + .background( + color = Color(0xFFFFF3E0), + shape = RoundedCornerShape(12.dp) + ) + .padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 16.dp), + color = colorResource(R.color.purple_500), + text = "${product.price} $rubStr", + fontWeight = FontWeight.Bold, + fontSize = TextUnit( + 16f, + TextUnitType.Sp + ) + ) + } + } +} + +@Composable +private fun DiscountBadge(modifier: Modifier, text: String) { + val shape = remember { + RoundedCornerShape( + topStart = 20.dp, + bottomEnd = 10.dp, + bottomStart = 20.dp, + topEnd = 3.dp + ) + } + + val gradientColors = + listOf(colorResource(R.color.purple_200), colorResource(R.color.purple_500)) + Box( + modifier = modifier + .border(width = 2.dp, color = Color.White, shape = shape) + .background( + brush = Brush.horizontalGradient(colors = gradientColors), + shape = shape + ) + + ) { + Text( + text = text, + fontWeight = FontWeight.Bold, + fontSize = TextUnit(14f, TextUnitType.Sp), + color = Color.White, + modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 4.dp, bottom = 4.dp) + ) + } +} 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 deleted file mode 100644 index 2b4c9fd..0000000 --- a/app/src/main/java/ru/otus/marketsample/products/feature/di/ProductListComponent.kt +++ /dev/null @@ -1,26 +0,0 @@ -package ru.otus.marketsample.products.feature.di - -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 - -@FeatureScope -@Component(dependencies = [ProductListComponentDependencies::class]) -interface ProductListComponent { - - @Component.Factory - interface Factory { - fun create( - dependencies: ProductListComponentDependencies, - ): ProductListComponent - } - - fun inject(productListFragment: ProductListFragment) -} - -interface ProductListComponentDependencies { - fun getPromoRepository(): PromoRepository - fun getProductRepository(): ProductRepository -} \ 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/PromoListViewModel.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/PromoListViewModel.kt index 6343012..3faf25f 100644 --- a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoListViewModel.kt +++ b/app/src/main/java/ru/otus/marketsample/promo/feature/PromoListViewModel.kt @@ -13,8 +13,9 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.update import ru.otus.marketsample.promo.domain.ConsumePromosUseCase import ru.otus.marketsample.R +import javax.inject.Inject -class PromoListViewModel( +class PromoListViewModel @Inject constructor( private val promoStateFactory: PromoStateFactory, private val consumePromosUseCase: ConsumePromosUseCase, ) : ViewModel() { @@ -22,7 +23,7 @@ class PromoListViewModel( private val _state = MutableStateFlow(PromoScreenState()) val state: StateFlow = _state.asStateFlow() - init { + fun initViewModel() { requestPromos() } @@ -45,6 +46,7 @@ class PromoListViewModel( .catch { _state.update { screenState -> screenState.copy( + isLoading = false, hasError = true, errorProvider = { context -> context.getString(R.string.error_wile_loading_data) } ) diff --git a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoListViewModelFactory.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/PromoListViewModelFactory.kt deleted file mode 100644 index f397c06..0000000 --- a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoListViewModelFactory.kt +++ /dev/null @@ -1,32 +0,0 @@ -package ru.otus.marketsample.promo.feature - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.CreationExtras -import ru.otus.common.di.FeatureScope -import ru.otus.marketsample.promo.domain.ConsumePromosUseCase -import javax.inject.Inject - -@FeatureScope -class PromoListViewModelFactory @Inject constructor( - private val promoStateFactory: PromoStateFactory, - private val consumePromosUseCase: ConsumePromosUseCase, -) : - ViewModelProvider.Factory { - - override fun create( - modelClass: Class, - extras: CreationExtras, - ): T { - when { - modelClass.isAssignableFrom(PromoListViewModel::class.java) -> { - @Suppress("UNCHECKED_CAST") - return PromoListViewModel( - promoStateFactory = promoStateFactory, - consumePromosUseCase = consumePromosUseCase, - ) as T - } - } - throw IllegalArgumentException("Unknown ViewModel class") - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoStateFactory.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/PromoStateFactory.kt index 6d350a6..a8f32de 100644 --- a/app/src/main/java/ru/otus/marketsample/promo/feature/PromoStateFactory.kt +++ b/app/src/main/java/ru/otus/marketsample/promo/feature/PromoStateFactory.kt @@ -1,10 +1,8 @@ package ru.otus.marketsample.promo.feature -import ru.otus.common.di.FeatureScope import ru.otus.marketsample.promo.domain.Promo import javax.inject.Inject -@FeatureScope class PromoStateFactory @Inject constructor() { fun map(promo: Promo): PromoState { return PromoState( 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 deleted file mode 100644 index 0f6b562..0000000 --- a/app/src/main/java/ru/otus/marketsample/promo/feature/adapter/PromoAdapter.kt +++ /dev/null @@ -1,40 +0,0 @@ -package ru.otus.marketsample.promo.feature.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -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 javax.inject.Inject - -@FeatureScope -class PromoAdapter @Inject constructor() : ListAdapter(DiffCallback()) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PromoHolder { - return PromoHolder( - ItemPromoBinding.inflate( - LayoutInflater.from(parent.context), parent, false - ) - ) - } - - override fun onBindViewHolder(holder: PromoHolder, position: Int) { - val entity = getItem(position) - entity?.let { - holder.bind(entity) - } - } -} - -private class DiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: PromoState, newItem: PromoState): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame(oldItem: PromoState, newItem: PromoState): Boolean { - return oldItem == newItem - } -} 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 deleted file mode 100644 index 5d08f5d..0000000 --- a/app/src/main/java/ru/otus/marketsample/promo/feature/adapter/PromoHolder.kt +++ /dev/null @@ -1,17 +0,0 @@ -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 - -class PromoHolder( - private val binding: ItemPromoBinding, -) : RecyclerView.ViewHolder(binding.root) { - - fun bind(promoState: PromoState) { - binding.image.load(promoState.image) - binding.name.text = promoState.name - binding.description.text = promoState.description - } -} diff --git a/app/src/main/java/ru/otus/marketsample/promo/feature/compose/PromoContent.kt b/app/src/main/java/ru/otus/marketsample/promo/feature/compose/PromoContent.kt new file mode 100644 index 0000000..30812a0 --- /dev/null +++ b/app/src/main/java/ru/otus/marketsample/promo/feature/compose/PromoContent.kt @@ -0,0 +1,94 @@ +package ru.otus.marketsample.promo.feature.compose + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +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.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.platform.LocalContext +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import ru.otus.marketsample.promo.feature.PromoListViewModel +import ru.otus.marketsample.promo.feature.PromoState + +@Composable +fun PromoContent(viewModel: PromoListViewModel, modifier: Modifier = Modifier) { + val context = LocalContext.current + val state by viewModel.state.collectAsState() + + LaunchedEffect(Unit) { + viewModel.initViewModel() + } + + Box( + modifier = modifier.fillMaxSize() + ) { + + when { + state.isLoading -> CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + + state.hasError -> Text( + modifier = Modifier.align(Alignment.Center), + text = state.errorProvider(context) + ) + + state.promoListState.isNotEmpty() -> LazyColumn(modifier = Modifier.fillMaxWidth()) { + items(state.promoListState) { + PromoCard(modifier = Modifier.padding(10.dp), promo = it) + } + } + } + } +} + +@Composable +private fun PromoCard(modifier: Modifier = Modifier, promo: PromoState) { + Box(modifier = modifier + .fillMaxWidth() + .height(IntrinsicSize.Min)) { + AsyncImage( + modifier = Modifier.fillMaxSize(), + model = promo.image, + contentScale = ContentScale.Crop, + contentDescription = null + ) + Column(modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomStart)) { + Text( + modifier = Modifier.padding(start = 10.dp), + text = promo.name, + color = Color.White, + fontSize = TextUnit( + 25f, + TextUnitType.Sp + ) + ) + Text( + modifier = Modifier.padding(10.dp), + text = promo.description, + color = Color.White, + fontSize = TextUnit( + 14f, + TextUnitType.Sp + ) + ) + } + } +} \ No newline at end of file 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 deleted file mode 100644 index b1ad582..0000000 --- a/app/src/main/java/ru/otus/marketsample/promo/feature/di/PromoComponent.kt +++ /dev/null @@ -1,22 +0,0 @@ -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 - -@FeatureScope -@Component(dependencies = [PromoComponentDependencies::class]) -interface PromoComponent { - - @Component.Factory - interface Factory { - fun create(dependencies: PromoComponentDependencies): PromoComponent - } - - fun inject(productFragment: PromoListFragment) -} - -interface PromoComponentDependencies { - fun getPromoRepository(): PromoRepository -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 56af33d..8de13e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,6 +3,7 @@ Products Main %1$s руб + руб Error wile loading data Promo Details diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ad9bf92..4f9106e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] agp = "8.11.1" +composeBom = "2026.02.00" kotlin = "2.2.10" core-ktx = "1.17.0" junit = "4.13.2" @@ -20,13 +21,50 @@ okhttp = "4.12.0" retrofit = "2.11.0" swiperefreshlayout = "1.1.0" datastore-core = "1.0.0" -kotlinSerialization = "2.1.10" serializationJson = "1.9.0" androidXDatastore = "1.1.7" kotlinx-coroutines-rx2 = "1.7.3" dagger = "2.57.1" +nav3Core = "1.0.1" +lifecycleViewmodelNav3 = "2.10.0" +kotlinSerialization = "1.9.0" +kotlinxSerializationCore = "1.9.0" +material3AdaptiveNav3 = "1.3.0-alpha08" +coilCompose = "3.3.0" [libraries] +# Core Navigation 3 libraries +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" } +androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" } +androidx-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3AdaptiveNav3" } +# Core Navigation 3 libraries + +# Compose + +# Core UI +compose-ui = { module = "androidx.compose.ui:ui" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } + +# Material 3 +compose-material3 = { module = "androidx.compose.material3:material3" } + +# Foundation (LazyColumn, animations и т.д.) +compose-foundation = { module = "androidx.compose.foundation:foundation" } + +# Activity integration +compose-activity = { module = "androidx.activity:activity-compose"} + +compose-lifecicle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose" } + +compose-coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" } +compose-coil-http = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilCompose" } + +# Compose + 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" } @@ -59,6 +97,7 @@ daggerCompiler = { module = "com.google.dagger:dagger-compiler", version.ref = " network = ["okhttp", "okhttp-logging-interceptor", "retrofit", "retrofit-converter-gson"] [plugins] +composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } androidApplication = { id = "com.android.application", version.ref = "agp" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }