Konstanta

Konstanta #

Setiap program punya nilai-nilai yang tidak boleh berubah selama eksekusi: URL endpoint, batas maksimum percobaan login, nama database, kode error standar. Menyimpan nilai-nilai ini sebagai literal yang tersebar di seluruh kode — yang sering disebut magic number atau magic string — adalah praktik yang berbahaya. Satu perubahan nilai memaksa kamu mencari dan mengganti di banyak tempat, dengan risiko melewatkan satu dan menciptakan inkonsistensi. Kotlin menyediakan dua mekanisme untuk konstanta: val untuk nilai yang ditentukan saat runtime, dan const val untuk nilai yang sudah pasti sejak kompilasi. Memahami perbedaan keduanya — dan kapan menggunakan yang mana — adalah bagian penting dari menulis kode Kotlin yang bersih dan efisien.

val sebagai Runtime Constant #

val mendeklarasikan referensi read-only: nilainya ditentukan satu kali saat runtime dan tidak bisa di-reassign setelahnya. Ini bukan konstanta dalam arti kompilasi — nilainya baru diketahui ketika program berjalan.

val pi = 3.14159
val tahunSekarang = Calendar.getInstance().get(Calendar.YEAR)
val namaHost = InetAddress.getLocalHost().hostName
val waktuMulai = System.currentTimeMillis()

Tiga contoh terakhir menunjukkan karakteristik val yang membedakannya dari const val: nilainya dihitung saat runtime, bisa berbeda di setiap eksekusi program, dan bisa memanggil fungsi atau membuat objek.

// val yang nilainya dihitung saat objek dibuat
class KonfigurasiAplikasi {
    val versi = BuildConfig.VERSION_NAME           // dibaca dari build config
    val lingkungan = System.getenv("APP_ENV") ?: "development"
    val koneksiMaksimal = Runtime.getRuntime().availableProcessors() * 2
}

Dalam skenario ini, val adalah pilihan yang tepat karena nilainya tidak diketahui saat kompilasi — ia bergantung pada environment dan hardware tempat program berjalan.


const val — Compile-Time Constant #

const val adalah konstanta yang sesungguhnya. Nilainya harus diketahui oleh compiler pada saat kompilasi, bukan saat runtime. Ini berarti compiler bisa langsung “menginline” nilai tersebut ke setiap tempat yang menggunakannya, menghasilkan bytecode yang lebih efisien.

const val VERSI_API = "v2"
const val URL_BASIS = "https://api.example.com/"
const val BATAS_PERCOBAAN_LOGIN = 5
const val TIMEOUT_MS = 30_000L
const val NAMA_DATABASE = "myapp_db"
const val DEBUG = false

Perbedaan cara compiler memperlakukan val vs const val bisa dilihat dari bytecode yang dihasilkan:

// Kode Kotlin
const val MAKS = 100
val batas = 100

fun cek(nilai: Int) = nilai <= MAKS
fun cek2(nilai: Int) = nilai <= batas

Setelah dikompilasi, cek setara dengan nilai <= 100 (nilai di-inline), sementara cek2 harus mengambil nilai batas dari memori setiap kali dipanggil. Untuk konstanta yang sering diakses dalam loop atau kode performa-kritis, perbedaan ini nyata.

Aturan const val #

const val punya aturan yang lebih ketat dibanding val:

const val HANYA BISA untuk:
  ✓ Tipe primitif: Int, Long, Double, Float, Short, Byte, Char, Boolean
  ✓ String
  ✓ Nilai literal — angka, string, atau ekspresi dari literal
  ✓ Dideklarasikan di top-level file atau di dalam object / companion object

const val TIDAK BISA untuk:
  ✗ Tipe referensi kustom (kelas, data class, list, dll)
  ✗ Hasil pemanggilan fungsi (termasuk konstruktor)
  ✗ Nilai yang baru diketahui saat runtime
  ✗ Dideklarasikan di dalam kelas biasa atau fungsi
// ✓ BENAR
const val NAMA = "MyApp"
const val VERSI = 2
const val PI = 3.14159

