Unggulan

Tugas 5 - Kalkulator Sederhana

Nama : Christoforus Indra Bagus Pratama

NRP : 5025231124

Mata Kuliah : Pemrograman Perangkat Bergerak (C)

Tanggal : 1 April 2026

Pertemuan : 6

Link :Github

Kalkultator Sederhana

1. Deklarasi Package dan Import

Bagian ini adalah fondasi dari setiap file Kotlin di Android. package com.example.kalkulatorsederhana mendefinisikan ruang nama (namespace) unik untuk aplikasimu, mencegah bentrok nama dengan aplikasi lain. Bagian import bertugas memuat pustaka (library) eksternal yang dibutuhkan. Dalam Jetpack Compose, kita banyak mengimpor dari androidx.compose.*. Misalnya, androidx.compose.foundation.layout.* memberikan kita alat untuk mengatur tata letak seperti Column dan Row. androidx.compose.material3.* memberikan kita akses ke komponen antarmuka siap pakai (seperti Button, Text, Card) yang sudah mematuhi panduan desain Material 3 terbaru dari Google. ComponentActivity diimpor karena ini adalah kelas dasar (base class) khusus yang dioptimalkan untuk menjalankan antarmuka berbasis Compose, berbeda dengan AppCompatActivity pada sistem Android lawas.

package com.example.kalkulatorsederhana

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.kalkulatorsederhana.ui.theme.KalkulatorSederhanaTheme

2. Kelas MainActivity (Entry Point)

MainActivity mewarisi ComponentActivity, menjadikannya layar utama yang pertama kali dimuat saat aplikasi dibuka. Fungsi onCreate adalah siklus hidup (lifecycle) pertama yang dipanggil oleh sistem Android. Di dalamnya, enableEdgeToEdge() digunakan agar tampilan aplikasimu bisa meluas hingga ke area status bar (jam/baterai) dan navigation bar (tombol back/home), memberikan kesan modern yang imersif. Blok setContent { ... } adalah jembatan penghubung antara dunia Android tradisional dengan dunia Jetpack Compose. Di dalam blok inilah kita mulai "menggambar" UI. Fungsi Scaffold adalah kerangka dasar Material Design. Ia secara otomatis menyediakan area standar untuk aplikasi dan memberikan innerPadding. Padding ini sangat krusial; ia harus diterapkan ke komponen di dalamnya (menggunakan Modifier.padding(innerPadding)) agar konten kalkulatormu tidak tertutup oleh status bar ponsel atau layar poni (notch).

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            KalkulatorSederhanaTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    CalculatorApp(modifier = Modifier.padding(innerPadding))
                }
            }
        }
    }
}

3. Fungsi CalculatorApp - Deklarasi State (Status UI)

Jetpack Compose menggunakan paradigma antarmuka deklaratif. Artinya, UI akan diperbarui (dirender ulang/rekomposisi) secara otomatis setiap kali data yang mengikatnya berubah. Data inilah yang disebut State. Variabel input1, input2, result, dan errorMessage dideklarasikan menggunakan blok remember { mutableStateOf("") }. Kata kunci mutableStateOf memberitahu Compose: "Pantau variabel ini. Jika nilainya berubah, gambar ulang bagian layar yang memakai variabel ini". Kata kunci remember memberitahu Compose: "Tolong ingat nilai variabel ini saat kamu menggambar ulang layarnya, jangan di-reset ke string kosong setiap kali siklus render terjadi". Penggunaan by (delegasi properti di Kotlin) memungkinkan kita membaca dan mengubah variabel tersebut langsung sebagai String, tanpa harus memanggil input1.value berulang kali. Ini membuat kode jauh lebih bersih.

