Enum #
Enums are one of the most underrated tools in programming — too often replaced with Strings or integer constants that make code prone to typos, hard to refactor, and unreadable. Kotlin elevates enums far beyond Java: Kotlin enums can have properties, methods, interface implementations, and even different implementations per entry. This makes a Kotlin enum not just a list of constants, but a rich and expressive data type. Combined with the exhaustive when expression, enums become a strong foundation for modeling state, configuration, and structured business logic. This article covers all the capabilities of Kotlin enums, their differences from sealed classes, and the idiomatic patterns that make code safer and easier to understand.
Basic Enum Declarations #
// The simplest declaration
enum class Arah {
UTARA, SELATAN, TIMUR, BARAT
}
enum class Status {
AKTIF, NONAKTIF, PENDING, DIHAPUS
}
enum class Prioritas {
RENDAH, SEDANG, TINGGI, KRITIS
}
// Usage
val arah = Arah.UTARA
val status = Status.AKTIF
// Comparison
println(arah == Arah.UTARA) // true
println(arah == Arah.SELATAN) // false
// An enum is a type — can't assign arbitrary values
// val salah: Arah = "UTARA" // ERROR: Type mismatch
Built-in Properties #
Every enum entry automatically has two built-in properties: name (the name as a String) and ordinal (the zero-based position).
enum class Planet {
MERKURIUS, VENUS, BUMI, MARS, JUPITER, SATURNUS, URANUS, NEPTUNUS
}
val planet = Planet.BUMI
println(planet.name) // "BUMI"
println(planet.ordinal) // 2 (0-based index)
// Iterate all entries with entries (Kotlin 1.9+)
Planet.entries.forEach { println("${it.ordinal}: ${it.name}") }
// 0: MERKURIUS
// 1: VENUS
// 2: BUMI
// ...
// values() — the old way, still usable but entries is preferred
val semuaPlanet: Array<Planet> = Planet.values()
entrieswas introduced in Kotlin 1.9 as a replacement forvalues(). The main difference:entriesreturns an immutableList<T>and is more efficient, whilevalues()allocates a new array on every call. For new code, always useentries.
Enums with Properties #
This is one of the biggest advantages of Kotlin enums over Java — every entry can have associated data.
// An enum with properties — declared in the 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")
}
// Accessing properties
val status = HttpStatus.NOT_FOUND
println(status.kode) // 404
println(status.pesan) // "Not Found"
println("${status.kode} ${status.pesan}") // "404 Not Found"
// A richer example — units of measurement with conversion factors
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...
Enums with Mutable Properties #
Enum properties can be var — but this is rarely used and needs care because enums are usually expected to be immutable.
// Rarely recommended, but valid
enum class Konfigurasi(var nilai: String) {
HOST("localhost"),
PORT("8080"),
DEBUG("false")
}
// Can be changed — but the change is GLOBAL!
Konfigurasi.HOST.nilai = "api.example.com"
println(Konfigurasi.HOST.nilai) // "api.example.com"
// ANTI-PATTERN: mutable enums for constantly changing state
// Use a data class or a regular class for dynamic state
Enums with Methods #
Enums can have regular methods and abstract methods implemented differently in each entry.
Regular Methods #
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
Abstract Methods — Different Implementations per Entry #
This is the most powerful feature of Kotlin enums: every entry can have a different implementation of the same method.
// An enum with an abstract method — every entry implements it
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) { "Can't divide by zero" }
return a / b
}
};
abstract fun hitung(a: Double, b: Double): Double
override fun toString() = simbol.toString()
}
// Usage
val hasil = Operasi.KALI.hitung(6.0, 7.0) // 42.0
println("6 ${Operasi.KALI} 7 = $hasil") // "6 * 7 = 42.0"
// A simple calculator
fun kalkulasi(a: Double, simbol: Char, b: Double): Double {
val op = Operasi.entries.find { it.simbol == simbol }
?: throw IllegalArgumentException("Unknown operator: $simbol")
return op.hitung(a, b)
}
kalkulasi(10.0, '+', 5.0) // 15.0
kalkulasi(10.0, '/', 3.0) // 3.333...
// Another example: different discount strategies per member type
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
Enums and Interfaces #
Enums can implement interfaces — this is a way to ensure all entries have the same contract.
interface Deskripsi {
fun deskripsi(): String
}
interface Dapat dihitung {
fun nilai(): Int
}
enum class Kartu(val simbol: String) : Deskripsi {
AS("A") {
override fun deskripsi() = "Ace — can be worth 1 or 11"
},
DUA("2") {
override fun deskripsi() = "Two — worth 2"
},
RAJA("K") {
override fun deskripsi() = "King — worth 10"
},
RATU("Q") {
override fun deskripsi() = "Queen — worth 10"
},
JACK("J") {
override fun deskripsi() = "Jack — worth 10"
};
}
// An interface with a default implementation in the enum body
interface Formatabel {
fun format(): String
}
enum class StatusPesanan(val kode: String, val label: String) : Formatabel {
MENUNGGU_PEMBAYARAN("WAIT_PAY", "Waiting for Payment"),
DIBAYAR("PAID", "Paid"),
DIPROSES("PROCESSING", "Processing"),
DIKIRIM("SHIPPED", "In Transit"),
DITERIMA("DELIVERED", "Delivered"),
DIBATALKAN("CANCELLED", "Cancelled");
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] Paid"
println(pesanan.isAktif()) // true
println(pesanan.bisaDibatalkan()) // true
println(pesanan.transisiBerikutnya()) // [DIPROSES, DIBATALKAN]
Enums in when Expressions #
when with an enum is automatically exhaustive — the compiler ensures all entries are handled when used as an expression.
enum class Cuaca { CERAH, BERAWAN, HUJAN, BADAI }
// when as an expression — must be exhaustive (all cases handled)
fun rekomendasiAktivitas(cuaca: Cuaca): String = when (cuaca) {
Cuaca.CERAH -> "Perfect for the outdoors!"
Cuaca.BERAWAN -> "A leisurely stroll"
Cuaca.HUJAN -> "Read a book at home"
Cuaca.BADAI -> "Stay indoors"
// No else needed — the compiler knows all cases are handled
}
// If a new entry is added to the enum but the when isn't updated:
// COMPILER ERROR — this is a huge advantage over String or Int
// Grouping several cases
fun butuhPayung(cuaca: Cuaca): Boolean = when (cuaca) {
Cuaca.HUJAN, Cuaca.BADAI -> true
Cuaca.CERAH, Cuaca.BERAWAN -> false
}
// when as a statement — else is optional but recommended
fun log(cuaca: Cuaca) {
when (cuaca) {
Cuaca.BADAI -> println("WARNING: Storm detected!")
else -> println("Weather: ${cuaca.name}")
}
}
// Leveraging enum properties in when
enum class Level(val minSkor: Int, val warna: String) {
PEMULA(0, "grey"),
MENENGAH(50, "green"),
MAHIR(80, "blue"),
EXPERT(95, "gold")
}
fun deskripsiLevel(level: Level): String = when (level) {
Level.PEMULA -> "Just starting the journey"
Level.MENENGAH -> "Good progress!"
Level.MAHIR -> "Almost at the top"
Level.EXPERT -> "A true master!"
}
// Getting a level from a score
fun levelDariSkor(skor: Int): Level =
Level.entries.lastOrNull { skor >= it.minSkor } ?: Level.PEMULA
Search Operations on Enums #
enum class Mata uang(val kode: String, val simbol: String) {
IDR("IDR", "Rp"),
USD("USD", "$"),
EUR("EUR", "€"),
SGD("SGD", "S$"),
MYR("MYR", "RM")
}
// valueOf — search by name (case-sensitive, throws if not found)
val idr = MataUang.valueOf("IDR") // MataUang.IDR
// MataUang.valueOf("idr") // IllegalArgumentException!
// A safe way with runCatching
fun String.toMataUangOrNull(): MataUang? =
runCatching { MataUang.valueOf(uppercase()) }.getOrNull()
"usd".toMataUangOrNull() // MataUang.USD
"xyz".toMataUangOrNull() // null
// Searching by other properties
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
// From an ordinal
fun fromOrdinal(ordinal: Int): MataUang? =
MataUang.entries.getOrNull(ordinal)
fromOrdinal(0) // MataUang.IDR
fromOrdinal(99) // null
Enum vs Sealed Class #
Both enums and sealed classes represent closed sets of types, but with different trade-offs.
flowchart TD
A{Do all instances\nhave the same shape?} -- Yes --> B["Enum\nOne class, many instances\nEvery entry: same type and data"]
A -- No --> C["Sealed Class\nMany different subclasses\nEach subclass: different data"]
B --> D["enum class Status { AKTIF, NONAKTIF }\nAll have name and ordinal"]
C --> E["sealed class Hasil\ndata class Sukses(val data: T)\ndata class Gagal(val pesan: String)\nobject Memuat"]// When Enum is more appropriate: all entries have the same structure
enum class StatusKoneksi(val label: String) {
TERHUBUNG("Connected"),
TERPUTUS("Disconnected"),
MENGHUBUNGKAN("Connecting..."),
ERROR("Error")
}
// When Sealed Class is more appropriate: each case has different data
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: can't store different data per entry (except in the body)
// ANTI-PATTERN: trying to store different data in a regular enum
enum class EventBuruk {
KLIK, // doesn't need data
KETIK, // needs the typed character — can't!
SCROLL // needs a delta — can't!
}
// CORRECT: a sealed class for events with different data
sealed class Event {
object Klik : Event()
data class Ketik(val karakter: Char) : Event()
data class Scroll(val deltaY: Float) : Event()
}
| Enum | Sealed Class | |
|---|---|---|
| Entry structure | All the same | Can differ |
entries / iteration | ✓ Built-in | ✗ None |
ordinal and name | ✓ Built-in | ✗ None |
valueOf | ✓ Built-in | ✗ None |
| Different data per case | ✗ Limited | ✓ Free |
| Different instances | ✗ Singleton | ✓ Many |
| Best for | Constants, state, categories | Results, events, ADTs |
Idiomatic Patterns with Enums #
Enums as State Machines #
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()) { "Door can't be opened from $name status" }
return TERBUKA
}
fun tutup(): StatusPintu {
require(bisaDitutup()) { "Door can't be closed from $name status" }
return TERTUTUP
}
fun kunci(): StatusPintu {
require(bisaDikunci()) { "Door can't be locked from $name status" }
return TERKUNCI
}
}
var pintu = StatusPintu.TERTUTUP
pintu = pintu.buka() // TERBUKA
pintu = pintu.tutup() // TERTUTUP
pintu = pintu.kunci() // TERKUNCI
// pintu = pintu.buka() // IllegalArgumentException: Door can't be opened from TERKUNCI status
Enums for Configuration #
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("Unknown environment: $nama")
fun aktif(): Environment {
val nama = System.getenv("APP_ENV") ?: "DEVELOPMENT"
return dariNama(nama)
}
}
}
val env = Environment.aktif()
println("Connecting to: ${env.baseUrl}")
println("Log level: ${env.logLevel}")
Companion Objects in Enums #
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 (dark red, white text is more readable)
println(Warna.PUTIH.warnaTeks()) // HITAM
println(Warna.dariHex("#00FF00")) // HIJAU
println(Warna.warnaCerah) // [HIJAU, PUTIH]
Serialization and Enum Persistence #
import com.google.gson.*
enum class Role { ADMIN, EDITOR, VIEWER }
// Storing in a database — use the name or a special code
data class User(val nama: String, val role: Role)
// Save to the DB as a String
fun simpanUser(user: User) {
val query = "INSERT INTO users VALUES (?, ?)"
db.execute(query, user.nama, user.role.name) // "ADMIN", "EDITOR", etc.
}
// Read from the DB
fun bacaUser(baris: ResultSet): User = User(
nama = baris.getString("nama"),
role = Role.valueOf(baris.getString("role"))
)
// JSON serialization with Gson — enums automatically become name Strings
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)
// If you need a numeric code — use an ordinal or a 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("Invalid code: $kode")
}
}
// Save the code to the DB, not the name — more compact and stable
fun simpanStatus(status: StatusDB): Int = status.kode
fun bacaStatus(kode: Int): StatusDB = StatusDB.dariKode(kode)
Summary #
- Kotlin enums are more than constants — they can have properties, methods, and even different implementations per entry through abstract methods. This makes enums a powerful modeling tool.
entries(Kotlin 1.9+) is the more efficient replacement forvalues()— returns an immutableList<T>, doesn’t allocate a new array on every call. Useentriesfor new code.whenwith enums is exhaustive when used as an expression — the compiler ensures all entries are handled. If a new entry is added to the enum but thewhenisn’t updated, a compile error occurs.- Abstract methods per entry allow every enum entry to have a different implementation — a clean alternative to the Strategy Pattern without separate classes.
valueOf(name)searches for an entry by name (case-sensitive) and throws an exception if not found. Wrap it withrunCatching { }.getOrNull()for safe handling.- Enums are suitable for constants with the same structure, simple state machines, per-environment configuration, and limited categories/types. Sealed classes are more appropriate when each case needs to store different data.
- Companion objects in enums are useful for factory methods:
dariKode(),dariNama(), or derived properties likewarnaCerah— logic related to the whole enum, not a single entry.- Enum persistence: save
nameto a database for readability, or a custom field (kode) for compactness. Avoidordinalfor persistence — ordinals can change if the entry order is modified.- Interfaces on enums ensure all entries implement the same contract — useful for polymorphism and more flexible dependency injection.
- Enums prevent typos and invalid values at compile time — always better than
StringorIntconstants that can hold any value.