September 28, 2026

Dasar Kotlin.

Dasar Kotlin untuk developer Android: val/var, null safety, data class, sealed class, extension function, lambda, dan dasar coroutines.

Kotlin adalah bahasa utama untuk Android modern: ringkas, null-safe, dan punya coroutines untuk kode asinkron. Catatan ini merangkum fitur yang benar-benar saya pakai saat membangun NotiFly , aplikasi Android yang meneruskan notifikasi bank ke server sendiri. Versi acuan: Kotlin 2.4 (compiler K2).

Semua contoh non-Android bisa dicoba langsung di play.kotlinlang.org atau dengan kotlinc:

brew install kotlin
kotlinc main.kt -include-runtime -d main.jar && java -jar main.jar

1. val, var, dan Tipe

fun main() {
    val appName = "NotiFly"          // read-only (seperti const/final)
    var retryCount = 0               // bisa diubah
    retryCount += 1

    val retentionDays: Int = 30      // tipe eksplisit (opsional, biasanya di-infer)
    val postTime = 1_727_500_000_000L

    // if, when, dan try adalah ekspresi → bisa langsung di-assign
    val label = if (retryCount > 1) "retry" else "first"
    println("$appName: $label, simpan $retentionDays hari, t=${postTime / 1000}")
}
KotlinSetara di JS/PHPCatatan
valconstreferensi tetap, isi objek bisa mutable
varlet / $xhindari bila tidak perlu
"Halo $nama ${a + b}"template literalstring template
listOf, mutableListOfarrayversi read-only vs mutable
mapOf("a" to 1)object/assoc arrayto membuat Pair
setOfSetunik

Biasakan val + koleksi read-only sebagai default; var/mutableListOf hanya saat memang perlu diubah.


2. Null Safety

Tipe biasa tidak boleh null. Tambahkan ? untuk mengizinkannya, lalu compiler memaksa kita menanganinya.

data class Notif(val title: String?, val text: String?, val bigText: String?)

fun body(n: Notif): String {
    // ?. safe call, ?: elvis (nilai cadangan)
    val len = n.title?.length ?: 0
    // pilih teks terpanjang yang tersedia
    return n.bigText ?: n.text ?: "(tanpa isi) judul=$len karakter"
}

fun main() {
    val n = Notif(title = "Transfer masuk", text = null, bigText = null)
    println(body(n))

    n.text?.let { println("Ada teks: $it") }   // blok hanya jalan bila tidak null
    val panjang = n.title!!.length              // !! = "yakin tidak null" → crash bila salah
    println(panjang)
}
OperatorArti
T?tipe yang boleh null
a?.bakses b bila a tidak null, selain itu hasilnya null
a ?: bpakai b bila a null
a!!paksa non-null; lempar NullPointerException bila null
a?.let { }jalankan blok hanya jika tidak null
as?cast aman, null bila gagal

Smart cast: setelah if (x != null), compiler memperlakukan x sebagai non-null di dalam blok (untuk val lokal).


3. Data Class

data class otomatis membuat equals, hashCode, toString, copy, dan destructuring — ideal untuk model data dan state UI.

data class AppConfig(
    val serverUrl: String = "",
    val apiKey: String = "",
    val bankPackages: Set<String> = emptySet(),
    val retentionDays: Int = 30,
) {
    val isServerConfigured: Boolean
        get() = serverUrl.isNotBlank() && apiKey.isNotBlank()
}

fun main() {
    val awal = AppConfig()
    val baru = awal.copy(serverUrl = "https://api.example.com", apiKey = "demo")
    println(baru.isServerConfigured)       // true
    println(awal == AppConfig())           // true (equals berdasarkan isi)
    val (url, _, packages) = baru          // destructuring
    println("$url ${packages.size}")
}

copy() adalah kunci pola immutable state: alih-alih mengubah objek, buat salinan dengan field yang berbeda. Pola yang sama dipakai StateFlow di arsitektur Android modern .


4. Enum & Sealed Class

enum untuk himpunan nilai tetap; sealed untuk himpunan tipe tetap yang masing-masing bisa membawa data berbeda.

enum class SendStatus { PENDING, SENDING, SENT, FAILED }

sealed interface SendResult {
    data object Success : SendResult
    data class Retryable(val message: String) : SendResult
    data class Permanent(val code: Int, val message: String) : SendResult
}

fun classify(httpCode: Int): SendResult = when (httpCode) {
    in 200..299 -> SendResult.Success
    408, 429, in 500..599 -> SendResult.Retryable("HTTP $httpCode, coba lagi")
    else -> SendResult.Permanent(httpCode, "Ditolak server")
}

fun main() {
    for (code in listOf(200, 503, 401)) {
        // when atas sealed type harus exhaustive → tanpa else
        val msg = when (val r = classify(code)) {
            SendResult.Success -> "terkirim"
            is SendResult.Retryable -> "ulang: ${r.message}"
            is SendResult.Permanent -> "gagal ${r.code}: ${r.message}"
        }
        println(msg)
    }
}

Di NotiFly, hasil pengiriman ke server dimodelkan persis seperti ini: worker memutuskan retry atau failure berdasarkan subtipe. Kalau nanti ada subtipe baru, semua when yang lupa menanganinya langsung gagal compile — bug tertangkap sebelum rilis.