@Composable
fun CalculatorApp(modifier: Modifier = Modifier) {
    // State untuk input dan hasil
    var input1 by remember { mutableStateOf("") }
    var input2 by remember { mutableStateOf("") }
    var result by remember { mutableStateOf("") }
    var errorMessage by remember { mutableStateOf("") }

4. Fungsi Logika Kalkulasi (calculate)

Ini adalah fungsi otak matematika dari aplikasimu. Saat dipanggil, ia pertama-tama membersihkan sisa hasil atau pesan error sebelumnya. Penggunaan toDoubleOrNull() sangat cerdas; fungsi bawaan Kotlin ini mencoba mengubah teks menjadi angka desimal ganda (Double). Jika teks tidak valid (misal kosong atau hanya berisi titik "."), ia tidak akan membuat aplikasi crash (force close), melainkan mengembalikan nilai null. Jika kedua angka valid (!= null), program masuk ke blok when untuk mengeksekusi operasi sesuai tombol yang ditekan. Logika if (res % 1.0 == 0.0) digunakan untuk mengecek apakah hasilnya bilangan bulat (misal 5.0). Jika ya, kita ubah menjadi tipe Int agar tampil sebagai "5" tanpa desimal. Jika hasilnya desimal (misal 10/3), kita gunakan format String "%.6f" untuk membatasinya maksimal 6 angka di belakang koma. Fungsi berantai .trimEnd('0').trimEnd('.') bertugas merapikan hasil, menghapus angka nol yang tidak berguna di akhir deret desimal (misalnya mengubah "2.500000" menjadi "2.5"). Terakhir, terdapat perlindungan logika if (num2 != 0.0) khusus untuk operasi pembagian guna mencegah error tak terdefinisi secara matematis.

    // Fungsi untuk menghitung hasil dengan pembulatan
    fun calculate(operator: String) {
        errorMessage = ""
        result = ""
        val num1 = input1.toDoubleOrNull()
        val num2 = input2.toDoubleOrNull()

        if (num1 != null && num2 != null) {
            when (operator) {
                "+" -> {
                    val res = num1 + num2
                    result = if (res % 1.0 == 0.0) res.toInt().toString() else "%.6f".format(res).trimEnd('0').trimEnd('.')
                }
                "-" -> {
                    val res = num1 - num2
                    result = if (res % 1.0 == 0.0) res.toInt().toString() else "%.6f".format(res).trimEnd('0').trimEnd('.')
                }
                "*" -> {
                    val res = num1 * num2
                    result = if (res % 1.0 == 0.0) res.toInt().toString() else "%.6f".format(res).trimEnd('0').trimEnd('.')
                }
                "/" -> {
                    if (num2 != 0.0) {
                        val res = num1 / num2
                        result = if (res % 1.0 == 0.0) res.toInt().toString() else "%.6f".format(res).trimEnd('0').trimEnd('.')
                    } else {
                        errorMessage = "Tidak bisa dibagi dengan nol!"
                    }
                }
            }
        } else {
            errorMessage = "Masukkan kedua angka dengan benar!"
        }
    }

5. Tata Letak Utama (Column) dan Judul

Column adalah tata letak dasar di Compose yang menyusun semua komponen anak (children) di dalamnya menjadi satu kolom vertikal. Properti modifier digunakan untuk memanipulasi tampilan Column ini. .fillMaxSize() memerintahkannya untuk mengambil seluruh lebar dan tinggi layar yang tersedia. .padding(24.dp) memberikan jarak aman sebesar 24 density-independent pixels dari tepi layar agar konten tidak terlalu mepet dengan bezel ponsel. horizontalAlignment = Alignment.CenterHorizontally memastikan semua elemen di dalam Column akan ditempatkan persis di tengah-tengah secara horizontal. Di dalam Column ini, komponen pertama adalah Text statis yang berfungsi sebagai judul aplikasi. Kita memberikan ukuran font besar (24.sp) dan menebalkannya (FontWeight.Bold) dengan memberikan sedikit jarak ke bawah (padding(bottom = 32.dp)) sebelum masuk ke kotak hasil.

    Column(
        modifier = modifier
            .fillMaxSize()
            .padding(24.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Top
    ) {
        // Area Judul
        Text(
            text = "Kalkulator Sederhana",
            fontSize = 24.sp,
            fontWeight = FontWeight.Bold,
            modifier = Modifier.padding(bottom = 32.dp)
        )

6. Area Tampilan Hasil (Card)

Untuk membuat area hasil terlihat terpisah dan menonjol, kita menggunakan komponen Card. Card memberikan latar belakang berbayang secara otomatis. Properti elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) memberikan efek bayangan seolah-olah kotak ini melayang di atas latar belakang utama. Di dalam Card, kita menggunakan Column lagi agar tulisan "Hasil:" berada di atas nilai angkanya. Kita atur horizontalAlignment = Alignment.End agar teks merapat ke sisi kanan kalkulator, mirip dengan kalkulator asli. Blok if (errorMessage.isNotEmpty()) adalah kontrol alur UI: jika ada error (seperti pembagian nol), maka layar akan menampilkan teks berwarna merah (colorScheme.error). Namun, jika berjalan lancar, ia akan masuk ke blok else dan menampilkan hasil. Terdapat properti maxLines = 1 yang berfungsi secara ketat memaksa teks tetap dalam satu baris, memastikan agar teks hasil yang panjang tidak meluber dan menumpuk ke baris bawahnya seperti yang terjadi sebelumnya.

        // Area Hasil yang Diperbaiki
        Card(
            modifier = Modifier
                .fillMaxWidth()
                .padding(bottom = 32.dp),
            colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
            elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
        ) {
            Column(
                modifier = Modifier
                    .padding(24.dp)
                    .fillMaxWidth(),
                horizontalAlignment = Alignment.End
            ) {
                Text(
                    text = "Hasil:",
                    color = MaterialTheme.colorScheme.onSurfaceVariant,
                    fontSize = 14.sp
                )
                if (errorMessage.isNotEmpty()) {
                    Text(
                        text = errorMessage,
                        fontSize = 18.sp,
                        color = MaterialTheme.colorScheme.error,
                        modifier = Modifier.padding(top = 8.dp),
                        textAlign = TextAlign.End
                    )
                } else {
                    Text(
                        text = result,
                        fontSize = 48.sp,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier
                            .padding(top = 8.dp)
                            .fillMaxWidth(),
                        textAlign = TextAlign.End,
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                        maxLines = 1,
                        overflow = TextOverflow.Visible
                    )
                }
            }
        }

7. Formulir Input Pengguna

Komponen OutlinedTextField memberikan kotak input teks dengan garis batas luar yang sesuai dengan Material 3. Properti value = input1 mengikat isi kotak dengan variabel state kita. Properti terpenting di sini adalah onValueChange. Setiap kali pengguna mengetik satu huruf di keyboard, kode di dalam kurung kurawal dieksekusi. Variabel it berisi string terbaru yang diketik. Fungsi it.filter { char -> char.isDigit() || char == '.' } menyisir teks tersebut dan membuang semua karakter selain angka (digit) dan titik (dot). Ini sangat berguna jika pengguna menempelkan (paste) teks acak ke dalam kotak, aplikasi akan langsung menolaknya. Properti keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) memberi sinyal pada sistem Android agar hanya menampilkan keyboard angka saat kotak ini diklik, memudahkan proses input. singleLine = true mencegah pengguna menekan tombol Enter/Return untuk membuat baris baru di dalam form input.

        // Form Input
        OutlinedTextField(
            value = input1,
            onValueChange = { input1 = it.filter { char -> char.isDigit() || char == '.' } },
            label = { Text("Angka Pertama") },
            modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
            singleLine = true
        )

        OutlinedTextField(
            value = input2,
            onValueChange = { input2 = it.filter { char -> char.isDigit() || char == '.' } },
            label = { Text("Angka Kedua") },
            modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp),
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
            singleLine = true
        )