// ✗ TIDAK BISA: hasil fungsi
const val WAKTU = System.currentTimeMillis()  // error

// ✗ TIDAK BISA: tipe kustom
const val CONFIG = Config()  // error

// ✗ TIDAK BISA: di dalam fungsi
fun setup() {
    const val TIMEOUT = 5000  // error
}

Tempat Deklarasi Konstanta #

Kotlin menyediakan tiga lokasi utama untuk menempatkan const val, masing-masing dengan trade-off yang berbeda.

Top-Level — Paling Mudah Diakses #

Deklarasi langsung di level file, di luar kelas atau fungsi. Ini adalah cara paling sederhana dan paling langsung.

// File: Konstanta.kt
package com.myapp.core

const val VERSI_APLIKASI = "1.0.0"
const val URL_API = "https://api.myapp.com/v2"
const val BATAS_UPLOAD_BYTE = 10 * 1024 * 1024  // 10 MB
const val FORMAT_TANGGAL = "yyyy-MM-dd"
const val FORMAT_WAKTU = "HH:mm:ss"

Cara menggunakannya di file lain:

import com.myapp.core.URL_API
import com.myapp.core.FORMAT_TANGGAL

// atau import semua sekaligus
import com.myapp.core.*

fun buatPermintaan(): HttpRequest {
    return HttpRequest.Builder()
        .url(URL_API + "/users")
        .build()
}

object — Konstanta Terkelompok #

Mengelompokkan konstanta yang saling berkaitan ke dalam sebuah object membuat kode lebih terorganisir dan memberi namespace yang jelas.

object KonstantaJaringan {
    const val TIMEOUT_KONEKSI_MS = 10_000L
    const val TIMEOUT_BACA_MS = 30_000L
    const val MAKS_PERCOBAAN_ULANG = 3
    const val JEDA_PERCOBAAN_ULANG_MS = 2_000L
    const val UKURAN_POOL_KONEKSI = 20
}

object KonstantaValidasi {
    const val PANJANG_SANDI_MINIMUM = 8
    const val PANJANG_SANDI_MAKSIMUM = 128
    const val PANJANG_NAMA_MINIMUM = 2
    const val PANJANG_NAMA_MAKSIMUM = 100
    const val MAKS_PERCOBAAN_LOGIN = 5
    const val DURASI_KUNCI_AKUN_MENIT = 30
}

Penggunaan:

fun konfigurasikanOkHttp(): OkHttpClient {
    return OkHttpClient.Builder()
        .connectTimeout(KonstantaJaringan.TIMEOUT_KONEKSI_MS, TimeUnit.MILLISECONDS)
        .readTimeout(KonstantaJaringan.TIMEOUT_BACA_MS, TimeUnit.MILLISECONDS)
        .retryOnConnectionFailure(true)
        .build()
}

companion object — Konstanta Terikat Kelas #

Saat konstanta sangat erat kaitannya dengan sebuah kelas, letakkan di dalam companion object kelas tersebut.

class HttpKlien private constructor(val urlBasis: String) {

    companion object {
        const val HEADER_OTORISASI = "Authorization"
        const val HEADER_TIPE_KONTEN = "Content-Type"
        const val TIPE_KONTEN_JSON = "application/json"
        const val TIPE_KONTEN_FORM = "application/x-www-form-urlencoded"

        fun buatDenganUrl(url: String): HttpKlien = HttpKlien(url)
    }

    fun tambahHeaderJson(request: Request.Builder): Request.Builder {
        return request
            .header(HEADER_TIPE_KONTEN, TIPE_KONTEN_JSON)
    }
}

// Akses dari luar
val tipeKonten = HttpKlien.TIPE_KONTEN_JSON

Diagram: Memilih Jenis Konstanta #

