Membuat Game Sederhana "Dessert Clicker" dengan Jetpack Compose
Nama : Iftala Zahri Sukmana
NRP : 5025221002
Kelas : G
Link GitHub : https://github.com/ifzahri/ppb/blob/main/dessert-clicker
Kita akan membedah file MainActivity.kt untuk memahami bagaimana setiap bagian berkontribusi pada game yang adiktif ini.
Struktur Utama: MainActivity dan Inisialisasi
Seperti biasa, MainActivity adalah titik masuk aplikasi kita. Di dalam metode onCreate, kita mengaktifkan tampilan edge-to-edge dengan enableEdgeToEdge() untuk pengalaman visual yang lebih imersif. Kemudian, kita mengatur konten utama aplikasi menggunakan setContent.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() // 1. Mengaktifkan tampilan edge-to-edge
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate Called") // Logging untuk siklus hidup Activity
setContent {
DessertClickerTheme { // 2. Menerapkan tema kustom aplikasi
Surface( // 3. Container dasar untuk UI
modifier = Modifier
.fillMaxSize() // Mengisi seluruh layar
.statusBarsPadding(), // Memberikan padding untuk status bar
) {
// 4. Memanggil composable utama aplikasi dengan data makanan penutup
DessertClickerApp(desserts = Datasource.dessertList)
}
}
}
}
// ... metode siklus hidup lainnya dengan logging (onStart, onResume, dll.)
}
Aplikasi ini dibungkus dengan DessertClickerTheme untuk gaya visual yang konsisten. Surface bertindak sebagai kanvas utama, dan di dalamnya, DessertClickerApp dipanggil, yang merupakan jantung dari UI game kita. Data makanan penutup dimuat dari Datasource.dessertList. Kode ini juga menyertakan logging untuk berbagai tahap siklus hidup Activity (onStart, onResume, dll.), yang sangat berguna untuk debugging.
Logika Inti dan Pengelolaan State: DessertClickerApp
DessertClickerApp adalah composable utama yang mengatur state dan logika game.
@Composable
private fun DessertClickerApp(
desserts: List<Dessert>
) {
// Mengelola state game menggunakan rememberSaveable agar state tetap terjaga
// bahkan setelah perubahan konfigurasi (seperti rotasi layar)
var revenue by rememberSaveable { mutableStateOf(0) } // Pendapatan saat ini
var dessertsSold by rememberSaveable { mutableStateOf(0) } // Jumlah makanan penutup terjual
// State untuk makanan penutup yang sedang ditampilkan
val currentDessertIndex by rememberSaveable { mutableStateOf(0) } // Indeks makanan penutup saat ini (tidak langsung digunakan untuk mengubah dessert)
var currentDessertPrice by rememberSaveable {
mutableStateOf(desserts[currentDessertIndex].price) // Harga makanan penutup saat ini
}
var currentDessertImageId by rememberSaveable {
mutableStateOf(desserts[currentDessertIndex].imageId) // ID gambar makanan penutup saat ini
}
Scaffold( // Menyediakan struktur dasar Material Design
topBar = { // AppBar bagian atas
val intentContext = LocalContext.current // Konteks untuk Intent
// ... (modifier untuk AppBar)
DessertClickerAppBar(
onShareButtonClicked = { // Aksi ketika tombol bagikan diklik
shareSoldDessertsInformation(
intentContext = intentContext,
dessertsSold = dessertsSold,
revenue = revenue
)
},
// ... (modifier)
)
}
) { contentPadding -> // Padding konten dari Scaffold
DessertClickerScreen( // Composable untuk layar utama game
revenue = revenue,
dessertsSold = dessertsSold,
dessertImageId = currentDessertImageId,
onDessertClicked = { // Aksi ketika makanan penutup diklik
// Perbarui pendapatan dan jumlah terjual
revenue += currentDessertPrice
dessertsSold++
// Tentukan makanan penutup berikutnya untuk ditampilkan
val dessertToShow = determineDessertToShow(desserts, dessertsSold)
currentDessertImageId = dessertToShow.imageId
currentDessertPrice = dessertToShow.price
},
modifier = Modifier.padding(contentPadding) // Terapkan padding dari Scaffold
)
}
}
Di sini, kita melihat penggunaan rememberSaveable dan mutableStateOf untuk mengelola state seperti revenue (pendapatan), dessertsSold (jumlah makanan penutup terjual), currentDessertPrice (harga makanan penutup saat ini), dan currentDessertImageId (ID gambar makanan penutup saat ini). rememberSaveable memastikan state ini tetap ada bahkan jika terjadi perubahan konfigurasi, seperti rotasi layar.
Aplikasi menggunakan Scaffold untuk struktur Material Design dasar, yang mencakup topBar. topBar ini diimplementasikan oleh DessertClickerAppBar. Konten utama adalah DessertClickerScreen, tempat interaksi game terjadi.
Ketika makanan penutup diklik (onDessertClicked):
revenuebertambah sebesarcurrentDessertPrice.dessertsSoldbertambah satu.- Fungsi
determineDessertToShowdipanggil untuk menentukan makanan penutup berikutnya berdasarkan jumlahdessertsSold. currentDessertImageIddancurrentDessertPricediperbarui sesuai dengan makanan penutup baru.
AppBar Aplikasi: DessertClickerAppBar
AppBar menampilkan judul aplikasi dan tombol untuk berbagi progres.
@Composable
private fun DessertClickerAppBar(
onShareButtonClicked: () -> Unit, // Lambda untuk aksi klik tombol bagikan
modifier: Modifier = Modifier
) {
Row( // Mengatur elemen secara horizontal
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween, // Menyebarkan elemen (judul di kiri, tombol di kanan)
verticalAlignment = Alignment.CenterVertically, // Menyelaraskan elemen secara vertikal di tengah
) {
Text( // Judul aplikasi
text = stringResource(R.string.app_name),
modifier = Modifier.padding(start = dimensionResource(R.dimen.padding_medium)),
color = MaterialTheme.colorScheme.onPrimary, // Warna teks dari tema
style = MaterialTheme.typography.titleLarge, // Gaya teks dari tema
)
IconButton( // Tombol ikon untuk berbagi
onClick = onShareButtonClicked,
modifier = Modifier.padding(end = dimensionResource(R.dimen.padding_medium)),
) {
Icon(
imageVector = Icons.Filled.Share, // Ikon bagikan bawaan Material
contentDescription = stringResource(R.string.share), // Deskripsi untuk aksesibilitas
tint = MaterialTheme.colorScheme.onPrimary // Warna ikon dari tema
)
}
}
}
DessertClickerAppBar menggunakan Row untuk menempatkan Text judul aplikasi di sebelah kiri dan IconButton untuk berbagi di sebelah kanan. Ketika IconButton diklik, lambda onShareButtonClicked dieksekusi, yang memanggil fungsi shareSoldDessertsInformation.
Berbagi Progres: shareSoldDessertsInformation
Fungsi ini bertanggung jawab untuk membuat dan meluncurkan intent untuk berbagi informasi penjualan makanan penutup.
private fun shareSoldDessertsInformation(intentContext: Context, dessertsSold: Int, revenue: Int) {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND // Mengatur aksi intent menjadi SEND
putExtra( // Menambahkan data teks ke intent
Intent.EXTRA_TEXT,
intentContext.getString(R.string.share_text, dessertsSold, revenue) // Format teks berbagi
)
type = "text/plain" // Jenis konten yang akan dibagikan
}
val shareIntent = Intent.createChooser(sendIntent, null) // Membuat chooser untuk pengguna memilih aplikasi
try {
ContextCompat.startActivity(intentContext, shareIntent, null) // Mencoba memulai activity
} catch (e: ActivityNotFoundException) { // Menangani jika tidak ada aplikasi yang bisa menangani intent
Toast.makeText(
intentContext,
intentContext.getString(R.string.sharing_not_available),
Toast.LENGTH_LONG
).show()
}
}
Fungsi ini membuat Intent dengan aksi ACTION_SEND dan tipe text/plain. Teks yang dibagikan mencakup jumlah makanan penutup yang terjual dan total pendapatan. Intent.createChooser memungkinkan pengguna memilih aplikasi mana yang ingin mereka gunakan untuk berbagi. Ada juga penanganan ActivityNotFoundException jika tidak ada aplikasi yang dapat menangani intent berbagi tersebut.
Layar Utama Game: DessertClickerScreen
Di sinilah aksi utama game berlangsung.
@Composable
fun DessertClickerScreen(
revenue: Int,
dessertsSold: Int,
@DrawableRes dessertImageId: Int, // ID resource gambar makanan penutup
onDessertClicked: () -> Unit, // Lambda untuk aksi klik makanan penutup
modifier: Modifier = Modifier
) {
Box(modifier = modifier) { // Box untuk menumpuk elemen (gambar latar dan konten)
Image( // Gambar latar belakang toko roti
painter = painterResource(R.drawable.bakery_back),
contentDescription = null, // Dekoratif
contentScale = ContentScale.Crop // Memotong gambar agar sesuai
)
Column { // Mengatur elemen secara vertikal
Box( // Box untuk gambar makanan penutup agar bisa di tengah dan menggunakan sisa ruang
modifier = Modifier
.weight(1f) // Mengambil sisa ruang vertikal
.fillMaxWidth(),
) {
Image( // Gambar makanan penutup yang bisa diklik
painter = painterResource(dessertImageId),
contentDescription = null, // Dekoratif
modifier = Modifier
.width(dimensionResource(R.dimen.image_size))
.height(dimensionResource(R.dimen.image_size))
.align(Alignment.Center) // Menempatkan di tengah Box
.clickable { onDessertClicked() }, // Membuat gambar bisa diklik
contentScale = ContentScale.Crop,
)
}
TransactionInfo( // Composable untuk menampilkan info transaksi
revenue = revenue,
dessertsSold = dessertsSold,
modifier = Modifier.background(MaterialTheme.colorScheme.secondaryContainer)
)
}
}
}
DessertClickerScreen menggunakan Box untuk menempatkan gambar latar belakang (R.drawable.bakery_back) di belakang konten game. Di atasnya, sebuah Column mengatur Image makanan penutup yang dapat diklik dan TransactionInfo. Image makanan penutup menggunakan Modifier.clickable untuk memicu lambda onDessertClicked saat diketuk.
Menampilkan Informasi Transaksi: TransactionInfo, RevenueInfo, dan DessertsSoldInfo
Composable ini bertanggung jawab untuk menampilkan pendapatan dan jumlah makanan penutup yang terjual.
@Composable
private fun TransactionInfo(
revenue: Int,
dessertsSold: Int,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) { // Mengatur info terjual dan pendapatan secara vertikal
DessertsSoldInfo( // Info jumlah makanan penutup terjual
dessertsSold = dessertsSold,
// ... (modifier)
)
RevenueInfo( // Info total pendapatan
revenue = revenue,
// ... (modifier)
)
}
}
@Composable
private fun RevenueInfo(revenue: Int, modifier: Modifier = Modifier) {
Row( // Mengatur label dan nilai pendapatan secara horizontal
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween // Menyebarkan elemen
) {
Text(text = stringResource(R.string.total_revenue), /* ... gaya ... */)
Text(text = "$${revenue}", /* ... gaya ... */) // Menampilkan nilai pendapatan
}
}
@Composable
private fun DessertsSoldInfo(dessertsSold: Int, modifier: Modifier = Modifier) {
Row( // Mengatur label dan nilai terjual secara horizontal
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween // Menyebarkan elemen
) {
Text(text = stringResource(R.string.dessert_sold), /* ... gaya ... */)
Text(text = dessertsSold.toString(), /* ... gaya ... */) // Menampilkan jumlah terjual
}
}
TransactionInfo hanyalah sebuah Column yang menampung DessertsSoldInfo dan RevenueInfo. Keduanya (RevenueInfo dan DessertsSoldInfo) menggunakan Row dengan Arrangement.SpaceBetween untuk menampilkan label di kiri dan nilai di kanan.
Logika Pergantian Makanan Penutup: determineDessertToShow
Fungsi utilitas ini menentukan makanan penutup mana yang harus ditampilkan berdasarkan jumlah yang telah terjual.
fun determineDessertToShow(
desserts: List<Dessert>,
dessertsSold: Int
): Dessert {
var dessertToShow = desserts.first() // Default ke makanan penutup pertama
for (dessert in desserts) {
if (dessertsSold >= dessert.startProductionAmount) { // Jika jumlah terjual memenuhi syarat
dessertToShow = dessert // Ganti ke makanan penutup ini
} else {
// Karena daftar diurutkan berdasarkan startProductionAmount,
// kita bisa berhenti jika syarat tidak terpenuhi lagi.
break
}
}
return dessertToShow
}
Fungsi ini mengiterasi daftar desserts (yang diasumsikan sudah diurutkan berdasarkan startProductionAmount). Ia memilih makanan penutup terakhir dalam daftar yang startProductionAmount-nya kurang dari atau sama dengan dessertsSold.
Pratinjau di Android Studio
Kode ini juga menyertakan fungsi pratinjau Composable, MyDessertClickerAppPreview, yang sangat berguna untuk melihat tampilan UI langsung di Android Studio tanpa harus menjalankan aplikasi di emulator atau perangkat.
@Preview // Anotasi untuk pratinjau
@Composable
fun MyDessertClickerAppPreview() {
DessertClickerTheme {
// Menyediakan daftar Dessert dummy untuk pratinjau
DessertClickerApp(listOf(Dessert(R.drawable.cupcake, 5, 0)))
}
}
Kesimpulan
Aplikasi "Dessert Clicker" adalah contoh yang bagus tentang bagaimana Jetpack Compose dapat digunakan untuk membangun game sederhana namun interaktif dengan cepat. Dengan manajemen state yang reaktif, sistem layout yang fleksibel, dan kemampuan pratinjau yang kuat, Compose memberdayakan pengembang untuk membuat UI yang menarik dengan lebih sedikit kode boilerplate. Dari menangani input pengguna hingga memperbarui UI secara dinamis dan bahkan menyertakan fungsionalitas berbagi, semua aspek game ini dirangkai dengan elegan menggunakan paradigma deklaratif Compose.


Komentar
Posting Komentar