Enum #
Enum adalah salah satu alat paling underrated dalam pemrograman — terlalu sering digantikan dengan String atau konstanta integer yang membuat kode rentan terhadap typo, sulit di-refactor, dan tidak terbaca. Kotlin mengangkat enum jauh melampaui Java: enum Kotlin bisa memiliki property, method, implementasi interface, dan bahkan implementasi yang berbeda per entry. Ini menjadikan enum Kotlin bukan sekadar daftar konstanta, melainkan tipe data yang kaya dan ekspresif. Dikombinasikan dengan when expression yang exhaustive, enum menjadi fondasi yang kuat untuk modeling state, konfigurasi, dan logika bisnis yang terstruktur. Artikel ini membahas seluruh kapabilitas enum Kotlin, perbedaannya dengan sealed class, dan pola idiomatik yang membuat kode lebih aman dan lebih mudah dipahami.
Deklarasi Enum Dasar #
// Deklarasi paling sederhana
enum class Arah {
UTARA, SELATAN, TIMUR, BARAT
}
enum class Status {
AKTIF, NONAKTIF, PENDING, DIHAPUS
}
enum class Prioritas {
RENDAH, SEDANG, TINGGI, KRITIS
}
// Penggunaan
val arah = Arah.UTARA
val status = Status.AKTIF
// Perbandingan
println(arah == Arah.UTARA) // true
println(arah == Arah.SELATAN) // false
// Enum adalah tipe — tidak bisa assign nilai sembarangan
// val salah: Arah = "UTARA" // ERROR: Type mismatch
Property Bawaan #
Setiap enum entry secara otomatis memiliki dua property bawaan: name (nama sebagai String) dan ordinal (posisi berbasis nol).
enum class Planet {
MERKURIUS, VENUS, BUMI, MARS, JUPITER, SATURNUS, URANUS, NEPTUNUS
}
val planet = Planet.BUMI
println(planet.name) // "BUMI"
println(planet.ordinal) // 2 (indeks berbasis 0)
// Iterasi semua entry dengan entries (Kotlin 1.9+)
Planet.entries.forEach { println("${it.ordinal}: ${it.name}") }
// 0: MERKURIUS
// 1: VENUS
// 2: BUMI
// ...
// values() — cara lama, masih bisa dipakai tapi entries lebih disukai
val semuaPlanet: Array<Planet> = Planet.values()
entriesdiperkenalkan di Kotlin 1.9 sebagai penggantivalues(). Perbedaan utama:entriesmengembalikanList<T>yang immutable dan lebih efisien, sedangkanvalues()mengalokasikan array baru setiap dipanggil. Untuk kode baru, selalu gunakanentries.
Enum dengan Property #
Ini adalah salah satu kelebihan terbesar enum Kotlin dibanding Java — setiap entry bisa memiliki data yang terkait.
// Enum dengan property — deklarasikan di constructor
enum class HttpStatus(val kode: Int, val pesan: String) {
OK(200, "OK"),
CREATED(201, "Created"),
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "Unauthorized"),
FORBIDDEN(403, "Forbidden"),
NOT_FOUND(404, "Not Found"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error")
}
// Akses property
val status = HttpStatus.NOT_FOUND
println(status.kode) // 404
println(status.pesan) // "Not Found"
println("${status.kode} ${status.pesan}") // "404 Not Found"
// Contoh yang lebih kaya — satuan ukuran dengan faktor konversi
enum class SatuanBerat(val kgFactor: Double, val simbol: String) {
GRAM(0.001, "g"),
KILOGRAM(1.0, "kg"),
TON(1000.0, "t"),
POUND(0.453592, "lb"),
OUNCE(0.0283495, "oz");
fun keKilogram(nilai: Double): Double = nilai * kgFactor
fun dariKilogram(kg: Double): Double = kg / kgFactor
}
fun konversi(nilai: Double, dari: SatuanBerat, ke: SatuanBerat): Double {
val kg = dari.keKilogram(nilai)
return ke.dariKilogram(kg)
}
println(konversi(1.0, SatuanBerat.KILOGRAM, SatuanBerat.GRAM)) // 1000.0
println(konversi(1.0, SatuanBerat.POUND, SatuanBerat.KILOGRAM)) // 0.453592
println(konversi(500.0, SatuanBerat.GRAM, SatuanBerat.OUNCE)) // 17.637...
Enum dengan Property Mutable #
Property enum bisa var — tapi ini jarang dipakai dan perlu hati-hati karena enum biasanya diharapkan immutable.
// Jarang direkomendasikan, tapi valid
enum class Konfigurasi(var nilai: String) {
HOST("localhost"),
PORT("8080"),
DEBUG("false")
}
// Bisa diubah — tapi perubahan bersifat global!
Konfigurasi.HOST.nilai = "api.example.com"
println(Konfigurasi.HOST.nilai) // "api.example.com"
// ANTI-PATTERN: enum mutable untuk state yang berubah-ubah
// Gunakan data class atau class biasa untuk state yang dinamis
Enum dengan Method #
Enum bisa memiliki method biasa dan method abstrak yang diimplementasikan berbeda di setiap entry.
Method Biasa #
enum class Musim(val bulan: IntRange) {
SEMI(3..5),
PANAS(6..8),
GUGUR(9..11),
DINGIN(12..2); // 12, 1, 2
fun apakahBulanIni(bulan: Int): Boolean = bulan in this.bulan
fun berikutnya(): Musim {
val semua = entries
return semua[(ordinal + 1) % semua.size]
}
fun sebelumnya(): Musim {
val semua = entries
return semua[(ordinal - 1 + semua.size) % semua.size]
}
}
println(Musim.SEMI.berikutnya()) // PANAS
println(Musim.DINGIN.berikutnya()) // SEMI (wrap around)
println(Musim.SEMI.apakahBulanIni(4)) // true
Method Abstrak — Implementasi Berbeda per Entry #
Ini adalah fitur paling powerful dari enum Kotlin: setiap entry bisa memiliki implementasi berbeda dari method yang sama.
// Enum dengan method abstrak — setiap entry mengimplementasikannya
enum class Operasi(val simbol: Char) {
TAMBAH('+') {
override fun hitung(a: Double, b: Double): Double = a + b
},
KURANG('-') {
override fun hitung(a: Double, b: Double): Double = a - b
},
KALI('*') {
override fun hitung(a: Double, b: Double): Double = a * b
},
BAGI('/') {
override fun hitung(a: Double, b: Double): Double {
require(b != 0.0) { "Tidak bisa membagi dengan nol" }
return a / b
}
};
abstract fun hitung(a: Double, b: Double): Double
override fun toString() = simbol.toString()
}
// Penggunaan
val hasil = Operasi.KALI.hitung(6.0, 7.0) // 42.0
println("6 ${Operasi.KALI} 7 = $hasil") // "6 * 7 = 42.0"
// Kalkulator sederhana
fun kalkulasi(a: Double, simbol: Char, b: Double): Double {
val op = Operasi.entries.find { it.simbol == simbol }
?: throw IllegalArgumentException("Operator tidak dikenal: $simbol")
return op.hitung(a, b)
}
kalkulasi(10.0, '+', 5.0) // 15.0
kalkulasi(10.0, '/', 3.0) // 3.333...
// Contoh lain: strategi diskon berbeda per tipe member
enum class TipeMember {
REGULER {
override fun hitungDiskon(harga: Double) = 0.0
override fun batasBeliGratis() = Int.MAX_VALUE
},
SILVER {
override fun hitungDiskon(harga: Double) = harga * 0.05
override fun batasBeliGratis() = 500_000
},
GOLD {
override fun hitungDiskon(harga: Double) = harga * 0.10
override fun batasBeliGratis() = 200_000
},
PLATINUM {
override fun hitungDiskon(harga: Double) = harga * 0.20
override fun batasBeliGratis() = 0
};
abstract fun hitungDiskon(harga: Double): Double
abstract fun batasBeliGratis(): Int
fun hargaAkhir(harga: Double): Double = harga - hitungDiskon(harga)
fun gratisOngkir(totalBelanja: Int): Boolean = totalBelanja >= batasBeliGratis()
}
val member = TipeMember.GOLD
println(member.hargaAkhir(100_000.0)) // 90000.0
println(member.gratisOngkir(250_000)) // true
Enum dan Interface #
Enum bisa mengimplementasikan interface — ini adalah cara untuk memastikan semua entry memiliki kontrak yang sama.
interface Deskripsi {
fun deskripsi(): String
}
interface Dapat dihitung {
fun nilai(): Int
}
enum class Kartu(val simbol: String) : Deskripsi {
AS("A") {
override fun deskripsi() = "As — bisa bernilai 1 atau 11"
},
DUA("2") {
override fun deskripsi() = "Dua — bernilai 2"
},
RAJA("K") {
override fun deskripsi() = "Raja — bernilai 10"
},
RATU("Q") {
override fun deskripsi() = "Ratu — bernilai 10"
},
JACK("J") {
override fun deskripsi() = "Jack — bernilai 10"
};
}
// Interface dengan implementasi default di enum body
interface Formatabel {
fun format(): String
}
enum class StatusPesanan(val kode: String, val label: String) : Formatabel {
MENUNGGU_PEMBAYARAN("WAIT_PAY", "Menunggu Pembayaran"),
DIBAYAR("PAID", "Sudah Dibayar"),
DIPROSES("PROCESSING", "Sedang Diproses"),
DIKIRIM("SHIPPED", "Dalam Pengiriman"),
DITERIMA("DELIVERED", "Diterima"),
DIBATALKAN("CANCELLED", "Dibatalkan");
override fun format(): String = "[$kode] $label"
fun isAktif(): Boolean = this !in listOf(DITERIMA, DIBATALKAN)
fun bisaDibatalkan(): Boolean = this in listOf(MENUNGGU_PEMBAYARAN, DIBAYAR)
fun transisiBerikutnya(): List<StatusPesanan> = when (this) {
MENUNGGU_PEMBAYARAN -> listOf(DIBAYAR, DIBATALKAN)
DIBAYAR -> listOf(DIPROSES, DIBATALKAN)
DIPROSES -> listOf(DIKIRIM)
DIKIRIM -> listOf(DITERIMA)
DITERIMA, DIBATALKAN -> emptyList()
}
}
val pesanan = StatusPesanan.DIBAYAR
println(pesanan.format()) // "[PAID] Sudah Dibayar"
println(pesanan.isAktif()) // true
println(pesanan.bisaDibatalkan()) // true
println(pesanan.transisiBerikutnya()) // [DIPROSES, DIBATALKAN]
Enum di when Expression #
when dengan enum adalah exhaustive secara otomatis — compiler memastikan semua entry ditangani jika digunakan sebagai expression.
enum class Cuaca { CERAH, BERAWAN, HUJAN, BADAI }
// when sebagai expression — harus exhaustive (semua case ditangani)
fun rekomendasiAktivitas(cuaca: Cuaca): String = when (cuaca) {
Cuaca.CERAH -> "Sempurna untuk outdoor!"
Cuaca.BERAWAN -> "Jalan-jalan santai"
Cuaca.HUJAN -> "Baca buku di rumah"
Cuaca.BADAI -> "Tetap di dalam ruangan"
// Tidak perlu else — compiler tahu semua case sudah ditangani
}
// Jika ada entry baru yang ditambahkan ke enum tapi when tidak diupdate:
// COMPILER ERROR — ini adalah keunggulan besar vs String atau Int
// Grouping beberapa case
fun butuhPayung(cuaca: Cuaca): Boolean = when (cuaca) {
Cuaca.HUJAN, Cuaca.BADAI -> true
Cuaca.CERAH, Cuaca.BERAWAN -> false
}
// when sebagai statement — else opsional tapi direkomendasikan
fun log(cuaca: Cuaca) {
when (cuaca) {
Cuaca.BADAI -> println("PERINGATAN: Badai terdeteksi!")
else -> println("Cuaca: ${cuaca.name}")
}
}
// Memanfaatkan property enum di when
enum class Level(val minSkor: Int, val warna: String) {
PEMULA(0, "abu-abu"),
MENENGAH(50, "hijau"),
MAHIR(80, "biru"),
EXPERT(95, "emas")
}
fun deskripsiLevel(level: Level): String = when (level) {
Level.PEMULA -> "Baru memulai perjalanan"
Level.MENENGAH -> "Progres yang baik!"
Level.MAHIR -> "Hampir di puncak"
Level.EXPERT -> "Master sejati!"
}
// Mendapatkan level dari skor
fun levelDariSkor(skor: Int): Level =
Level.entries.lastOrNull { skor >= it.minSkor } ?: Level.PEMULA
Operasi Pencarian pada Enum #
enum class Mata uang(val kode: String, val simbol: String) {
IDR("IDR", "Rp"),
USD("USD", "$"),
EUR("EUR", "€"),
SGD("SGD", "S$"),
MYR("MYR", "RM")
}
// valueOf — cari berdasarkan nama (case-sensitive, throw exception jika tidak ada)
val idr = MataUang.valueOf("IDR") // MataUang.IDR
// MataUang.valueOf("idr") // IllegalArgumentException!
// Cara aman dengan runCatching
fun String.toMataUangOrNull(): MataUang? =
runCatching { MataUang.valueOf(uppercase()) }.getOrNull()
"usd".toMataUangOrNull() // MataUang.USD
"xyz".toMataUangOrNull() // null
// Pencarian berdasarkan property lain
fun cariBerdasarkanKode(kode: String): MataUang? =
MataUang.entries.find { it.kode == kode }
fun cariBerdasarkanSimbol(simbol: String): MataUang? =
MataUang.entries.find { it.simbol == simbol }
cariBerdasarkanKode("EUR") // MataUang.EUR
cariBerdasarkanSimbol("S$") // MataUang.SGD
// Dari ordinal
fun fromOrdinal(ordinal: Int): MataUang? =
MataUang.entries.getOrNull(ordinal)
fromOrdinal(0) // MataUang.IDR
fromOrdinal(99) // null
Enum vs Sealed Class #
Enum dan sealed class keduanya merepresentasikan himpunan tipe tertutup, tapi dengan trade-off berbeda.
flowchart TD
A{Semua instance\npunya shape yang sama?} -- Ya --> B["Enum\nSatu kelas, banyak instance\nSetiap entry: tipe dan data sama"]
A -- Tidak --> C["Sealed Class\nBanyak subclass berbeda\nSetiap subclass: data berbeda"]
B --> D["enum class Status { AKTIF, NONAKTIF }\nSemua punya name dan ordinal"]
C --> E["sealed class Hasil\ndata class Sukses(val data: T)\ndata class Gagal(val pesan: String)\nobject Memuat"]// Kapan Enum lebih tepat: semua entry punya struktur yang sama
enum class StatusKoneksi(val label: String) {
TERHUBUNG("Terhubung"),
TERPUTUS("Terputus"),
MENGHUBUNGKAN("Menghubungkan..."),
ERROR("Error")
}
// Kapan Sealed Class lebih tepat: setiap kasus punya data berbeda
sealed class HasilKoneksi {
object Terhubung : HasilKoneksi()
data class Gagal(val pesan: String, val kode: Int) : HasilKoneksi()
data class Timeout(val detikMenunggu: Int) : HasilKoneksi()
object SedangMenghubungkan : HasilKoneksi()
}
// Enum: tidak bisa menyimpan data berbeda per entry (kecuali di body)
// ANTI-PATTERN: mencoba menyimpan data berbeda di enum biasa
enum class EventBuruk {
KLIK, // tidak butuh data
KETIK, // butuh karakter yang diketik — tidak bisa!
SCROLL // butuh delta — tidak bisa!
}
// BENAR: sealed class untuk event dengan data berbeda
sealed class Event {
object Klik : Event()
data class Ketik(val karakter: Char) : Event()
data class Scroll(val deltaY: Float) : Event()
}
| Enum | Sealed Class | |
|---|---|---|
| Struktur entry | Semua sama | Bisa berbeda |
entries / iterasi | ✓ Bawaan | ✗ Tidak ada |
ordinal dan name | ✓ Bawaan | ✗ Tidak ada |
valueOf | ✓ Bawaan | ✗ Tidak ada |
| Data berbeda per case | ✗ Terbatas | ✓ Bebas |
| Instance berbeda | ✗ Singleton | ✓ Bisa banyak |
| Cocok untuk | Konstanta, state, kategori | Result, event, ADT |
Pola Idiomatik dengan Enum #
Enum sebagai State Machine #
enum class StatusPintu {
TERTUTUP, TERBUKA, TERKUNCI, RUSAK;
fun bisaDibuka(): Boolean = this == TERTUTUP
fun bisaDitutup(): Boolean = this == TERBUKA
fun bisaDikunci(): Boolean = this == TERTUTUP
fun bisaDiperbaiki(): Boolean = this == RUSAK
fun buka(): StatusPintu {
require(bisaDibuka()) { "Pintu tidak bisa dibuka dari status $name" }
return TERBUKA
}
fun tutup(): StatusPintu {
require(bisaDitutup()) { "Pintu tidak bisa ditutup dari status $name" }
return TERTUTUP
}
fun kunci(): StatusPintu {
require(bisaDikunci()) { "Pintu tidak bisa dikunci dari status $name" }
return TERKUNCI
}
}
var pintu = StatusPintu.TERTUTUP
pintu = pintu.buka() // TERBUKA
pintu = pintu.tutup() // TERTUTUP
pintu = pintu.kunci() // TERKUNCI
// pintu = pintu.buka() // IllegalArgumentException: Pintu tidak bisa dibuka dari status TERKUNCI
Enum untuk Konfigurasi #
enum class Environment(
val baseUrl: String,
val logLevel: String,
val debugMode: Boolean,
val timeoutDetik: Int
) {
DEVELOPMENT(
baseUrl = "http://localhost:8080",
logLevel = "DEBUG",
debugMode = true,
timeoutDetik = 60
),
STAGING(
baseUrl = "https://staging.api.example.com",
logLevel = "INFO",
debugMode = true,
timeoutDetik = 30
),
PRODUCTION(
baseUrl = "https://api.example.com",
logLevel = "ERROR",
debugMode = false,
timeoutDetik = 15
);
companion object {
fun dariNama(nama: String): Environment =
entries.find { it.name.equals(nama, ignoreCase = true) }
?: throw IllegalArgumentException("Environment tidak dikenal: $nama")
fun aktif(): Environment {
val nama = System.getenv("APP_ENV") ?: "DEVELOPMENT"
return dariNama(nama)
}
}
}
val env = Environment.aktif()
println("Menghubungkan ke: ${env.baseUrl}")
println("Log level: ${env.logLevel}")
Companion Object dalam Enum #
enum class Warna(val hex: String, val r: Int, val g: Int, val b: Int) {
MERAH("#FF0000", 255, 0, 0),
HIJAU("#00FF00", 0, 255, 0),
BIRU("#0000FF", 0, 0, 255),
PUTIH("#FFFFFF", 255, 255, 255),
HITAM("#000000", 0, 0, 0);
fun luminance(): Double = 0.2126 * r + 0.7152 * g + 0.0722 * b
fun isTerang(): Boolean = luminance() > 128.0
fun warnaTeks(): Warna = if (isTerang()) HITAM else PUTIH
companion object {
fun dariHex(hex: String): Warna? =
entries.find { it.hex.equals(hex, ignoreCase = true) }
fun dariRGB(r: Int, g: Int, b: Int): Warna? =
entries.find { it.r == r && it.g == g && it.b == b }
val warnaCerah: List<Warna>
get() = entries.filter { it.isTerang() }
}
}
println(Warna.MERAH.warnaTeks()) // PUTIH (merah gelap, teks putih lebih terbaca)
println(Warna.PUTIH.warnaTeks()) // HITAM
println(Warna.dariHex("#00FF00")) // HIJAU
println(Warna.warnaCerah) // [HIJAU, PUTIH]
Serialisasi dan Persistensi Enum #
import com.google.gson.*
enum class Role { ADMIN, EDITOR, VIEWER }
// Menyimpan ke database — gunakan name atau kode khusus
data class User(val nama: String, val role: Role)
// Simpan ke DB sebagai String
fun simpanUser(user: User) {
val query = "INSERT INTO users VALUES (?, ?)"
db.execute(query, user.nama, user.role.name) // "ADMIN", "EDITOR", dst
}
// Baca dari DB
fun bacaUser(baris: ResultSet): User = User(
nama = baris.getString("nama"),
role = Role.valueOf(baris.getString("role"))
)
// Serialisasi JSON dengan Gson — enum otomatis jadi String nama
val gson = Gson()
val user = User("Andi", Role.ADMIN)
val json = gson.toJson(user) // {"nama":"Andi","role":"ADMIN"}
val balik = gson.fromJson(json, User::class.java)
// Jika butuh kode numerik — gunakan ordinal atau custom field
enum class StatusDB(val kode: Int) {
DRAFT(0), PUBLISHED(1), ARCHIVED(2);
companion object {
fun dariKode(kode: Int): StatusDB =
entries.find { it.kode == kode }
?: throw IllegalArgumentException("Kode tidak valid: $kode")
}
}
// Simpan kode ke DB, bukan nama — lebih kompak dan stabil
fun simpanStatus(status: StatusDB): Int = status.kode
fun bacaStatus(kode: Int): StatusDB = StatusDB.dariKode(kode)
Ringkasan #
- Enum Kotlin lebih dari sekadar konstanta — bisa memiliki property, method, dan bahkan implementasi berbeda per entry melalui method abstrak. Ini menjadikan enum alat modeling yang kuat.
entries(Kotlin 1.9+) adalah penggantivalues()yang lebih efisien — mengembalikanList<T>immutable, tidak mengalokasikan array baru setiap dipanggil. Gunakanentriesuntuk kode baru.whendengan enum bersifat exhaustive saat digunakan sebagai expression — compiler memastikan semua entry ditangani. Jika entry baru ditambahkan ke enum tapiwhentidak diupdate, terjadi compile error.- Method abstrak per entry memungkinkan setiap enum entry memiliki implementasi berbeda — alternatif bersih untuk Strategy Pattern tanpa perlu kelas terpisah.
valueOf(nama)mencari entry berdasarkan nama (case-sensitive) dan melempar exception jika tidak ditemukan. Bungkus denganrunCatching { }.getOrNull()untuk penanganan aman.- Enum cocok untuk konstanta dengan struktur sama, state machine sederhana, konfigurasi per environment, dan kategori/tipe yang terbatas. Sealed class lebih tepat saat setiap kasus perlu menyimpan data yang berbeda.
- Companion object dalam enum berguna untuk factory methods:
dariKode(),dariNama(), atau properti turunan sepertiwarnaCerah— logika yang berkaitan dengan seluruh enum, bukan satu entry.- Persistensi enum: simpan
nameke database untuk keterbacaan, atau field kustom (kode) untuk kompaknya. Hindariordinaluntuk persistensi — ordinal bisa berubah jika urutan entry diubah.- Interface pada enum memastikan semua entry mengimplementasikan kontrak yang sama — berguna untuk polimorfisme dan dependency injection yang lebih fleksibel.
- Enum mencegah typo dan nilai tidak valid di compile time — selalu lebih baik dari konstanta
StringatauIntyang bisa berisi nilai apapun.