Tugas 11 - Pemrograman Perangkat Bergerak G - Redesign Starbucks App
Moch. Avin (5025221061)
Pemrograman Perangkat Bergerak - PPB GLink Repo Github: https://github.com/mochavin/ppb-redesign-starbucks
Arsitektur Aplikasi
UI Layer (Jetpack Compose): Seluruh antarmuka pengguna dibangun secara deklaratif menggunakan Jetpack Compose. Ini memungkinkan pembuatan UI yang dinamis dan interaktif dengan kode Kotlin yang lebih sedikit dan lebih intuitif.
ViewModel (Komponen Arsitektur Android): CartViewModel digunakan untuk mengelola state dan logika bisnis yang terkait dengan keranjang belanja. Ini untuk memisahkan logika UI dari logika data, membuat kode lebih terstruktur dan mudah diuji. Navigation (Jetpack Navigation Compose): Navigasi antar layar dikelola menggunakan komponen Navigation untuk Compose. Ini menyediakan cara yang terstruktur untuk mendefinisikan alur navigasi aplikasi. Data Models (Kotlin Data Classes): Entitas data seperti Product, Category, Size, dan CartItem direpresentasikan menggunakan Kotlin data class.
1. Model Data (Product.kt)
data class Product(
val id: String,
val name: String,
val description: String,
val price: Double,
val imageUrl: String,
val category: Category,
val size: Size = Size.MEDIUM,
val isAvailable: Boolean = true
)
enum class Category {
COFFEE, TEA, FRAPPUCCINO, FOOD, MERCHANDISE
}
enum class Size(val displayName: String, val priceMultiplier: Double) {
SMALL("Small", 0.9),
MEDIUM("Medium", 1.0),
LARGE("Large", 1.2)
}
data class CartItem(
val product: Product,
val quantity: Int = 1,
val size: Size = Size.MEDIUM,
val customizations: List<String> = emptyList()
)
2. ViewModel untuk Keranjang (CartViewModel.kt)
class CartViewModel : ViewModel() {
private val _cartItems = mutableStateListOf<CartItem>()
val cartItems: List<CartItem> = _cartItems
fun addToCart(product: Product, size: Size, quantity: Int) {
val existingItem = _cartItems.find {
it.product.id == product.id && it.size == size
}
if (existingItem != null) {
val index = _cartItems.indexOf(existingItem)
_cartItems[index] = existingItem.copy(quantity = existingItem.quantity + quantity)
} else {
_cartItems.add(CartItem(product, quantity, size))
}
}
fun updateQuantity(cartItem: CartItem, newQuantity: Int) {
if (newQuantity <= 0) {
removeFromCart(cartItem)
} else {
val index = _cartItems.indexOf(cartItem)
if (index != -1) {
_cartItems[index] = cartItem.copy(quantity = newQuantity)
}
}
}
fun removeFromCart(cartItem: CartItem) {
_cartItems.remove(cartItem)
}
fun clearCart() {
_cartItems.clear()
}
fun getTotalItems(): Int {
return _cartItems.sumOf { it.quantity }
}
fun getTotalPrice(): Double {
return _cartItems.sumOf { it.product.price * it.size.priceMultiplier * it.quantity }
}
}
3. Layar Utama (HomeScreen.kt)
@Composable
fun HomeScreen(
onProductClick: (Product) -> Unit = {},
onCartClick: () -> Unit = {}
) {
var selectedCategory by remember { mutableStateOf(Category.COFFEE) }
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
// Top bar
TopAppBar(
title = {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.LocationOn,
contentDescription = "Location",
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
"Selamat Pagi",
fontSize = 12.sp,
color = Color.Gray
)
Text(
"Sukolilo, Surabaya",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold
)
}
}
},
actions = {
IconButton(onClick = onCartClick) {
Icon(
Icons.Default.ShoppingCart,
contentDescription = "Cart"
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.White
)
)
// Content
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
// Welcome Section
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color(0xFF00704A)),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier.padding(20.dp)
) {
Text(
"Welcome to Starbucks",
color = Color.White,
fontSize = 22.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Discover your favorite coffee and treats",
color = Color.White.copy(alpha = 0.8f),
fontSize = 14.sp
)
}
}
}
item {
// Category Selection
Text(
"Categories",
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(12.dp))
LazyRow(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(Category.values()) { category ->
CategoryChip(
category = category,
isSelected = category == selectedCategory,
onClick = { selectedCategory = category }
)
}
}
}
item {
Text(
"Popular Items",
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold
)
}
items(
SampleData.sampleProducts.filter { it.category == selectedCategory }
) { product ->
ProductCard(
product = product,
onClick = { onProductClick(product) }
)
}
}
}
}
@Composable
fun CategoryChip(
category: Category,
isSelected: Boolean,
onClick: () -> Unit
) {
val backgroundColor = if (isSelected) {
Color(0xFF00704A)
} else {
Color.Gray.copy(alpha = 0.1f)
}
val textColor = if (isSelected) {
Color.White
} else {
Color.Gray
}
Box(
modifier = Modifier
.clip(RoundedCornerShape(20.dp))
.background(backgroundColor)
.clickable { onClick() }
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(
text = category.name.lowercase().replaceFirstChar { it.uppercase() },
color = textColor,
fontSize = 14.sp,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal
)
}
}
@Composable
fun ProductCard(
product: Product,
onClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() },
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Product Image Placeholder
Box(
modifier = Modifier
.size(80.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color.Gray.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Coffee,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = Color(0xFF00704A)
)
}
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(
product.name,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
product.description,
fontSize = 12.sp,
color = Color.Gray,
maxLines = 2
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"$${String.format("%.2f", product.price)}",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF00704A)
)
}
Icon(
Icons.Default.Add,
contentDescription = "Add to cart",
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.background(Color(0xFF00704A))
.padding(6.dp),
tint = Color.White
)
}
}
}
4. Layar Detail Produk (ProductDetailScreen.kt)
@Composable
fun ProductDetailScreen(
product: Product,
onBackClick: () -> Unit = {},
onAddToCart: (Product, Size, Int) -> Unit = { _, _, _ -> }
) {
var selectedSize by remember { mutableStateOf(Size.MEDIUM) }
var quantity by remember { mutableStateOf(1) }
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
// Top bar
TopAppBar(
title = { Text("Product Details") },
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.White
)
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
// Product Image Placeholder
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(16.dp))
.background(Color.Gray.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Coffee,
contentDescription = null,
modifier = Modifier.size(80.dp),
tint = Color(0xFF00704A)
)
}
// Product Info
Column {
Text(
product.name,
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
product.description,
fontSize = 16.sp,
color = Color.Gray,
lineHeight = 22.sp
)
}
// Size Selection
Column {
Text(
"Size",
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(12.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Size.values().forEach { size ->
SizeChip(
size = size,
isSelected = size == selectedSize,
onClick = { selectedSize = size }
)
}
}
}
// Quantity Selection
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Quantity",
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
IconButton(
onClick = { if (quantity > 1) quantity-- },
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(Color.Gray.copy(alpha = 0.2f))
) {
Icon(Icons.Default.Remove, contentDescription = "Decrease")
}
Text(
quantity.toString(),
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold
)
IconButton(
onClick = { quantity++ },
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(Color.Gray.copy(alpha = 0.2f))
) {
Icon(Icons.Default.Add, contentDescription = "Increase")
}
}
}
Spacer(modifier = Modifier.weight(1f))
// Price and Add to Cart
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
"Total Price",
fontSize = 14.sp,
color = Color.Gray
)
Text(
"$${String.format("%.2f", product.price * selectedSize.priceMultiplier * quantity)}",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF00704A)
)
}
Button(
onClick = { onAddToCart(product, selectedSize, quantity) },
modifier = Modifier
.height(56.dp)
.width(160.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00704A)
),
shape = RoundedCornerShape(28.dp)
) {
Text(
"Add to Cart",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
}
}
}
}
}
@Composable
fun SizeChip(
size: Size,
isSelected: Boolean,
onClick: () -> Unit
) {
val backgroundColor = if (isSelected) {
Color(0xFF00704A)
} else {
Color.Gray.copy(alpha = 0.1f)
}
val textColor = if (isSelected) {
Color.White
} else {
Color.Gray
}
FilterChip(
onClick = onClick,
label = {
Text(
size.displayName,
color = textColor,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal
)
},
selected = isSelected,
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = Color(0xFF00704A),
containerColor = Color.Gray.copy(alpha = 0.1f)
)
)
}
5. Layar Keranjang (CartScreen.kt)
@Composable
fun CartScreen(
cartItems: List<CartItem>,
onBackClick: () -> Unit = {},
onUpdateQuantity: (CartItem, Int) -> Unit = { _, _ -> },
onRemoveItem: (CartItem) -> Unit = {},
onCheckout: () -> Unit = {}
) {
val total = cartItems.sumOf { it.product.price * it.size.priceMultiplier * it.quantity }
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
// Top bar
TopAppBar(
title = { Text("Cart") },
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.White
)
)
if (cartItems.isEmpty()) {
// Empty cart state
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
Icons.Default.ShoppingCart,
contentDescription = null,
modifier = Modifier.size(80.dp),
tint = Color.Gray.copy(alpha = 0.5f)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"Your cart is empty",
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = Color.Gray
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Add some delicious items to get started",
fontSize = 14.sp,
color = Color.Gray
)
}
} else {
// Cart with items
Column(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(cartItems) { cartItem ->
CartItemCard(
cartItem = cartItem,
onUpdateQuantity = { quantity -> onUpdateQuantity(cartItem, quantity) },
onRemove = { onRemoveItem(cartItem) }
)
}
}
// Bottom section with total and checkout
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier.padding(20.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
"Total",
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold
)
Text(
"$${String.format("%.2f", total)}",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF00704A)
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onCheckout,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00704A)
),
shape = RoundedCornerShape(28.dp)
) {
Text(
"Checkout",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
}
}
}
}
}
}
}
@Composable
fun CartItemCard(
cartItem: CartItem,
onUpdateQuantity: (Int) -> Unit,
onRemove: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Product Image Placeholder
Box(
modifier = Modifier
.size(60.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color.Gray.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Coffee,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = Color(0xFF00704A)
)
}
Spacer(modifier = Modifier.width(12.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(
cartItem.product.name,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
cartItem.size.displayName,
fontSize = 12.sp,
color = Color.Gray
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"$${String.format("%.2f", cartItem.product.price * cartItem.size.priceMultiplier)}",
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF00704A)
)
}
// Quantity controls
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
IconButton(
onClick = {
if (cartItem.quantity > 1) {
onUpdateQuantity(cartItem.quantity - 1)
} else {
onRemove()
}
},
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.background(Color.Gray.copy(alpha = 0.2f))
) {
Icon(
if (cartItem.quantity > 1) Icons.Default.Remove else Icons.Default.Delete,
contentDescription = if (cartItem.quantity > 1) "Decrease" else "Remove",
modifier = Modifier.size(16.dp)
)
}
Text(
cartItem.quantity.toString(),
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
IconButton(
onClick = { onUpdateQuantity(cartItem.quantity + 1) },
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.background(Color.Gray.copy(alpha = 0.2f))
) {
Icon(
Icons.Default.Add,
contentDescription = "Increase",
modifier = Modifier.size(16.dp)
)
}
}
}
}
}

Comments
Post a Comment