Tugas 9 - Aplikasi Dessert Clicker

Aplikasi Dessert Clicker

Ricardo Supriyanto - 5025221218

Pada tugas pertemuan 9, saya mencoba untuk memberikan penambahan fitur sebuah aplikasi Android yang diberi nama "Dessert Clicker". Inspirasi utama dan panduan desain untuk aplikasi ini berasal dari prinsip-prinsip Material Design, dengan tujuan menciptakan antarmuka pengguna (UI) yang tidak hanya fungsional tetapi juga menarik secara visual, intuitif, dan konsisten. Aplikasi ini merupakan implementasi dan pengembangan dari sebuah codelab yang fokus pada pemahaman siklus hidup aktivitas (Activity Lifecycle).

Aplikasi "Dessert Clicker" ini dibangun sepenuhnya menggunakan Jetpack Compose, toolkit UI modern dari Android yang memungkinkan pengembangan UI secara deklaratif dengan Kotlin. Fokus utama adalah menghadirkan sebuah permainan klik sederhana di mana pengguna dapat "menjual" dessert dengan mengetuknya, meningkatkan pendapatan dan jumlah dessert yang terjual. Seiring dengan peningkatan penjualan, jenis dessert yang ditampilkan akan berubah menjadi yang lebih mahal.

Fitur-fitur utama yang diimplementasikan meliputi:

  • Mekanisme Klik Dessert: Pengguna mengetuk gambar dessert untuk "menjualnya", yang akan meningkatkan pendapatan dan jumlah total dessert yang terjual.
  • Perubahan Dessert Dinamis: Gambar dessert akan berubah secara otomatis menjadi jenis dessert yang lebih mahal setelah mencapai ambang batas penjualan tertentu.
  • Pelestarian Status Aplikasi: Menggunakan rememberSaveable untuk memastikan progres permainan (pendapatan dan jumlah dessert terjual) tetap tersimpan saat terjadi perubahan konfigurasi, seperti rotasi layar.
  • Top App Bar Informatif: Menampilkan judul aplikasi dan fungsionalitas berbagi.
  • Struktur Proyek yang Terorganisir: Memisahkan data, UI, dan tema untuk pengelolaan kode yang lebih baik.
Selain fungsionalitas dasar dari codelab, saya juga menambahkan beberapa modifikasi untuk meningkatkan pengalaman pengguna dan fungsionalitas aplikasi:
  • Tombol Reset Game: Sebuah tombol "Reset" telah ditambahkan pada Top App Bar yang memungkinkan pengguna untuk mengatur ulang seluruh progres permainan (pendapatan dan jumlah dessert terjual) kembali ke nilai awal.
  • Animasi Klik Gambar: Setiap kali gambar dessert diklik, gambar tersebut akan membesar sedikit dan kemudian kembali ke ukuran semula, memberikan umpan balik visual yang responsif dan interaktif kepada pengguna.
  • Visualisasi Progres: Sebuah progress bar ditempatkan di bagian bawah layar untuk menunjukkan seberapa dekat pengguna dengan pencapaian target penjualan untuk membuka jenis dessert berikutnya.

Berikut merupakan sedikit demo dari aplikasi "Dessert Clicker" yang telah saya buat:


Kode Program Modifikasi:

private const val TAG = "MainActivity"

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate Called")
setContent {
DessertClickerTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding(),
) {
DessertClickerApp(desserts = Datasource.dessertList)
}
}
}
}

override fun onStart() {
super.onStart()
Log.d(TAG, "onStart Called")
}

override fun onResume() {
super.onResume()
Log.d(TAG, "onResume Called")
}

override fun onRestart() {
super.onRestart()
Log.d(TAG, "onRestart Called")
}

override fun onPause() {
super.onPause()
Log.d(TAG, "onPause Called")
}

override fun onStop() {
super.onStop()
Log.d(TAG, "onStop Called")
}