8. Baris Tombol Operasi Matematika

Berbeda dengan Column, tata letak Row digunakan untuk memposisikan komponen berjejer ke samping (horizontal). Pada Row ini, kita menggunakan properti modifier = Modifier.fillMaxWidth() agar memanjang sepenuh layar, dan horizontalArrangement = Arrangement.SpaceEvenly. Properti SpaceEvenly adalah fitur ajaib dari Compose yang akan menghitung lebar layar, mengurangi dengan ukuran keempat tombol, lalu membagi sisa ruang kosong tersebut sama rata untuk diselipkan di antara setiap tombol. Hasilnya, tombol-tombol operasi akan sejajar rapi dengan jarak yang konsisten dan simetris. Kita memanggil komponen khusus OpButton yang telah kita buat (dijelaskan di poin 10), dan menyisipkan fungsi (lambda) { calculate("+") }. Artinya: "Ketika tombol berlogo '+' ditekan, jalankan fungsi calculate dengan mengirimkan parameter tanda tambah". Proses perhitungan dan penampilan hasil terjadi secara instan berkat reaktivitas Jetpack Compose.

        // Tombol Operasi
        Row(
            modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
            horizontalArrangement = Arrangement.SpaceEvenly
        ) {
            OpButton("+") { calculate("+") }
            OpButton("-") { calculate("-") }
            OpButton("*") { calculate("*") }
            OpButton("/") { calculate("/") }
        }

