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.

Kotlin
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.

Kotlin
@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):

  1. revenue bertambah sebesar currentDessertPrice.
  2. dessertsSold bertambah satu.
  3. Fungsi determineDessertToShow dipanggil untuk menentukan makanan penutup berikutnya berdasarkan jumlah dessertsSold.
  4. currentDessertImageId dan currentDessertPrice diperbarui sesuai dengan makanan penutup baru.

AppBar Aplikasi: DessertClickerAppBar

AppBar menampilkan judul aplikasi dan tombol untuk berbagi progres.

Kotlin
@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.

Kotlin
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.

Kotlin
@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.

Kotlin
@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.

Kotlin
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.

Kotlin
@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

Postingan populer dari blog ini

Memahami Jetpack Compose dengan konsep Composable & Recomposition

Membuat Login Form Elegan dengan Jetpack Compose di Android Studio

Aplikasi Starbucks Clone dengan Android Studio