TUGAS 9 PPB - KEYSA ANADEA 5025211028
TUGAS 9 PPB
Deskripsi Proyek: Dessert Clicker App
Nama: Keysa Anadea Aqiva Ajie
NRP: 5025211028
Kelas: PPB A
Pada pertemuan ke-12 mata kuliah Pemrograman Perangkat Bergerak, mahasiswa ditugaskan membuat aplikasi interaktif bertema makanan penutup (dessert). Proyek ini dirancang untuk melatih penggunaan state, pengelolaan UI berbasis gambar, serta pemanfaatan intent Android.
Tujuan Proyek
-
Melatih penggunaan Jetpack Compose untuk membangun UI dinamis.
-
Memahami konsep stateful UI – bagaimana tampilan berubah seiring interaksi pengguna.
-
Menerapkan Intent Android untuk fitur berbagi data (share score).
-
Belajar modularisasi data melalui model dan data source.
Struktur Proyek
-
MainActivity.kt
— Menyusun tampilan utama dengan gambar dessert, skor, dan aksi klik. -
Datasource.kt
— Berisi daftar dessert dalam bentuk objek statis. -
Dessert.kt
— Mendefinisikan data classDessert(val name: String, @DrawableRes val imageResId: Int)
. -
Folder
res/drawable
— Menyimpan gambar-gambar dessert.
Fitur Aplikasi
-
Klik Gambar: Setiap klik pada gambar dessert akan menaikkan skor.
-
Ganti Dessert: Setelah beberapa klik, gambar dessert berubah (berdasarkan list di
Datasource.kt
). -
Bagikan Skor: Tersedia tombol untuk membagikan skor pengguna ke aplikasi lain (WA, email, dll).
Teknologi Digunakan
-
Bahasa: Kotlin
-
UI: Jetpack Compose
-
Desain: Material3
-
Navigasi: Single-screen app
-
Sharing: Implicit Intent Android
Pembelajaran
Proyek ini mengajarkan:
-
Penggunaan
@Composable
,remember
, danmutableStateOf
untuk mengelola state. -
Implementasi
clickable()
pada gambar untuk memicu interaksi. -
Modularisasi kode dengan pemisahan data (
Datasource.kt
) dan model (Dessert.kt
). -
Pemanfaatan intent
Intent.ACTION_SEND
untuk berbagi konten lintas aplikasi.
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 {
// 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) }
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)))
}
}
DataSource.kt
package com.uni.dessertclicker.data
import com.uni.dessertclicker.R
import com.uni.dessertclicker.model.Dessert
/**
* [Datasource] generates a list of [Dessert]
*/
object Datasource {
val dessertList = listOf(
Dessert(R.drawable.cupcake, 5, 0),
Dessert(R.drawable.donut, 10, 5),
Dessert(R.drawable.eclair, 15, 20),
Dessert(R.drawable.froyo, 30, 50),
Dessert(R.drawable.gingerbread, 50, 100),
Dessert(R.drawable.honeycomb, 100, 200),
Dessert(R.drawable.icecreamsandwich, 500, 500),
Dessert(R.drawable.jellybean, 1000, 1000),
Dessert(R.drawable.kitkat, 2000, 2000),
Dessert(R.drawable.lollipop, 3000, 4000),
Dessert(R.drawable.marshmallow, 4000, 8000),
Dessert(R.drawable.nougat, 5000, 16000),
Dessert(R.drawable.oreo, 6000, 20000)
)
}
package com.uni.dessertclicker.data
import com.uni.dessertclicker.R
import com.uni.dessertclicker.model.Dessert
/**
* [Datasource] generates a list of [Dessert]
*/
object Datasource {
val dessertList = listOf(
Dessert(R.drawable.cupcake, 5, 0),
Dessert(R.drawable.donut, 10, 5),
Dessert(R.drawable.eclair, 15, 20),
Dessert(R.drawable.froyo, 30, 50),
Dessert(R.drawable.gingerbread, 50, 100),
Dessert(R.drawable.honeycomb, 100, 200),
Dessert(R.drawable.icecreamsandwich, 500, 500),
Dessert(R.drawable.jellybean, 1000, 1000),
Dessert(R.drawable.kitkat, 2000, 2000),
Dessert(R.drawable.lollipop, 3000, 4000),
Dessert(R.drawable.marshmallow, 4000, 8000),
Dessert(R.drawable.nougat, 5000, 16000),
Dessert(R.drawable.oreo, 6000, 20000)
)
}
Komentar
Posting Komentar