Tugas 9: Membuat Aplikasi Dessert Clicker menggunakan Activity Lifecycle dan Intent
Nama : Fathin Muhashibi Putra
NRP : 5025211229
Kelas : PPB - A
Tugas 9:
Membuat Aplikasi Dessert Clicker menggunakan Activity Lifecycle dan Intent
Pada tugas kali ini, saya membuat aplikasi Dessert Clicker untuk memahami dan menerapkan Activity Lifecycle dan Intent pada aplikasi Android. Aplikasi ini adalah sebuah game klik sederhana yang menampilkan berbagai dessert yang dapat "dibeli" dengan mengetuknya, serta memiliki fitur berbagi hasil permainan menggunakan Intent.
1. Konsep Activity Lifecycle dan Intent
Activity Lifecycle adalah sekumpulan status yang dilalui sebuah Activity, mulai dari saat pertama kali dibuat hingga dihancurkan oleh sistem. Saat pengguna bernavigasi antar Activity atau keluar masuk aplikasi, setiap Activity berpindah di antara status-status dalam siklus hidupnya.
Intent adalah objek yang berfungsi sebagai media komunikasi antar komponen aplikasi Android. Intent memungkinkan kita untuk melakukan perpindahan dari satu Activity ke Activity lain atau berkomunikasi dengan komponen sistem Android seperti kamera, galeri, atau aplikasi berbagi.
Dalam aplikasi Dessert Clicker, kita akan mengimplementasikan:
- Pemahaman dan penerapan Activity Lifecycle dengan menambahkan logging
- Penggunaan Intent untuk membagikan informasi penjualan dessert (Intent Explicit)
Beberapa metode callback utama dalam Activity Lifecycle:
- onCreate() - Dipanggil saat Activity dibuat pertama kali
- onStart() - Dipanggil saat Activity mulai terlihat di layar
- onResume() - Dipanggil saat Activity siap berinteraksi dengan pengguna
- onPause() - Dipanggil saat Activity kehilangan fokus
- onStop() - Dipanggil saat Activity tidak lagi terlihat
- onRestart() - Dipanggil saat Activity akan dimulai ulang setelah dihentikan
- onDestroy() - Dipanggil saat Activity dihancurkan
2. Membuat Project Dessert Clicker
Pertama, saya membuat proyek baru di Android Studio dengan memilih Empty Compose Activity dan memberi nama "DessertClicker". Berikut adalah struktur file utama yang diperlukan:
3. Implementasi File Utama
3.1. Model Data (Dessert.kt)
Pertama, buat file Dessert.kt di dalam package com.example.dessertclicker.model:
package com.example.dessertclicker.model
data class Dessert(val imageId: Int, val price: Int, val startProductionAmount: Int)
3.2. Data Source (Datasource.kt)
Selanjutnya, buat file Datasource.kt di dalam package com.example.dessertclicker.data:
package com.example.dessertclicker.data
import com.example.dessertclicker.R
import com.example.dessertclicker.model.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)
)
}
3.3. Theme Files
Untuk tema aplikasi, buat dua file di package com.example.dessertclicker.ui.theme:
Color.kt:
package com.example.dessertclicker.ui.theme
import androidx.compose.ui.graphics.Color
val md_theme_light_primary = Color(0xFF006781)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFCFE6F1)
val md_theme_light_onSecondaryContainer = Color(0xFF071E26)
val md_theme_light_background = Color(0xFFFBFCFE)
val md_theme_dark_primary = Color(0xFF5FD4FD)
val md_theme_dark_onPrimary = Color(0xFF003544)
val md_theme_dark_secondaryContainer = Color(0xFF354A53)
val md_theme_dark_onSecondaryContainer = Color(0xFFCFE6F1)
val md_theme_dark_background = Color(0xFF191C1D)
Theme.kt:
package com.example.dessertclicker.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
background = md_theme_dark_background,
)
private val LightColorScheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
background = md_theme_light_background,
)
@Composable
fun DessertClickerTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
window.navigationBarColor = colorScheme.secondaryContainer.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
content = content
)
}
3.4. Resource Files
Buat resource files berikut di direktori res:
res/values/strings.xml:
<resources>
<string name="app_name">Dessert Clicker</string>
<string name="dessert_sold">Desserts sold</string>
<string name="total_revenue">Total Revenue</string>
<string name="share">Share</string>
<string name="share_text">I\'ve clicked %1$d Desserts for a total of $%2$d #AndroidDessertClicker</string>
<string name="sharing_not_available">Sharing Not Available</string>
</resources>
res/values/dimens.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="padding_medium">16dp</dimen>
<dimen name="image_size">150dp</dimen>
</resources>
Gambar: Siapkan gambar dessert berikut di folder res/drawable:
- bakery_back.xml
- cupcake.xml
- donut.xml
- eclair.xml
- froyo.xml
- gingerbread.xml
- honeycomb.xml
- icecreamsandwich.xml
- jellybean.xml
- kitkat.xml
- lollipop.xml
- marshmallow.xml
- nougat.xml
- oreo.xml
3.5. MainActivity
File MainActivity.kt adalah file utama aplikasi. Berikut adalah implementasi lengkapnya:
package com.example.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.example.dessertclicker.data.Datasource
import com.example.dessertclicker.model.Dessert
import com.example.dessertclicker.ui.theme.DessertClickerTheme
private const val TAG = "MainActivity"
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate Called")
setContent {
DessertClickerTheme {
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")
}
}
fun determineDessertToShow(
desserts: List,
dessertsSold: Int
): Dessert {
var dessertToShow = desserts.first()
for (dessert in desserts) {
if (dessertsSold >= dessert.startProductionAmount) {
dessertToShow = dessert
} else {
break
}
}
return dessertToShow
}
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
) {
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 = {
revenue += currentDessertPrice
dessertsSold++
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)))
}
}
3. Explorasi Activity Lifecycle
Setelah aplikasi berhasil dijalankan, kita dapat mengeksplorasi Activity Lifecycle menggunakan Logcat:
3.1. Mencari Catatan Log
Di Android Studio, klik tab Logcat dan ketik tag:MainActivity di kolom pencarian untuk memfilter log.
3.2. Skenario yang Diamati
- Saat membuka aplikasi: onCreate() → onStart() → onResume()
- Saat menutup aplikasi (Back): onPause() → onStop() → onDestroy()
- Saat ke Home Screen: onPause() → onStop()
- Saat kembali dari Recent Apps: onRestart() → onStart() → onResume()
- Saat dialog berbagi muncul: onPause()
- Saat dialog ditutup: onResume()
- Saat rotasi layar: onPause() → onStop() → onDestroy() → onCreate() → onStart() → onResume()
4. Mengatasi Masalah Data Hilang
Untuk mengatasi masalah data hilang saat rotasi layar, kita menggunakan rememberSaveable pada semua state:
var revenue by rememberSaveable { mutableStateOf(0) }
var dessertsSold by rememberSaveable { mutableStateOf(0) }
rememberSaveable menyimpan state bahkan saat Activity dihancurkan dan dibuat ulang, tidak seperti remember yang hanya menyimpan selama recomposition.
5. Implementasi Intent untuk Berbagi
Fitur berbagi menggunakan Intent untuk berkomunikasi dengan aplikasi lain:
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()
}
}
6. Fitur Utama Aplikasi
- Interaksi dengan Gambar Dessert: Mengetuk dessert untuk "membelinya"
- Penghitungan Penjualan: Melacak jumlah dessert yang terjual dan total pendapatan
- Progresi Dessert: Dessert berubah seiring peningkatan jumlah penjualan
- Fitur Berbagi dengan Intent: Tombol Share untuk membagikan prestasi penjualan
- Implementasi Activity Lifecycle: Logging dan pemahaman siklus hidup Activity
- Manajemen State: Data disimpan bahkan saat rotasi layar dengan rememberSaveable
7. Hasil Akhir
- Screenshot Hasil:
- Kode Lengkap (Github Code):
- Video Demo Aplikasi Dessert Clicker:

Comments
Post a Comment