flowchart TD
    A{Nilai diketahui\nsaat kompilasi?} -- Ya --> B{Tipe primitif\natau String?}
    A -- Tidak --> C["val\nnilai runtime"]
    B -- Ya --> D{Terkait erat\ndengan satu kelas?}
    B -- Tidak --> C
    D -- Ya --> E["const val\ndalam companion object"]
    D -- Tidak --> F{Perlu\ndikelompokkan?}
    F -- Ya --> G["const val\ndalam object"]
    F -- Tidak --> H["const val\ntop-level"]

const val dalam Annotation #

Salah satu use case penting const val yang tidak bisa digantikan val biasa adalah penggunaannya sebagai argumen annotation. Annotation di JVM hanya menerima nilai compile-time — bukan nilai runtime.

const val NAMA_TABEL = "pengguna"
const val KOLOM_ID = "id"
const val KOLOM_NAMA = "nama"
const val KOLOM_EMAIL = "email"

@Entity(tableName = NAMA_TABEL)
data class Pengguna(
    @PrimaryKey
    @ColumnInfo(name = KOLOM_ID)
    val id: Long = 0,

    @ColumnInfo(name = KOLOM_NAMA)
    val nama: String,

    @ColumnInfo(name = KOLOM_EMAIL)
    val email: String
)

Tanpa const val, kamu terpaksa menulis string literal langsung di annotation — yang rawan typo dan sulit di-refactor:

// ANTI-PATTERN: magic string di annotation — rawan typo, sulit refactor
@Entity(tableName = "pengguna")
data class Pengguna(
    @PrimaryKey
    @ColumnInfo(name = "id")
    val id: Long = 0,

    @ColumnInfo(name = "nama")  // kalau typo "naama" — error hanya ketahuan saat runtime
    val nama: String
)

// BENAR: gunakan const val — refactor aman, typo ketahuan saat kompilasi
@Entity(tableName = NAMA_TABEL)
data class Pengguna(
    @PrimaryKey
    @ColumnInfo(name = KOLOM_ID)
    val id: Long = 0,

    @ColumnInfo(name = KOLOM_NAMA)
    val nama: String
)

val vs const val — Perbandingan Lengkap #

Aspekvalconst val
Waktu penentuan nilaiRuntimeCompile-time
Tipe yang didukungSemua tipePrimitif dan String
Bisa memanggil fungsi✓ Ya✗ Tidak
Bisa membuat objek✓ Ya✗ Tidak
Lokasi deklarasiDi mana sajaTop-level, object, companion object
Inline oleh compiler✗ Tidak✓ Ya
Bisa dipakai di annotation✗ Tidak✓ Ya
Performa aksesAkses memoriDi-inline langsung

Enum sebagai Konstanta Terstruktur #

Untuk kumpulan nilai yang saling eksklusif dan memiliki hubungan semantik, enum class adalah pilihan yang lebih ekspresif dibanding serangkaian const val.

// ANTI-PATTERN: const val untuk nilai yang saling berkaitan
const val STATUS_MENUNGGU = "PENDING"
const val STATUS_DIPROSES = "PROCESSING"
const val STATUS_SELESAI = "COMPLETED"
const val STATUS_GAGAL = "FAILED"

// Tidak ada yang mencegah kode seperti ini:
fun prosesTransaksi(status: String) { ... }
prosesTransaksi("SALAH_TULIS")  // ✗ tidak ketahuan saat kompilasi!

// BENAR: enum memberi type safety
enum class StatusTransaksi {
    MENUNGGU, DIPROSES, SELESAI, GAGAL
}

fun prosesTransaksi(status: StatusTransaksi) { ... }
prosesTransaksi(StatusTransaksi.SELESAI)       // ✓
// prosesTransaksi("SELESAI")                  // ✗ error kompilasi!

Enum bisa diperkaya dengan properti dan fungsi:

enum class KodeHttp(val kode: Int, val pesan: String) {
    OK(200, "Berhasil"),
    DIBUAT(201, "Resource berhasil dibuat"),
    TIDAK_VALID(400, "Permintaan tidak valid"),
    TIDAK_TEROTORISASI(401, "Autentikasi diperlukan"),
    DILARANG(403, "Akses ditolak"),
    TIDAK_DITEMUKAN(404, "Resource tidak ditemukan"),
    ERROR_SERVER(500, "Kesalahan internal server");

