diff --git a/app/src/main/java/com/maruchin/domaindrivenandroid/MainNavGraph.kt b/app/src/main/java/com/maruchin/domaindrivenandroid/MainNavGraph.kt index a762efb..12edc8a 100644 --- a/app/src/main/java/com/maruchin/domaindrivenandroid/MainNavGraph.kt +++ b/app/src/main/java/com/maruchin/domaindrivenandroid/MainNavGraph.kt @@ -3,6 +3,8 @@ package com.maruchin.domaindrivenandroid import androidx.compose.runtime.Composable import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController +import com.maruchin.domaindrivenandroid.ui.couponPreview.couponPreviewScreen +import com.maruchin.domaindrivenandroid.ui.couponPreview.navigateToCouponPreview import com.maruchin.domaindrivenandroid.ui.home.HOME_ROUTE import com.maruchin.domaindrivenandroid.ui.home.homeScreen @@ -10,6 +12,11 @@ import com.maruchin.domaindrivenandroid.ui.home.homeScreen fun MainNavGraph() { val navController = rememberNavController() NavHost(navController = navController, startDestination = HOME_ROUTE) { - homeScreen() + homeScreen( + onOpenCoupon = { navController.navigateToCouponPreview(it) } + ) + couponPreviewScreen( + onBack = { navController.navigateUp() } + ) } } diff --git a/app/src/main/java/com/maruchin/domaindrivenandroid/data/ID.kt b/app/src/main/java/com/maruchin/domaindrivenandroid/data/ID.kt new file mode 100644 index 0000000..fecc127 --- /dev/null +++ b/app/src/main/java/com/maruchin/domaindrivenandroid/data/ID.kt @@ -0,0 +1,4 @@ +package com.maruchin.domaindrivenandroid.data + +@JvmInline +value class ID(val value: String) diff --git a/app/src/main/java/com/maruchin/domaindrivenandroid/data/coupon/Coupon.kt b/app/src/main/java/com/maruchin/domaindrivenandroid/data/coupon/Coupon.kt index 7ba4324..263c080 100644 --- a/app/src/main/java/com/maruchin/domaindrivenandroid/data/coupon/Coupon.kt +++ b/app/src/main/java/com/maruchin/domaindrivenandroid/data/coupon/Coupon.kt @@ -1,10 +1,11 @@ package com.maruchin.domaindrivenandroid.data.coupon +import com.maruchin.domaindrivenandroid.data.ID import java.net.URL import java.util.Currency data class Coupon( - val id: String, + val id: ID, val name: String, val price: Money, val image: URL, @@ -12,31 +13,31 @@ data class Coupon( val sampleCoupons = listOf( Coupon( - id = "1", + id = ID("1"), name = "Cheesburger with fries", price = Money(value = 17.99, currency = Currency.getInstance("USD")), image = URL("https://raw.githubusercontent.com/Maruchin1/domain-driven-android/master/images/cheesburger_with_fries_coupon.jpeg"), ), Coupon( - id = "2", + id = ID("2"), name = "Chicekburger with fries", price = Money(value = 15.99, currency = Currency.getInstance("USD")), image = URL("https://raw.githubusercontent.com/Maruchin1/domain-driven-android/master/images/chickenburger_with_fries_coupon.jpeg"), ), Coupon( - id = "3", + id = ID("3"), name = "Chicken nuggets with fries", price = Money(value = 20.99, currency = Currency.getInstance("USD")), image = URL("https://raw.githubusercontent.com/Maruchin1/domain-driven-android/master/images/chicken_nuggets_with_fries_coupon.jpeg"), ), Coupon( - id = "4", + id = ID("4"), name = "2 x Milkshake", price = Money(value = 8.99, currency = Currency.getInstance("USD")), image = URL("https://raw.githubusercontent.com/Maruchin1/domain-driven-android/master/images/two_milkshakes_coupon.jpeg"), ), Coupon( - id = "5", + id = ID("5"), name = "2 x Soda drink", price = Money(value = 6.99, currency = Currency.getInstance("USD")), image = URL("https://raw.githubusercontent.com/Maruchin1/domain-driven-android/master/images/two_soda_drinks_coupon.jpeg"), diff --git a/app/src/main/java/com/maruchin/domaindrivenandroid/data/coupon/CouponsRepository.kt b/app/src/main/java/com/maruchin/domaindrivenandroid/data/coupon/CouponsRepository.kt index 04d6cf3..502b356 100644 --- a/app/src/main/java/com/maruchin/domaindrivenandroid/data/coupon/CouponsRepository.kt +++ b/app/src/main/java/com/maruchin/domaindrivenandroid/data/coupon/CouponsRepository.kt @@ -1,5 +1,6 @@ package com.maruchin.domaindrivenandroid.data.coupon +import com.maruchin.domaindrivenandroid.data.ID import javax.inject.Inject import javax.inject.Singleton @@ -9,4 +10,8 @@ class CouponsRepository @Inject constructor() { suspend fun getAllCoupons(): List { return sampleCoupons } + + suspend fun getCoupon(id: ID): Coupon? { + return sampleCoupons.find { it.id == id } + } } diff --git a/app/src/main/java/com/maruchin/domaindrivenandroid/ui/couponPreview/CouponPreviewDestination.kt b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/couponPreview/CouponPreviewDestination.kt new file mode 100644 index 0000000..d88ae8d --- /dev/null +++ b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/couponPreview/CouponPreviewDestination.kt @@ -0,0 +1,29 @@ +package com.maruchin.domaindrivenandroid.ui.couponPreview + +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.maruchin.domaindrivenandroid.data.ID + +const val COUPON_PREVIEW_ROUTE = "coupon-preview" +const val COUPON_IN = "couponId" + +fun NavGraphBuilder.couponPreviewScreen(onBack: () -> Unit) { + composable("$COUPON_PREVIEW_ROUTE/{$COUPON_IN}") { + val couponId = ID(it.arguments?.getString(COUPON_IN) ?: "") + val viewModel = hiltViewModel() + val state by viewModel.uiState.collectAsState() + LaunchedEffect(Unit) { + viewModel.selectCoupon(couponId) + } + CouponPreviewScreen(state = state, onBack = onBack, onCollect = {}) + } +} + +fun NavController.navigateToCouponPreview(couponId: ID) { + navigate("$COUPON_PREVIEW_ROUTE/${couponId.value}") +} diff --git a/app/src/main/java/com/maruchin/domaindrivenandroid/ui/couponPreview/CouponPreviewScreen.kt b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/couponPreview/CouponPreviewScreen.kt new file mode 100644 index 0000000..de47fb8 --- /dev/null +++ b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/couponPreview/CouponPreviewScreen.kt @@ -0,0 +1,80 @@ +package com.maruchin.domaindrivenandroid.ui.couponPreview + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CouponPreviewScreen(state: CouponPreviewUiState, onBack: () -> Unit, onCollect: () -> Unit) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = "My Coupons") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.Outlined.ArrowBack, + contentDescription = "Navigate up", + ) + } + }, + ) + } + ) { paddingValues -> + when (state) { + CouponPreviewUiState.Loading -> {} + is CouponPreviewUiState.Ready -> { + Column(modifier = Modifier.padding(paddingValues)) { + OutlinedCard(modifier = Modifier.padding(12.dp)) { + AsyncImage( + model = state.imageUrl, + contentDescription = null, + modifier = Modifier.fillMaxWidth(), + contentScale = ContentScale.FillWidth, + ) + } + Text( + text = state.couponName, + style = MaterialTheme.typography.displaySmall, + modifier = Modifier.padding(vertical = 12.dp, horizontal = 20.dp) + ) + Text( + text = state.price, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(vertical = 12.dp, horizontal = 20.dp), + ) + Spacer(modifier = Modifier.weight(1f)) + Button( + onClick = onCollect, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp, horizontal = 12.dp), + ) { + Text(text = "Collect".uppercase()) + } + } + } + } + } +} diff --git a/app/src/main/java/com/maruchin/domaindrivenandroid/ui/couponPreview/CouponPreviewUiState.kt b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/couponPreview/CouponPreviewUiState.kt new file mode 100644 index 0000000..846db91 --- /dev/null +++ b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/couponPreview/CouponPreviewUiState.kt @@ -0,0 +1,23 @@ +package com.maruchin.domaindrivenandroid.ui.couponPreview + +import androidx.compose.runtime.Immutable +import com.maruchin.domaindrivenandroid.data.coupon.Coupon + +@Immutable +sealed class CouponPreviewUiState { + + object Loading : CouponPreviewUiState() + + class Ready( + val imageUrl: String, + val couponName: String, + val price: String, + ) : CouponPreviewUiState() { + + constructor(coupon: Coupon) : this( + imageUrl = coupon.image.toString(), + couponName = coupon.name, + price = coupon.price.toString(), + ) + } +} diff --git a/app/src/main/java/com/maruchin/domaindrivenandroid/ui/couponPreview/CouponPreviewViewModel.kt b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/couponPreview/CouponPreviewViewModel.kt new file mode 100644 index 0000000..ede2d10 --- /dev/null +++ b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/couponPreview/CouponPreviewViewModel.kt @@ -0,0 +1,29 @@ +package com.maruchin.domaindrivenandroid.ui.couponPreview + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.maruchin.domaindrivenandroid.data.ID +import com.maruchin.domaindrivenandroid.data.coupon.CouponsRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class CouponPreviewViewModel @Inject constructor( + private val couponsRepository: CouponsRepository, +) : ViewModel() { + + private val _uiState = MutableStateFlow(CouponPreviewUiState.Loading) + val uiState = _uiState.asStateFlow() + + fun selectCoupon(couponId: ID) = viewModelScope.launch { + couponsRepository.getCoupon(couponId)?.let { coupon -> + _uiState.update { + CouponPreviewUiState.Ready(coupon) + } + } + } +} diff --git a/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeDestination.kt b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeDestination.kt index 5d3677f..b6d3e55 100644 --- a/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeDestination.kt +++ b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeDestination.kt @@ -5,13 +5,14 @@ import androidx.compose.runtime.getValue import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import com.maruchin.domaindrivenandroid.data.ID const val HOME_ROUTE = "home" -fun NavGraphBuilder.homeScreen() { +fun NavGraphBuilder.homeScreen(onOpenCoupon: (ID) -> Unit) { composable(HOME_ROUTE) { val viewModel = hiltViewModel() val state by viewModel.uiState.collectAsState() - HomeScreen(state = state) + HomeScreen(state = state, onOpenCoupon = onOpenCoupon) } } diff --git a/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeScreen.kt b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeScreen.kt index 3b72eea..9499c90 100644 --- a/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeScreen.kt @@ -24,18 +24,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import com.maruchin.domaindrivenandroid.data.coupon.Coupon -import com.maruchin.domaindrivenandroid.data.coupon.sampleCoupons -import com.maruchin.domaindrivenandroid.ui.theme.DomainDrivenAndroidTheme +import com.maruchin.domaindrivenandroid.data.ID @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HomeScreen(state: HomeUiState) { +fun HomeScreen(state: HomeUiState, onOpenCoupon: (ID) -> Unit) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() Scaffold( topBar = { @@ -53,7 +48,7 @@ fun HomeScreen(state: HomeUiState) { .nestedScroll(scrollBehavior.nestedScrollConnection), ) { items(state.coupons) { coupon -> - CouponView(coupon = coupon) + CouponView(state = coupon, onClick = { onOpenCoupon(coupon.id) }) } } } @@ -61,20 +56,20 @@ fun HomeScreen(state: HomeUiState) { @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun CouponView(coupon: Coupon) { +private fun CouponView(state: CouponUiState, onClick: () -> Unit) { val density = LocalDensity.current.density var couponNamePadding by remember { mutableStateOf(0.dp) } - OutlinedCard(onClick = { /*TODO*/ }, modifier = Modifier.padding(6.dp)) { + OutlinedCard(onClick = onClick, modifier = Modifier.padding(6.dp)) { Column { AsyncImage( - model = coupon.image.toString(), + model = state.imageUrl, contentDescription = null, modifier = Modifier .fillMaxWidth() .aspectRatio(1f / 1f), ) Text( - text = coupon.name, + text = state.couponName, style = MaterialTheme.typography.titleMedium, maxLines = 2, modifier = Modifier @@ -87,7 +82,7 @@ private fun CouponView(coupon: Coupon) { } ) Text( - text = coupon.price.toString(), + text = state.price, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.SemiBold, @@ -96,18 +91,3 @@ private fun CouponView(coupon: Coupon) { } } } - -@Preview -@Composable -private fun HomeScreenPreview(@PreviewParameter(HomeUiStateProvider::class) state: HomeUiState) { - DomainDrivenAndroidTheme { - HomeScreen(state = state) - } -} - -class HomeUiStateProvider : PreviewParameterProvider { - override val values = sequenceOf( - HomeUiState(), - HomeUiState(coupons = sampleCoupons, loading = false) - ) -} diff --git a/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeUiState.kt b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeUiState.kt index 8af32d9..e610d0c 100644 --- a/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeUiState.kt +++ b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeUiState.kt @@ -1,10 +1,27 @@ package com.maruchin.domaindrivenandroid.ui.home import androidx.compose.runtime.Immutable +import com.maruchin.domaindrivenandroid.data.ID import com.maruchin.domaindrivenandroid.data.coupon.Coupon @Immutable data class HomeUiState( - val coupons: List = emptyList(), + val coupons: List = emptyList(), val loading: Boolean = true, ) + +@Immutable +data class CouponUiState( + val id: ID, + val imageUrl: String, + val couponName: String, + val price: String, +) { + + constructor(coupon: Coupon) : this( + id = coupon.id, + imageUrl = coupon.image.toString(), + couponName = coupon.name, + price = coupon.price.toString(), + ) +} diff --git a/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeViewModel.kt b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeViewModel.kt index a8c2dc2..8d29c6f 100644 --- a/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/maruchin/domaindrivenandroid/ui/home/HomeViewModel.kt @@ -28,7 +28,7 @@ class HomeViewModel @Inject constructor( } val coupons = couponsRepository.getAllCoupons() _uiState.update { - it.copy(loading = false, coupons = coupons) + it.copy(loading = false, coupons = coupons.map(::CouponUiState)) } } }