Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.kotlinAndroid)
alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.kapt)
alias(libs.plugins.kotlinCompose)
}

android {
Expand Down Expand Up @@ -34,15 +35,30 @@ android {
}
buildFeatures {
viewBinding true
compose = true
}
}

composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_reports")
metricsDestination = layout.buildDirectory.dir("compose_metrics")
}


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("androidx.compose:compose-bom:2025.05.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.activity:activity-compose:1.10.1")
implementation("io.coil-kt:coil-compose:2.7.0")
debugImplementation("androidx.compose.ui:ui-tooling")
implementation("androidx.compose.ui:ui-tooling-preview")

implementation libs.core.ktx
implementation libs.appcompat
Expand All @@ -64,6 +80,8 @@ dependencies {
implementation libs.androidx.datastore.preferences

implementation libs.dagger
implementation libs.androidx.runtime
implementation libs.androidx.foundation.layout
kapt libs.daggerCompiler

testImplementation libs.junit
Expand Down
20 changes: 20 additions & 0 deletions app/src/main/java/ru/otus/marketsample/common/Compose.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ru.otus.marketsample.common

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp

@Composable
fun Loader(modifier: Modifier = Modifier) {
Box(modifier = modifier) {
CircularProgressIndicator(
modifier = Modifier.size(32.dp),
color = colorResource(ru.otus.common.ui.R.color.teal_700),
strokeWidth = 3.dp
)
}
}
191 changes: 191 additions & 0 deletions app/src/main/java/ru/otus/marketsample/products/compose/Products.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package ru.otus.marketsample.products.compose

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
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.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
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.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import ru.otus.marketsample.products.feature.ProductState
import ru.otus.marketsample.R
import ru.otus.marketsample.products.feature.ProductsScreenState
import kotlin.String


@OptIn(ExperimentalMaterial3Api::class)
@Suppress("NonSkippableComposable")
@Composable
fun ProductsList(
state: ProductsScreenState,
modifier: Modifier = Modifier,
onRefresh: () -> Unit,
onClick: (String) -> Unit
) {
PullToRefreshBox(
isRefreshing = state.isLoading,
onRefresh = onRefresh,
) {
LazyColumn(modifier = modifier) {
items(
items = state.productListState,
key = { it.id }) { product ->
ProductItem(productState = product, onClick = { id ->
onClick(id)
})
}
}
}
}

@Composable
fun ProductItem(
productState: ProductState,
modifier: Modifier = Modifier,
onClick: (String) -> Unit
) {
Row(
modifier = modifier
.background(color = Color.White)
.clickable(
onClick = { onClick(productState.id) }
)
.padding(horizontal = 16.dp, vertical = 24.dp)
.height(130.dp)
) {
Box(
modifier = Modifier
.weight(1f)
) {
val isPreview =
androidx.compose.ui.platform.LocalInspectionMode.current

if (isPreview) {//оставил для дебага, в рабочем коде бы убрал такое
Image(
painter = painterResource(R.drawable.ic_launcher_background),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(20.dp))
)
} else {
AsyncImage(
model = productState.image,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(20.dp))
)
}
if (productState.hasDiscount) {
Box(
modifier = Modifier
.align(alignment = Alignment.TopEnd)
.padding(vertical = 4.dp, horizontal = 10.dp)
.padding(8.dp)
.background(
brush = Brush.linearGradient(
listOf(Color(0xFFCE93D8), Color(0xFF7E57C2))
),
shape = RoundedCornerShape(40.dp)
)
.border(2.dp, Color.White, RoundedCornerShape(40.dp))
.padding(horizontal = 10.dp, vertical = 4.dp)
) {
Text(
text = productState.discount,
fontSize = 14.sp,
color = Color.White,
modifier = Modifier
)
}
}
}
Column(
modifier = Modifier
.weight(1f)
) {
Text(
text = productState.name,
maxLines = 2,
fontSize = 18.sp,
fontFamily = FontFamily.SansSerif,
color = Color.Black,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(horizontal = 12.dp)

)

Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) {
Text(
text = stringResource(R.string.price_with_arg, productState.price),
fontSize = 16.sp,
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight.Bold,
color = colorResource(ru.otus.common.ui.R.color.purple_500),
modifier = Modifier
.align(Alignment.BottomEnd)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFFFFF3E0),
Color(0xFFFFF3E0)
)
),
shape = RoundedCornerShape(4.dp)
)
.padding(horizontal = 12.dp, vertical = 8.dp)
)
}
}
}
}

@Preview
@Composable
fun ProductItemPreview() {
ProductItem(
ProductState(
id = "11",
name = "Персиковый кранч",
image = "https://otus-android.github.io/static/compose-hw1/img/product01.webp",
price = "1213",
hasDiscount = true,
discount = "12 %",
),
onClick = {}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,17 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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.common.Loader
import ru.otus.marketsample.databinding.FragmentProductListBinding
import ru.otus.marketsample.products.feature.adapter.ProductsAdapter
import ru.otus.marketsample.products.compose.ProductsList
import ru.otus.marketsample.products.feature.di.DaggerProductListComponent
import javax.inject.Inject

Expand Down Expand Up @@ -55,61 +53,28 @@ class ProductListFragment : Fragment() {
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)
}
}
}
binding.productsComposeView.setContent {
val state by viewModel.state.collectAsState()

when {
state.isLoading -> Loader()
state.hasError -> Toast.makeText(
requireContext(),
"Error wile loading data",
Toast.LENGTH_SHORT
).show()
else -> ProductsList(state, onRefresh = { viewModel.refresh()},
onClick = { productId ->
requireActivity().findNavController(R.id.nav_host_activity_main)
.navigate(
resId = R.id.action_main_to_details,
args = bundleOf("productId" to productId),
)
})
}
}
}

private fun showProductList(productListState: List<ProductState>) {
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
Expand Down
Loading