Setiap aplikasi Android modern dimulai dari satu pertanyaan: bagaimana cara menyajikan data ke pengguna dengan Compose? Pada Bagian 1 ini kita menjawab pertanyaan tersebut dengan membangun kerangka antarmuka untuk pencatat keuangan: LazyColumn daftar transaksi, item composable yang menampilkan tiap transaksi, dan layar form tambah transaksi. Kita sengaja menulis kode sebagai Kotlin murni yang dapat dijalankan di terminal, sehingga konsep composable dan state-nya dapat dipelajari tanpa setup Android Studio.
Composable sebagai Fungsi Murni #
Inti filosofi Jetpack Compose adalah composable: fungsi @Composable yang menerima data (state) sebagai input dan mendeskripsikan UI sebagai output, tanpa efek samping. Composable tidak langsung memanipulasi View seperti pendekatan lama (XML + findViewById); composable mengembalikan deskripsi pohon UI yang kemudian di-render oleh runtime Compose.
Pola fungsi murni ini membuat UI mudah diuji: berikan state tertentu, dan hasil render dapat diprediksi. Pola yang sama kita pakai pada kode contoh di bawah — renderLayarDaftar(state) menerima UiStateDaftar dan mencetak representasi UI ke terminal. Logika yang sama, jika dipindah ke @Composable fun LayarDaftar(state: UiStateDaftar), akan menghasilkan LazyColumn berisi item transaksi di Android.
State UI untuk Layar Daftar #
Sebelum menulis composable, kita perlu mendefinisikan state yang dipakai layar. Pola umum Compose adalah membungkus seluruh state layar ke dalam satu data class:
data class UiStateDaftar(
val transaksi: List<Transaksi> = emptyList(),
val sedangMemuat: Boolean = false
)UiStateDaftar berisi dua hal: daftar transaksi yang akan ditampilkan dan flag sedangMemuat untuk menampilkan spinner saat data sedang dimuat dari database. Pemisahan status memuat dari data membuat UI dapat menampilkan empty state, loading state, dan data state sebagai tiga cabang berbeda.
Komponen Antarmuka #
Kode berikut mendemonstrasikan tiga komponen yang akan kita bangun: LayarDaftar (LazyColumn), ItemTransaksi (item composable), dan FormTambahTransaksi (form input). Kode berjalan di terminal, tetapi polanya identik dengan Compose asli.
// Bagian 1: Desain antarmuka Jetpack Compose.
// Mendemonstrasikan pola composable sebagai fungsi murni: menerima state,
// mengembalikan deskripsi UI yang dapat di-render. LazyColumn disimulasikan
// dengan iterasi list transaksi dan println setiap item.
enum class JenisTransaksi { PEMASUKAN, PENGELUARAN }
data class Transaksi(
val id: Long,
val jenis: JenisTransaksi,
val jumlah: Long,
val kategori: String,
val catatan: String
)
data class UiStateDaftar(
val transaksi: List<Transaksi> = emptyList(),
val sedangMemuat: Boolean = false
)
// Simulasi pemanggilan Composable di terminal.
// Pada Compose asli: @Composable fun LayarDaftar(state: UiStateDaftar) { ... }
fun renderLayarDaftar(state: UiStateDaftar) {
println("--- LayarDaftar ---")
if (state.sedangMemuat) {
println("[Spinner: Memuat data...]")
return
}
if (state.transaksi.isEmpty()) {
println("[Empty state: Belum ada transaksi]")
return
}
println("[Header: Daftar Transaksi]")
// LazyColumn: hanya render item yang terlihat di layar.
// Untuk simulasi terminal, kita render semua item.
state.transaksi.forEachIndexed { idx, t ->
renderItemTransaksi(idx, t)
}
println("[Footer: ${state.transaksi.size} transaksi]")
}
fun renderItemTransaksi(indeks: Int, t: Transaksi) {
val arah = if (t.jenis == JenisTransaksi.PEMASUKAN) "+" else "-"
val warna = if (t.jenis == JenisTransaksi.PEMASUKAN) "hijau" else "merah"
println(" Item#$indeks $arah Rp${t.jumlah} | ${t.kategori} - ${t.catatan} (warna=$warna)")
}
fun renderTombol(label: String, enabled: Boolean = true) {
val status = if (enabled) "enabled" else "disabled"
println(" [Button: $label] ($status)")
}
fun renderFormTambah() {
println("--- FormTambahTransaksi ---")
println(" [TextField: Judul catatan]")
println(" [Dropdown: Jenis (Pemasukan/Pengeluaran)]")
println(" [TextField numeric: Jumlah]")
println(" [TextField: Kategori]")
renderTombol("Simpan", enabled = true)
renderTombol("Batal", enabled = true)
}
fun main() {
val dataAwal = UiStateDaftar(transaksi = emptyList(), sedangMemuat = true)
val dataKosong = UiStateDaftar(transaksi = emptyList(), sedangMemuat = false)
val dataIsi = UiStateDaftar(
transaksi = listOf(
Transaksi(1, JenisTransaksi.PEMASUKAN, 5_000_000, "Gaji", "Gaji Januari"),
Transaksi(2, JenisTransaksi.PENGELUARAN, 750_000, "Makan", "Makan siang"),
Transaksi(3, JenisTransaksi.PENGELUARAN, 1_200_000, "Transport", "Bensin")
),
sedangMemuat = false
)
println("=== Skenario 1: Loading ===")
renderLayarDaftar(dataAwal)
println()
println("=== Skenario 2: Empty ===")
renderLayarDaftar(dataKosong)
println()
println("=== Skenario 3: Berisi ===")
renderLayarDaftar(dataIsi)
println()
println("=== Skenario 4: Form tambah ===")
renderFormTambah()
}Output yang Dihasilkan:
=== Skenario 1: Loading ===
--- LayarDaftar ---
[Spinner: Memuat data...]
=== Skenario 2: Empty ===
--- LayarDaftar ---
[Empty state: Belum ada transaksi]
=== Skenario 3: Berisi ===
--- LayarDaftar ---
[Header: Daftar Transaksi]
Item#0 + Rp5000000 | Gaji - Gaji Januari (warna=hijau)
Item#1 - Rp750000 | Makan - Makan siang (warna=merah)
Item#2 - Rp1200000 | Transport - Bensin (warna=merah)
[Footer: 3 transaksi]
=== Skenario 4: Form tambah ===
--- FormTambahTransaksi ---
[TextField: Judul catatan]
[Dropdown: Jenis (Pemasukan/Pengeluaran)]
[TextField numeric: Jumlah]
[TextField: Kategori]
[Button: Simpan] (enabled)
[Button: Batal] (enabled)Mari kita bedah komponen-komponen di atas:
data class UiStateDaftar: membungkus seluruh state layar dalam satu kelas. Compose merekomendasikan pola ini agar state dapat di-pass sebagai satu argumen dan di-compare dengan mudah untuk menentukan apakah perlu re-render.renderLayarDaftar(state): mengandung tiga cabang — loading, empty, dan filled. Cabang loading menampilkan spinner; cabang empty menampilkan pesan placeholder; cabang filled menampilkanLazyColumnitem.forEachIndexed { idx, t -> renderItemTransaksi(idx, t) }: simulasiLazyColumn. Di Compose asli,LazyColumnhanya merender item yang terlihat di layar untuk efisiensi memori; untuk daftar kecil,Columnsederhana juga cukup.renderItemTransaksi(indeks, t): composable item. Di Compose asli, ini akan menjadi@Composable fun ItemTransaksi(t: Transaksi)yang menggunakanRowberisiTextuntuk jumlah, kategori, dan catatan.renderFormTambah(): composable form. Di Compose asli, setiapTextFielddiikat ke state viavalue = state.xdanonValueChange = { state = state.copy(x = it) }.
LazyColumn vs Column #
LazyColumn adalah versi RecyclerView di Compose: hanya merender item yang terlihat di viewport. Untuk daftar pendek (< 20 item), Column sederhana dengan forEach cukup dan lebih ringan. Untuk daftar panjang (ratusan atau ribuan item), LazyColumn menghemat memori dan mempercepat render awal.
Pilihan antara keduanya mudah diubah karena keduanya menerima content: @Composable () -> Unit. Migrasi dari Column ke LazyColumn biasanya hanya mengganti nama composable dan membungkus setiap item dengan item { ... }.
Empty State dan Loading State #
Dua state yang sering dilupakan pemula adalah loading dan empty. Tanpa penanganan khusus, layar akan tampak kosong tanpa penjelasan saat data sedang dimuat atau belum ada. Kode di atas menangani keduanya secara eksplisit:
- Loading: tampilkan spinner atau teks “Memuat…”. Pengguna tahu aplikasi sedang bekerja.
- Empty: tampilkan ilustrasi dan teks “Belum ada transaksi” dengan tombol “Tambah” yang menavigasi ke form.
State sedangMemuat dan daftar kosong adalah dua kondisi berbeda. Loading berarti data belum tersedia (sedang diproses); empty berarti data tersedia tapi isinya nol. Membedakan keduanya memberi UX yang lebih baik.
Color dan Theme #
Compose memakai MaterialTheme untuk warna, tipografi, dan bentuk. Kode di atas hanya menandai warna sebagai string (“hijau”, “merah”) untuk demo. Di Compose asli, gunakan MaterialTheme.colorScheme.primary dan MaterialTheme.colorScheme.error:
val warna = if (t.jenis == JenisTransaksi.PEMASUKAN)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.error
Text(text = "Rp${t.jumlah}", color = warna)MaterialTheme mengikuti light/dark mode otomatis. Saat peranti beralih ke dark mode, warna primer dan error menyesuaikan tanpa perlu kode tambahan.
Referensi Lanjutan #
- Kotlin Multiplatform Mobile getting started — panduan resmi KMP Mobile untuk merancang antarmuka aplikasi Android Kotlin
- Android Studio — halaman resmi Android Studio sebagai IDE utama untuk mendesain UI aplikasi
Ringkasan Bagian 1 #
Kerangka antarmuka telah siap: UiStateDaftar membungkus state layar, renderLayarDaftar menangani tiga cabang (loading, empty, filled), renderItemTransaksi menampilkan setiap transaksi, dan renderFormTambah menyediakan form input. Pada Bagian 2 kita menambahkan state management dengan ViewModel dan navigasi antar layar, sehingga aplikasi dapat berpindah dari daftar ke form dan kembali.