    val berhasil: Boolean get() = kode in 200..299
    val errorKlien: Boolean get() = kode in 400..499
    val errorServer: Boolean get() = kode in 500..599

    override fun toString() = "$kode $pesan"
}

// Penggunaan
val respons = KodeHttp.TIDAK_DITEMUKAN
println(respons)                    // 404 Resource tidak ditemukan
println(respons.berhasil)           // false
println(respons.errorKlien)         // true

// when dengan enum — exhaustive check otomatis
fun tanganiRespons(kode: KodeHttp): String {
    return when (kode) {
        KodeHttp.OK, KodeHttp.DIBUAT -> "Sukses: ${kode.pesan}"
        KodeHttp.TIDAK_VALID -> "Perbaiki input kamu"
        KodeHttp.TIDAK_TEROTORISASI -> "Silakan login terlebih dahulu"
        KodeHttp.DILARANG -> "Kamu tidak punya akses ke resource ini"
        KodeHttp.TIDAK_DITEMUKAN -> "Resource yang dicari tidak ada"
        KodeHttp.ERROR_SERVER -> "Ada masalah di server, coba lagi nanti"
    }
    // Compiler memaksa semua kasus ditangani — tidak bisa lupa satu pun
}

Mengelola Konstanta dalam Proyek Besar #

Saat proyek berkembang, organisasi konstanta menjadi penting. Beberapa pola yang umum digunakan:

Satu File Per Domain #

src/main/kotlin/com/myapp/
  ├── core/
  │   ├── KonstantaAplikasi.kt     // versi, nama, environment
  │   ├── KonstantaJaringan.kt     // timeout, retry, pool size
  │   └── KonstantaValidasi.kt     // panjang field, format, regex
  ├── fitur/
  │   ├── auth/
  │   │   └── KonstantaAuth.kt     // JWT, session, token
  │   └── pembayaran/
  │       └── KonstantaPembayaran.kt // midtrans, xendit config

Hierarki Object Tersarang #

object Konstanta {
    object Jaringan {
        const val TIMEOUT_MS = 30_000L
        const val MAKS_PERCOBAAN = 3
        
        object Header {
            const val OTORISASI = "Authorization"
            const val TIPE_KONTEN = "Content-Type"
        }
    }
    
    object Database {
        const val NAMA = "myapp.db"
        const val VERSI = 5
        const val MAKS_KONEKSI = 10
    }
    
    object Paginasi {
        const val UKURAN_HALAMAN_DEFAULT = 20
        const val UKURAN_HALAMAN_MAKSIMUM = 100
    }
}

// Penggunaan
val timeout = Konstanta.Jaringan.TIMEOUT_MS
val headerAuth = Konstanta.Jaringan.Header.OTORISASI
val namaDb = Konstanta.Database.NAMA

Menghindari Magic Number dan Magic String #

Magic number adalah angka literal yang muncul di kode tanpa penjelasan konteks — pembaca harus menebak artinya.

// ANTI-PATTERN: magic number dan magic string
fun validasiSandi(sandi: String): Boolean {
    return sandi.length >= 8 &&
        sandi.length <= 128 &&
        sandi.matches(Regex(".*[A-Z].*")) &&
        sandi.matches(Regex(".*[0-9].*"))
}

fun hitungDiskon(harga: Double, kodeAnggota: String): Double {
    return when (kodeAnggota) {
        "GOLD"     -> harga * 0.80   // 20% diskon — tapi mengapa 0.80?
        "SILVER"   -> harga * 0.90   // 10% diskon
        "BRONZE"   -> harga * 0.95   // 5% diskon
        else       -> harga
    }
}

// BENAR: beri nama pada semua nilai bermakna
object KonstantaValidasi {
    const val SANDI_PANJANG_MIN = 8
    const val SANDI_PANJANG_MAKS = 128
    const val REGEX_HURUF_BESAR = ".*[A-Z].*"
    const val REGEX_ANGKA = ".*[0-9].*"
}