5. Extension Function

Menambahkan fungsi ke tipe yang sudah ada tanpa mewarisi atau mengubah source-nya.

fun String.maskSecret(visible: Int = 4): String =
    if (length <= visible) "*".repeat(length)
    else "*".repeat(length - visible) + takeLast(visible)

val Long.asSeconds: Long get() = this / 1000   // extension property

fun main() {
    println("sk_live_abcdef123456".maskSecret())   // ****************3456
    println(1_727_500_000_000L.asSeconds)
}

Extension di-resolve secara statis (berdasarkan tipe yang dideklarasikan) dan tidak bisa mengakses member private. Library Android KTX (core-ktx, lifecycle-ktx) sebagian besar berisi extension seperti ini.


6. Lambda & Higher-Order Function

data class Item(val pkg: String, val status: String, val postTime: Long)

fun main() {
    val items = listOf(
        Item("com.bank.a", "SENT", 3), Item("com.bank.b", "FAILED", 2),
        Item("com.bank.a", "PENDING", 1),
    )

    val failed = items.filter { it.status == "FAILED" }
    val perApp = items.groupingBy { it.pkg }.eachCount()        // {com.bank.a=2, com.bank.b=1}
    val terbaru = items.sortedByDescending { it.postTime }.map(Item::pkg)
    val adaPending = items.any { it.status == "PENDING" }
    println("$failed\n$perApp\n$terbaru\n$adaPending")

    // fungsi yang menerima lambda
    fun retry(times: Int, block: (attempt: Int) -> Boolean): Boolean {
        repeat(times) { if (block(it + 1)) return true }
        return false
    }
    println(retry(3) { attempt -> println("percobaan $attempt"); attempt == 2 })
}

Scope function yang sering muncul:

FungsiObjek diakses sebagaiMengembalikanPakai untuk
letithasil lambdablok untuk nilai non-null
applythisobjek itu sendirikonfigurasi objek (builder)
alsoitobjek itu sendiriefek samping (log)
runthishasil lambdahitung sesuatu dari objek
with(x)thishasil lambdabanyak panggilan ke objek yang sama

7. Coroutines Dasar

Coroutine adalah “thread ringan”: kode asinkron ditulis berurutan seperti kode sinkron. Konsepnya mirip async/await di JavaScript (lihat Event Loop & Async/Await ), tetapi dengan structured concurrency.

// dependensi: org.jetbrains.kotlinx:kotlinx-coroutines-core
import kotlinx.coroutines.*

suspend fun sendToServer(id: Long): Boolean {
    delay(300)                 // suspend, tidak memblokir thread
    return id % 2 == 0L
}

fun main() = runBlocking {
    // launch: fire-and-forget, async: mengembalikan Deferred<T>
    val job = launch { println("log: mulai kirim") }

    val hasil = listOf(1L, 2L, 3L).map { id ->
        async(Dispatchers.IO) { id to sendToServer(id) }
    }.awaitAll()                // ketiganya jalan paralel (~300 ms total)

    job.join()
    println(hasil)             // [(1, false), (2, true), (3, false)]

    val r = withTimeoutOrNull(100) { sendToServer(4) }
    println("dengan timeout: $r")  // null karena lebih dari 100 ms
}
KonsepPenjelasan
suspend funfungsi yang boleh berhenti sementara; hanya bisa dipanggil dari coroutine/suspend lain
CoroutineScopepemilik coroutine; bila scope dibatalkan, semua anaknya ikut batal
launch / asyncmemulai coroutine tanpa / dengan nilai balik
Dispatchers.Main/IO/DefaultUI thread / I/O (disk, jaringan) / komputasi berat
withContext(Dispatchers.IO)pindah thread pool untuk satu blok
Flow<T>aliran nilai asinkron (mis. hasil query Room yang terus ter-update)

Di Android, jangan membuat scope sendiri sembarangan: pakai viewModelScope, lifecycleScope, atau CoroutineWorker, sehingga coroutine otomatis dibatalkan saat pemiliknya selesai. runBlocking hanya untuk main() dan test.

Lanjut ke Jetpack Compose Dasar untuk membangun UI.


Kesalahan Umum

KesalahanAkibatPerbaikan
!! di mana-manaNullPointerException — persis yang ingin dihindari Kotlin?., ?:, let, atau validasi di awal
var + mutableList sebagai defaultState berubah dari mana saja, sulit dilacakval + koleksi read-only + copy()
when dengan else pada sealed typeSubtipe baru tidak ketahuanHilangkan else agar exhaustive dicek compiler
runBlocking di kode AndroidUI freeze / ANRviewModelScope.launch, suspend
GlobalScope.launchCoroutine bocor, tidak pernah dibatalkanScope yang terikat lifecycle
Menangkap Exception di dalam coroutine lalu diabaikanCancellationException tertelan, pembatalan rusakLempar ulang CancellationException, atau tangkap tipe spesifik
== vs === tertukarMembandingkan referensi, bukan isi== untuk isi (memanggil equals)
Blocking I/O di Dispatchers.MainNetworkOnMainThreadException / jankwithContext(Dispatchers.IO)

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.

Comments