Pertemuan 12 - Dessert Clicker

 

Naufal Khairul Rizky - 5025221127


Dessert Clicker - Pertemuan 12

Dessert Clicker adalah aplikasi Android sederhana berbasis Jetpack Compose yang mensimulasikan penjualan dessert (makanan penutup). Setiap kali pengguna mengklik gambar dessert, jumlah dessert terjual dan total pendapatan akan bertambah. Dessert yang ditampilkan akan berubah sesuai jumlah penjualan.


Struktur Kode Utama

1. MainActivity

  • Kelas utama MainActivity mewarisi ComponentActivity.

  • Lifecycle methods (onCreate, onStart, onResume, dll.) digunakan untuk log aktivitas saat aplikasi berjalan.

  • setContent memanggil DessertClickerApp() untuk memulai UI.

2. DessertClickerApp()

  • Komponen utama Composable.

  • Mengelola state revenue (total uang), dessertsSold (jumlah dessert terjual), dan dessert yang sedang ditampilkan (currentDessertImageId dan currentDessertPrice).

  • Menggunakan Scaffold untuk menyusun layout, termasuk AppBar dan konten utama.

3. DessertClickerAppBar()

  • AppBar dengan judul dan tombol Share.

  • Menampilkan nama aplikasi (app_name) dan ikon berbagi (Icons.Filled.Share).

  • Ketika tombol Share ditekan, memanggil fungsi shareSoldDessertsInformation().

4. DessertClickerScreen()

  • Menampilkan UI utama:

    • Gambar latar belakang.

    • Gambar dessert yang dapat diklik.

    • Informasi transaksi: jumlah dessert terjual dan total pendapatan.

5. TransactionInfo()

  • Menyusun dua baris informasi:

    • DessertsSoldInfo → Jumlah dessert yang terjual.

    • RevenueInfo → Total pendapatan ($).


Logika Inti Aplikasi

determineDessertToShow()

  • Fungsi untuk menentukan dessert mana yang harus ditampilkan berdasarkan jumlah penjualan (dessertsSold).

  • Akan mengembalikan dessert dengan startProductionAmount paling cocok.

shareSoldDessertsInformation()

  • Membuat Intent.ACTION_SEND untuk membagikan jumlah penjualan dan pendapatan.

  • Menampilkan pesan Toast jika tidak ada aplikasi yang mendukung sharing.


Model & Data

Dessert

  • Merupakan data class (berasal dari package model) dengan properti:

    • imageId: ID drawable untuk gambar dessert.

    • price: Harga dessert.

    • startProductionAmount: Batas minimal jumlah terjual untuk mulai menampilkan dessert ini.

Datasource.dessertList

  • Berisi list data dessert yang digunakan dalam aplikasi.

  • Digunakan sebagai parameter di DessertClickerApp().


Preview

  • Fungsi MyDessertClickerAppPreview() memungkinkan preview UI di Android Studio.

  • Menampilkan UI dengan satu dessert statis (cupcake) untuk keperluan desain.


Fitur Utama

  • Klik dessert → menambah penjualan dan pendapatan.

  • Dessert berubah berdasarkan jumlah penjualan.

  • UI yang responsif dengan Jetpack Compose.

  • Fitur share hasil penjualan.


Dependencies dan Resource

  • Menggunakan Material 3 (material3.*) untuk UI modern.

  • Gambar seperti bakery_back, cupcake, dan lainnya ada di folder res/drawable.

  • Resource string dan dimensi seperti R.string.app_name, R.dimen.padding_medium digunakan untuk fleksibilitas dan kemudahan i18n.



Repository: Github


Source Code:
MainActivity.kt
package com.uni.dessertclicker

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
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.safeDrawing
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.content.ContextCompat
import com.uni.dessertclicker.data.Datasource
import com.uni.dessertclicker.model.Dessert
import com.uni.dessertclicker.ui.theme.DessertClickerTheme

// Tag for logging
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 {
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) }

val currentDessertIndex by rememberSaveable { mutableStateOf(0) }

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

Scaffold(
topBar = {
val intentContext = LocalContext.current
val layoutDirection = LocalLayoutDirection.current
DessertClickerAppBar(
onShareButtonClicked = {
shareSoldDessertsInformation(
intentContext = intentContext,
dessertsSold = dessertsSold,
revenue = revenue
)
},
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,
onDessertClicked = {

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

// Show the next dessert
val dessertToShow = determineDessertToShow(desserts, dessertsSold)
currentDessertImageId = dessertToShow.imageId
currentDessertPrice = dessertToShow.price
},
modifier = Modifier.padding(contentPadding)
)
}
}

@Composable
private fun DessertClickerAppBar(
onShareButtonClicked: () -> 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,
)
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,
onDessertClicked: () -> Unit,
modifier: Modifier = Modifier
) {
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)
.clickable { onDessertClicked() },
contentScale = ContentScale.Crop,
)
}
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)))
}
}

Komentar