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
14 changes: 14 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.compose.compiler)
}

android {
Expand Down Expand Up @@ -35,6 +36,12 @@ android {
buildFeatures {
viewBinding true
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.15"
}
}

dependencies {
Expand Down Expand Up @@ -62,6 +69,13 @@ dependencies {
implementation libs.kotlinx.serializationJson
implementation libs.androidx.datastore
implementation libs.androidx.datastore.preferences
implementation platform(libs.compose.bom)
implementation libs.material3
implementation libs.compose.preview
implementation libs.compose.activity
implementation libs.compose.viewmodel
debugImplementation libs.androidx.ui.tooling
implementation libs.coil.compose

implementation libs.dagger
kapt libs.daggerCompiler
Expand Down
78 changes: 77 additions & 1 deletion app/src/main/java/ru/otus/marketsample/MainFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,80 @@ class MainFragment : Fragment() {
super.onDestroyView()
_binding = null
}
}
}

//@Serializable
//sealed class Screen(
// @StringRes val title: Int,
// @DrawableRes val icon: Int
//) {
// @Serializable
// object ProductList : Screen(
// title = R.string.title_products,
// icon = ru.otus.common.ui.R.drawable.ic_list
// )
//
// @Serializable
// object PromoList : Screen(
// title = R.string.title_promo,
// icon = ru.otus.common.ui.R.drawable.ic_discount
// )
//
// @Serializable
// data class ProductDetails(val id: String)
//}
//
//@Composable
//fun MainScreen() {
// val navController = rememberNavController()
// val bottomNavigationItems = listOf(ProductList, PromoList)
// val navBackStackEntry by navController.currentBackStackEntryAsState()
// val currentRoute = navBackStackEntry?.destination
//
// Scaffold(
// bottomBar = {
// NavigationBar {
// bottomNavigationItems.forEach { navigationItem ->
// NavigationBarItem(
// icon = {
// Icon(
// painter = painterResource(navigationItem.icon),
// contentDescription = null
// )
// },
// label = {
// Text(stringResource(navigationItem.title))
// },
// selected = currentRoute == navigationItem,
// onClick = {
// navController.navigate(navigationItem)
// }
// )
// }
// }
// },
// ) { padding ->
// NavHost(
// navController,
// startDestination = ProductList,
// modifier = Modifier.padding(padding)
// ) {
// composable<ProductList> {
// ProductListScreen(
// navigateToDetails = { productId ->
// navController.navigate(
// route = ProductDetails(productId)
// )
// }
// )
// }
// composable<PromoList> {
// PromoListScreen()
// }
// composable<ProductDetails> { backStackEntry ->
// val productId: String = backStackEntry.toRoute()
// DetailsScreen(productId = productId)
// }
// }
// }
//}
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,51 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
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 ru.otus.marketsample.details.feature.di.DaggerDetailsComponent
import javax.inject.Inject

class DetailsFragment : Fragment() {

private var _binding: FragmentDetailsBinding? = null
private val binding get() = _binding!!

@Inject
lateinit var factory: DetailsViewModelFactory

Expand All @@ -48,78 +74,140 @@ class DetailsFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentDetailsBinding.inflate(inflater, container, false)
return binding.root
): View? {
return ComposeView(requireContext()).apply {
setContent {
DetailsScreen(viewModel)
}
}
}
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
@Composable
private fun DetailsScreen(viewModel: DetailsViewModel) {
val state by viewModel.state.collectAsStateWithLifecycle()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeUI()
}
DetailsContent(
state = state,
onErrorShown = viewModel::errorHasShown
)
}

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)
}
}
}
@Composable
private fun DetailsContent(
state: DetailsScreenState,
onErrorShown: () -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.fillMaxSize()
.background(Color(0xFFFFFFFF))
) {
when {
state.isLoading -> LoadingState(Modifier.align(Alignment.Center))
state.hasError -> {
ErrorState(modifier = Modifier.align(Alignment.BottomCenter), onErrorShown)
}

else -> ProductState(state.detailsState)
}
}
}

private fun showLoading() {
hideAll()
binding.progress.visibility = View.VISIBLE
}
@Composable
fun LoadingState(modifier: Modifier) {
CircularProgressIndicator(modifier = modifier)
}

private fun showProduct(detailsState: DetailsState) {
hideAll()
binding.image.load(detailsState.image)
binding.image.visibility = View.VISIBLE
@Composable
fun ErrorState(modifier: Modifier, onErrorShown: () -> Unit) {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }

binding.name.text = detailsState.name
binding.name.visibility = View.VISIBLE
LaunchedEffect(Unit) {
scope.launch {
snackbarHostState.showSnackbar(message = "Error while loading data")
}
onErrorShown()
}

binding.price.text = getString(R.string.price_with_arg, detailsState.price)
binding.price.visibility = View.VISIBLE
SnackbarHost(
hostState = snackbarHostState,
modifier = modifier
)
}

@Composable
private fun ProductState(detailsState: DetailsState) {
val priceBrush = Brush.linearGradient(
start = Offset(0f, Float.POSITIVE_INFINITY),
end = Offset(Float.POSITIVE_INFINITY, 0f),
colors = listOf(Color(0xFF6200EE), Color(0xFFBB86FC))
)

Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Top
) {
AsyncImage(
model = detailsState.image,
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(300.dp),
contentScale = ContentScale.Crop
)
Text(
text = detailsState.name,
color = Color(0xFF000000),
fontSize = 24.sp
)
if (detailsState.hasDiscount) {
binding.promo.visibility = View.VISIBLE
binding.promo.text = detailsState.discount
} else {
binding.promo.visibility = View.GONE
Text(
text = detailsState.discount,
modifier = Modifier
.align(Alignment.End)
.clip(RoundedCornerShape(20.dp, 2.dp, 10.dp, 20.dp))
.background(priceBrush)
.padding(horizontal = 10.dp),
color = Color(0xFFFFFFFF),
fontSize = 20.sp
)
}
Text(
text = stringResource(R.string.price_with_arg, detailsState.price),
modifier = Modifier
.align(Alignment.End)
.padding(14.dp),
color = Color(0xFF6200EE),
fontSize = 18.sp
)
Spacer(Modifier.size(10.dp))
Button(
onClick = { },
modifier = Modifier
.padding(14.dp)
.align(Alignment.End)
) {
Text(
text = "Add to Cart",
fontSize = 18.sp
)
}

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
}
@Preview
@Composable
private fun PreviewDetailsContent() {
DetailsContent(
state = DetailsScreenState(
detailsState = DetailsState(
name = "Product Name",
price = "244",
hasDiscount = true,
discount = "-50%"
)
), onErrorShown = {})
}
Loading