override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "onDestroy Called")
}
}

/**
* Determine which dessert to show.
*/
fun determineDessertToShow(
desserts: List<Dessert>,
dessertsSold: Int
): Dessert {
var dessertToShow = desserts.first()
for (dessert in desserts) {
if (dessertsSold >= dessert.startProductionAmount) {
dessertToShow = dessert
} else {
// The list of desserts is sorted by startProductionAmount. As you sell more desserts,
// you'll start producing more expensive desserts as determined by startProductionAmount
// We know to break as soon as we see a dessert who's "startProductionAmount" is greater
// than the amount sold.
break
}
}

return dessertToShow
}

/**
* Share desserts sold information using ACTION_SEND intent
*/
private fun shareSoldDessertsInformation(intentContext: Context, dessertsSold: Int, revenue: Int) {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(
Intent.EXTRA_TEXT,
intentContext.getString(R.string.share_text, dessertsSold, revenue)
)
type = "text/plain"
}

val shareIntent = Intent.createChooser(sendIntent, null)

try {
ContextCompat.startActivity(intentContext, shareIntent, null)
} catch (e: ActivityNotFoundException) {
Toast.makeText(
intentContext,
intentContext.getString(R.string.sharing_not_available),
Toast.LENGTH_LONG
).show()
}
}

@Composable
private fun DessertClickerApp(
desserts: List<Dessert>
) {

var revenue by rememberSaveable { mutableStateOf(0) }
var dessertsSold by rememberSaveable { mutableStateOf(0) }

var currentDessertIndex by rememberSaveable { mutableStateOf(0) }

var currentDessertPrice by rememberSaveable {
mutableStateOf(desserts[currentDessertIndex].price)
}
var currentDessertImageId by rememberSaveable {
mutableStateOf(desserts[currentDessertIndex].imageId)
}

var progressToNextDessert by rememberSaveable { mutableStateOf(0f) }

Scaffold(
topBar = {
val intentContext = LocalContext.current
val layoutDirection = LocalLayoutDirection.current
DessertClickerAppBar(
onShareButtonClicked = {
shareSoldDessertsInformation(
intentContext = intentContext,
dessertsSold = dessertsSold,
revenue = revenue
)
},
onResetButtonClicked = {
revenue = 0
dessertsSold = 0
currentDessertIndex = 0
currentDessertPrice = desserts[currentDessertIndex].price
currentDessertImageId = desserts[currentDessertIndex].imageId
progressToNextDessert = 0f
Toast.makeText(intentContext, "Menu Reset!", Toast.LENGTH_SHORT).show()
},
modifier = Modifier
.fillMaxWidth()
.padding(
start = WindowInsets.safeDrawing.asPaddingValues()
.calculateStartPadding(layoutDirection),
end = WindowInsets.safeDrawing.asPaddingValues()
.calculateEndPadding(layoutDirection),
)
.background(MaterialTheme.colorScheme.primary)
)
}
) { contentPadding ->
DessertClickerScreen(
revenue = revenue,
dessertsSold = dessertsSold,
dessertImageId = currentDessertImageId,
progressToNextDessert = progressToNextDessert,
onDessertClicked = {

// Update the revenue
revenue += currentDessertPrice
dessertsSold++

// Show the next dessert
val dessertToShow = determineDessertToShow(desserts, dessertsSold)
currentDessertImageId = dessertToShow.imageId
currentDessertPrice = dessertToShow.price

currentDessertIndex = desserts.indexOf(dessertToShow)
val currentDessert = desserts[currentDessertIndex]
val nextDessertIndex = if (currentDessertIndex + 1 < desserts.size) {
currentDessertIndex + 1
} else {
-1
}

if (nextDessertIndex != -1) {
val nextDessert = desserts[nextDessertIndex]
val intervalStart = currentDessert.startProductionAmount
val intervalEnd = nextDessert.startProductionAmount
val soldInCurrentInterval = dessertsSold - intervalStart
val intervalSize = intervalEnd - intervalStart

progressToNextDessert = if (intervalSize > 0) {
min(1.0f, soldInCurrentInterval.toFloat() / intervalSize.toFloat())
} else {
1.0f
}
} else {
progressToNextDessert = 1.0f
}
},
modifier = Modifier.padding(contentPadding)
)
}
}