object KonstantaDiskon {
    const val GOLD_PERSEN = 20
    const val SILVER_PERSEN = 10
    const val BRONZE_PERSEN = 5
}

fun validasiSandi(sandi: String): Boolean {
    return sandi.length >= KonstantaValidasi.SANDI_PANJANG_MIN &&
        sandi.length <= KonstantaValidasi.SANDI_PANJANG_MAKS &&
        sandi.matches(Regex(KonstantaValidasi.REGEX_HURUF_BESAR)) &&
        sandi.matches(Regex(KonstantaValidasi.REGEX_ANGKA))
}

fun hitungDiskon(harga: Double, kodeAnggota: String): Double {
    val persenDiskon = when (kodeAnggota) {
        "GOLD"   -> KonstantaDiskon.GOLD_PERSEN
        "SILVER" -> KonstantaDiskon.SILVER_PERSEN
        "BRONZE" -> KonstantaDiskon.BRONZE_PERSEN
        else     -> 0
    }
    return harga * (1 - persenDiskon / 100.0)
}
Aturan praktisnya: jika sebuah angka atau string muncul lebih dari satu kali di codebase, atau maknanya tidak langsung jelas dari konteksnya, buat konstantanya. Satu kali pun, jika nilainya bisa berubah di masa depan atau punya makna bisnis yang spesifik, lebih baik dijadikan konstanta.

Konvensi Penamaan Konstanta #

Kotlin mengikuti konvensi yang berbeda untuk val biasa dan const val:

// val biasa — camelCase (sama seperti variabel)
val waktuMulai = System.currentTimeMillis()
val koneksiDb = Database.buat()

// const val — SCREAMING_SNAKE_CASE
const val URL_API = "https://api.example.com"
const val BATAS_PERCOBAAN = 3
const val TIMEOUT_MS = 30_000L

// Enum — PascalCase untuk nama enum, SCREAMING_SNAKE_CASE untuk entry
//        atau camelCase untuk entry — keduanya umum, pilih satu dan konsisten
enum class StatusPesanan { MENUNGGU, DIPROSES, DIKIRIM, SELESAI }

SCREAMING_SNAKE_CASE untuk const val adalah konvensi yang diturunkan dari Java dan diikuti oleh seluruh ekosistem Kotlin. Ini membuat konstanta mudah dikenali sekilas di antara variabel biasa.


Ringkasan #

  • val untuk runtime constant — nilainya bisa dihitung saat program berjalan, bisa memanggil fungsi, bisa membuat objek. Cocok untuk konfigurasi yang bergantung pada environment atau hardware.
  • const val untuk compile-time constant — nilai harus sudah pasti saat kompilasi, hanya untuk tipe primitif dan String. Compiler menginline nilainya untuk performa lebih baik.
  • const val adalah satu-satunya pilihan untuk annotation — argumen annotation harus compile-time constant; val biasa tidak bisa dipakai di sini.
  • Tiga lokasi deklarasi — top-level untuk konstanta umum, object untuk konstanta terkelompok per domain, companion object untuk konstanta yang terikat erat ke satu kelas.
  • Enum untuk nilai yang saling berkaitan — lebih type-safe daripada serangkaian const val. Compiler memaksa semua kasus ditangani di when, dan tidak ada yang bisa memasukkan nilai di luar enum.
  • Hindari magic number dan magic string — setiap angka atau string dengan makna bisnis harus punya nama. Ini membuat refactoring aman dan kode lebih mudah dibaca.
  • Konvensi penamaanval mengikuti camelCase seperti variabel biasa; const val menggunakan SCREAMING_SNAKE_CASE agar mudah dikenali sekilas.
  • Organisasi dalam proyek besar — kelompokkan konstanta per domain ke dalam file atau object terpisah. Hindari satu file raksasa Constants.kt yang berisi semua konstanta dari seluruh aplikasi.

← Sebelumnya: Variabel   Berikutnya: Tipe Data →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact