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}")
}
| Kotlin | Setara di JS/PHP | Catatan |
|---|---|---|
val | const | referensi tetap, isi objek bisa mutable |
var | let / $x | hindari bila tidak perlu |
"Halo $nama ${a + b}" | template literal | string template |
listOf, mutableListOf | array | versi read-only vs mutable |
mapOf("a" to 1) | object/assoc array | to membuat Pair |
setOf | Set | unik |
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)
}
| Operator | Arti |
|---|---|
T? | tipe yang boleh null |
a?.b | akses b bila a tidak null, selain itu hasilnya null |
a ?: b | pakai 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:
| Fungsi | Objek diakses sebagai | Mengembalikan | Pakai untuk |
|---|---|---|---|
let | it | hasil lambda | blok untuk nilai non-null |
apply | this | objek itu sendiri | konfigurasi objek (builder) |
also | it | objek itu sendiri | efek samping (log) |
run | this | hasil lambda | hitung sesuatu dari objek |
with(x) | this | hasil lambda | banyak 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
}
| Konsep | Penjelasan |
|---|---|
suspend fun | fungsi yang boleh berhenti sementara; hanya bisa dipanggil dari coroutine/suspend lain |
CoroutineScope | pemilik coroutine; bila scope dibatalkan, semua anaknya ikut batal |
launch / async | memulai coroutine tanpa / dengan nilai balik |
Dispatchers.Main/IO/Default | UI 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
| Kesalahan | Akibat | Perbaikan |
|---|---|---|
!! di mana-mana | NullPointerException — persis yang ingin dihindari Kotlin | ?., ?:, let, atau validasi di awal |
var + mutableList sebagai default | State berubah dari mana saja, sulit dilacak | val + koleksi read-only + copy() |
when dengan else pada sealed type | Subtipe baru tidak ketahuan | Hilangkan else agar exhaustive dicek compiler |
runBlocking di kode Android | UI freeze / ANR | viewModelScope.launch, suspend |
GlobalScope.launch | Coroutine bocor, tidak pernah dibatalkan | Scope yang terikat lifecycle |
Menangkap Exception di dalam coroutine lalu diabaikan | CancellationException tertelan, pembatalan rusak | Lempar ulang CancellationException, atau tangkap tipe spesifik |
== vs === tertukar | Membandingkan referensi, bukan isi | == untuk isi (memanggil equals) |
Blocking I/O di Dispatchers.Main | NetworkOnMainThreadException / jank | withContext(Dispatchers.IO) |

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