Pertemuan 11 - Aplikasi Marketplace Mahasiswa
Nama: Kadek Fajar Pramartha Yasodana
NRP: 5025231185
Kelas: PPB C
Pertemuan 11 - Aplikasi Marketplace Siswa
Didalam tugas kali ini, kita diminta untuk membuat sebuah marketplace simple yang bertujuan untuk berlatih dalam menggunakan material design. Untuk project yang saya buat berdasarkan basis dari yang diberikan pada blog yaitu https://drive.google.com/file/d/10OHSlACMjyyy-XQuVz_VHKgjAXiinHDU/view?usp=drive_link. Terdapat beberapa perubahan yang telah saya lakukan yaitu, dengan tidak menggunakan hard coded color dan menggunakan color sesuai theme. Menambahkan scaffold untuk inner padding karena itu cukup standar agar aplikasi tidak terlihat buruk pada saat penggunaan asli. Selain itu, mempercantik UI sedikit.
package com.fajary.marketplacemahasiswa
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
data class Product(
val id: Long = System.currentTimeMillis(),
val name: String,
val price: String,
val description: String
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MarketplaceTheme {
MainScreen()
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
var currentScreen by remember { mutableStateOf("home") }
val productList = remember { mutableStateListOf<Product>() }
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
if (productList.isEmpty()) {
productList.add(Product(name = "Brownies Lumer", price = "15000", description = "Cokelat melimpah."))
productList.add(Product(name = "Kaos Custom", price = "85000", description = "Bahan adem."))
}
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
if (currentScreen == "add") "Tambah Produk" else "MarketSiswa",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer
),
navigationIcon = {
if (currentScreen == "add") {
IconButton(onClick = { currentScreen = "home" }) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
}
}
)
},
bottomBar = {
NavigationBar(containerColor = MaterialTheme.colorScheme.surfaceVariant) {
NavigationBarItem(
selected = currentScreen == "home",
onClick = { currentScreen = "home" },
label = { Text("Home") },
icon = { Icon(Icons.Default.Home, contentDescription = null) }
)
NavigationBarItem(
selected = currentScreen == "profile",
onClick = { currentScreen = "profile" },
label = { Text("Profile") },
icon = { Icon(Icons.Default.Person, contentDescription = null) }
)
}
},
floatingActionButton = {
if (currentScreen == "home") {
ExtendedFloatingActionButton(
onClick = { currentScreen = "add" },
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
shape = RoundedCornerShape(50)
) {
Icon(Icons.Default.Add, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text("Jual", fontWeight = FontWeight.Bold)
}
}
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
when (currentScreen) {
"home" -> HomeScreen(productList)
"add" -> AddProductScreen(
onProductAdded = { newProduct ->
productList.add(0, newProduct)
scope.launch {
currentScreen = "home"
snackbarHostState.showSnackbar("Produk berhasil ditambahkan!")
}
}
)
"profile" -> ProfileScreen()
}
}
}
}
@Composable
fun HomeScreen(products: List<Product>) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
Card(
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.primaryContainer,
shape = RoundedCornerShape(20.dp)
)
.padding(24.dp)
) {
Column {
Text(
"Halo, Siswa!",
fontSize = 28.sp,
fontWeight = FontWeight.ExtraBold,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"Mau belanja apa hari ini?",
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),
fontSize = 16.sp
)
}
}
}
}
if (products.isEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
Icons.Default.ShoppingCart,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.outline
)
Spacer(modifier = Modifier.height(12.dp))
Text(
"Belum ada produk",
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.titleMedium
)
}
}
}
}
items(products, key = { it.id }) { product ->
Card(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.secondaryContainer),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.ShoppingCart,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.size(24.dp)
)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
product.name,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(4.dp))
Text(
product.description,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 14.sp,
maxLines = 2
)
}
Text(
"Rp ${product.price}",
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
}
@Composable
fun AddProductScreen(onProductAdded: (Product) -> Unit) {
var name by remember { mutableStateOf("") }
var price by remember { mutableStateOf("") }
var desc by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
Text(
"Isi detail produk yang ingin dijual",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Nama Produk") },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline
),
singleLine = true
)
OutlinedTextField(
value = price,
onValueChange = { price = it },
label = { Text("Harga (Rp)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline
),
singleLine = true
)
OutlinedTextField(
value = desc,
onValueChange = { desc = it },
label = { Text("Deskripsi") },
modifier = Modifier.fillMaxWidth(),
minLines = 3,
shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline
)
)
Button(
onClick = {
isLoading = true
scope.launch {
delay(1000) // Simulasi loading (FR-04)
onProductAdded(Product(name = name, price = price, description = desc))
isLoading = false
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = name.isNotBlank() && price.isNotBlank() && !isLoading,
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
disabledContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
)
} else {
Icon(
Icons.Default.Add,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Simpan Produk", fontWeight = FontWeight.Bold)
}
}
}
}
@Composable
fun ProfileScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Card(
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Avatar circle
Box(
modifier = Modifier
.size(100.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.secondaryContainer),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(60.dp),
tint = MaterialTheme.colorScheme.onSecondaryContainer
)
}
Spacer(modifier = Modifier.height(24.dp))
Text(
"Kadek Fajar Pramartha Yasodana",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"PPB C",
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Medium,
fontSize = 16.sp
)
}
}
}
}
@Composable
fun MarketplaceTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = lightColorScheme(
primary = Color(0xFF6B8F71),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFD7E8D5),
onPrimaryContainer = Color(0xFF1B3A22),
secondary = Color(0xFFB2A76C),
onSecondary = Color(0xFF000000),
secondaryContainer = Color(0xFFEFEDD3),
onSecondaryContainer = Color(0xFF2B2A15),
background = Color(0xFFF6F5EF),
onBackground = Color(0xFF1C1B16),
surface = Color(0xFFFCFBF5),
onSurface = Color(0xFF1C1B16),
surfaceVariant = Color(0xFFE0E1D6),
onSurfaceVariant = Color(0xFF45483F),
outline = Color(0xFF75786E)
),
content = content
)
}
@Preview(showBackground = true)
@Composable
fun MainScreenPreview() {
MarketplaceTheme {
MainScreen()
}
}
Dalam penjelasan kodenya, saya akan menjelaskan dalam style seperti kita membuat sebuah website, karena sebenarnya logikanya tidak jauh berbeda.
Product Class
Product merupakan data class yang digunakan untuk menyimpan informasi produk seperti id, name, price, dan description. Konsep ini mirip seperti object atau interface pada React/Next.js yang digunakan sebagai struktur data utama aplikasi.
MainActivity
MainActivity berfungsi sebagai entry point aplikasi Android. Di dalam function onCreate(), aplikasi akan menjalankan MarketplaceTheme dan menampilkan MainScreen() sebagai halaman utama. Konsepnya mirip seperti root layout atau halaman utama pada Next.js.
MainScreen
MainScreen() merupakan composable utama yang mengatur navigasi dan state aplikasi. Function ini menggunakan currentScreen untuk menentukan halaman aktif seperti Home, Add Product, atau Profile. Selain itu, terdapat productList yang digunakan untuk menyimpan daftar produk secara reaktif sehingga tampilan otomatis diperbarui ketika data berubah.
LaunchedEffect(Unit) digunakan untuk menjalankan proses inisialisasi data ketika halaman pertama kali dimuat. Mekanismenya mirip seperti useEffect(() => {}, []) pada React.
Scaffold digunakan sebagai struktur layout utama aplikasi yang berisi topBar, bottomBar, floatingActionButton, dan content utama. Konsep ini mirip seperti layout wrapper pada website modern menggunakan React atau Next.js.
HomeScreen
HomeScreen() berfungsi untuk menampilkan daftar produk menggunakan LazyColumn. Komponen ini bekerja seperti .map() pada React untuk merender list data secara dinamis dan efisien. Jika daftar produk kosong, aplikasi akan menampilkan empty state sebagai informasi kepada pengguna.
AddProductScreen
AddProductScreen() digunakan untuk menambahkan produk baru melalui form input. State seperti name, price, dan desc digunakan untuk menyimpan input pengguna secara real-time, mirip seperti useState pada React. Tombol simpan akan aktif hanya jika data valid dan akan menampilkan loading indicator ketika proses penyimpanan berlangsung.
ProfileScreen
ProfileScreen() berfungsi menampilkan informasi profil pengguna dalam bentuk card sederhana yang berisi avatar, nama, dan kelas pengguna.
MarketplaceTheme
MarketplaceTheme() digunakan untuk mengatur tema global aplikasi seperti warna utama, background, dan surface. Konsep ini mirip seperti theme provider atau konfigurasi styling global pada React dan Tailwind CSS.




Comments
Post a Comment