@Composable
private fun DessertClickerAppBar(
onShareButtonClicked: () -> Unit,
onResetButtonClicked: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(R.string.app_name),
modifier = Modifier.padding(start = dimensionResource(R.dimen.padding_medium)),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.titleLarge,
)
Row(
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(
onClick = onResetButtonClicked,
modifier = Modifier.padding(end = dimensionResource(R.dimen.padding_small)),
) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = stringResource(R.string.reset_game),
tint = MaterialTheme.colorScheme.onPrimary
)
}
IconButton(
onClick = onShareButtonClicked,
modifier = Modifier.padding(end = dimensionResource(R.dimen.padding_medium)),
) {
Icon(
imageVector = Icons.Filled.Share,
contentDescription = stringResource(R.string.share),
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
}
}

@Composable
fun DessertClickerScreen(
revenue: Int,
dessertsSold: Int,
@DrawableRes dessertImageId: Int,
progressToNextDessert: Float,
onDessertClicked: () -> Unit,
modifier: Modifier = Modifier
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

val scale by animateFloatAsState(
targetValue = if (isPressed) 1.05f else 1f,
animationSpec = tween(durationMillis = 100),
label = "DessertClickAnimation"
)
Box(modifier = modifier) {
Image(
painter = painterResource(R.drawable.bakery_back),
contentDescription = null,
contentScale = ContentScale.Crop
)
Column {
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
) {
Image(
painter = painterResource(dessertImageId),
contentDescription = null,
modifier = Modifier
.width(dimensionResource(R.dimen.image_size))
.height(dimensionResource(R.dimen.image_size))
.align(Alignment.Center)
.graphicsLayer {
scaleX = scale
scaleY = scale
}
.clickable(
interactionSource = interactionSource,
indication = null,
onClick = { onDessertClicked() }
),
contentScale = ContentScale.Crop,
)
}
LinearProgressIndicator(
progress = progressToNextDessert,
modifier = Modifier
.fillMaxWidth()
.height(dimensionResource(R.dimen.progress_bar_height))
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
color = MaterialTheme.colorScheme.tertiary,
trackColor = MaterialTheme.colorScheme.tertiaryContainer
)
TransactionInfo(
revenue = revenue,
dessertsSold = dessertsSold,
modifier = Modifier.background(MaterialTheme.colorScheme.secondaryContainer)
)
}
}
}

@Composable
private fun TransactionInfo(
revenue: Int,
dessertsSold: Int,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
DessertsSoldInfo(
dessertsSold = dessertsSold,
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_medium))
)
RevenueInfo(
revenue = revenue,
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_medium))
)
}
}

@Composable
private fun RevenueInfo(revenue: Int, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = stringResource(R.string.total_revenue),
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
Text(
text = "$${revenue}",
textAlign = TextAlign.Right,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}

@Composable
private fun DessertsSoldInfo(dessertsSold: Int, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = stringResource(R.string.dessert_sold),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
Text(
text = dessertsSold.toString(),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}

@Preview
@Composable
fun MyDessertClickerAppPreview() {
DessertClickerTheme {
DessertClickerApp(listOf(Dessert(R.drawable.cupcake, 5, 0)))
}
}

Link Repository Github: Tugas 9 - PPB

Demo Aplikasi:











Komentar

Postingan populer dari blog ini

Tugas 3 - Membangun Aplikasi Sederhana dengan Composable Teks

EAS - NewsApp

Tugas 11 - Membuat Aplikasi Authentication