9. Baris Tombol Kontrol (Hapus & Kosongkan)

Bagian ini menggunakan tata letak Row serupa dengan bagian operasi, namun kali ini menampilkan tombol utilitas. Ada satu teknik Modifier penting di sini, yaitu Modifier.weight(1f). Properti weight (bobot) yang diberikan nilai 1 pada kedua tombol memberitahu Compose: "Berikan sisa ruang kosong secara proporsional". Karena keduanya memiliki bobot yang sama (1 berbanding 1), maka tombol "Hapus" dan tombol "Kosongkan" akan membagi lebar layar tepat masing-masing 50%. Untuk fungsionalitas, tombol Hapus mengeksekusi input1.dropLast(1). Ini adalah fungsi bawaan Kotlin untuk mengambil string dan mengembalikan string baru dengan memotong 1 karakter paling belakang, berguna sebagai fungsi Backspace sederhana. Di saat yang sama, ia mengosongkan teks error dan hasil perhitungan untuk memaksa pengguna menghitung ulang. Tombol Kosongkan bertindak sebagai tombol "Reset/Clear All", di mana semua variabel state dikembalikan ke kondisi awalnya (string kosong). Kita juga menggunakan skema warna kustom dari MaterialTheme.colorScheme untuk memberikan petunjuk visual (merah/errorContainer untuk tindakan hapus).

        // Tombol Kontrol (Hapus dan Kosongkan)
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceEvenly
        ) {
            Button(
                onClick = {
                    if (input1.isNotEmpty()) input1 = input1.dropLast(1)
                    errorMessage = ""
                    result = ""
                },
                modifier = Modifier.weight(1f).padding(horizontal = 8.dp),
                colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.errorContainer)
            ) {
                Text("Hapus", color = MaterialTheme.colorScheme.onErrorContainer)
            }
            Button(
                onClick = {
                    input1 = ""
                    input2 = ""
                    result = ""
                    errorMessage = ""
                },
                modifier = Modifier.weight(1f).padding(horizontal = 8.dp),
                colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
            ) {
                Text("Kosongkan", color = MaterialTheme.colorScheme.onSecondaryContainer)
            }
        }
    }
}

10. Komponen Reusable (OpButton)

Dalam pemrograman, terdapat prinsip DRY (Don't Repeat Yourself / Jangan Mengulangi Kodemu Sendiri). Kita tahu keempat tombol operasi matematika (+, -, *, /) memiliki warna, ukuran, bentuk lingkaran, dan gaya font yang identik. Daripada menulis ulang blok kode tombol sepanjang 10 baris sebanyak empat kali (yang akan memakan 40 baris dan sulit di-maintenance), kita membuat sebuah fungsi @Composable baru bernama OpButton. Fungsi ini menerima dua argumen (parameter): symbol bertipe String (untuk teks yang akan ditampilkan, misal "+"), dan onClick yang berupa Lambda Expression () -> Unit (sebuah blok fungsi tanpa parameter yang tidak mengembalikan apa-apa). Ketika kita menetapkan Modifier.size(72.dp) secara statis dengan contentPadding = PaddingValues(0.dp), komponen tombol Material 3 akan otomatis menyesuaikan diri membentuk bulatan sempurna. Warna ditarik dari MaterialTheme agar otomatis beradaptasi jika kelak perangkat pengguna berpindah dari Mode Terang (Light Mode) ke Mode Gelap (Dark Mode).

@Composable
fun OpButton(symbol: String, onClick: () -> Unit) {
    Button(
        onClick = onClick,
        modifier = Modifier.size(72.dp),
        colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
        contentPadding = PaddingValues(0.dp) // Untuk memastikan tombol tetap bulat
    ) {
        Text(
            text = symbol,
            fontSize = 32.sp,
            fontWeight = FontWeight.Bold,
            color = MaterialTheme.colorScheme.onPrimaryContainer
        )
    }
}

Komentar

Postingan Populer