Scope Functions

Scope Functions #

Ada lima fungsi di Kotlin Standard Library yang terlihat mirip tapi memiliki peran yang berbeda: let, run, with, apply, dan also. Kelimanya disebut scope functions karena mereka membuka blok kode baru dengan konteks objek tertentu — kamu bisa mengakses objek itu di dalam blok tanpa menyebut namanya berulang kali. Scope functions adalah salah satu fitur yang paling sering disalahgunakan sekaligus paling berguna dalam Kotlin. Digunakan dengan tepat, mereka menghilangkan variabel sementara, memperjelas intent kode, dan membuat chain operasi terasa alami. Digunakan sembarangan, mereka menciptakan kode yang sulit dibaca dan di-debug. Artikel ini membahas kapan dan bagaimana menggunakan masing-masing dengan tepat.

Peta Scope Functions #

Sebelum masuk ke masing-masing, penting untuk memahami dua dimensi yang membedakan kelimanya: bagaimana objek diakses di dalam blok, dan apa yang dikembalikan fungsi.

flowchart TD
    A["Scope Function"] --> B["Referensi objek\ndi dalam blok?"]
    B --> C["it — sebagai parameter lambda\nlet, also"]
    B --> D["this — sebagai receiver\nrun, with, apply"]
    A --> E["Nilai yang dikembalikan?"]
    E --> F["Hasil lambda\nlet, run, with"]
    E --> G["Objek itu sendiri\napply, also"]
FungsiReferensi objekNilai kembaliDipanggil pada
letitHasil lambdaObjek (extension)
runthisHasil lambdaObjek (extension)
withthisHasil lambdaObjek (bukan extension)
applythisObjek itu sendiriObjek (extension)
alsoitObjek itu sendiriObjek (extension)

Dua pertanyaan yang selalu bisa membantumu memilih:

  1. Apakah kamu butuh hasilnya, atau butuh objeknya?
  2. Apakah kamu perlu merujuk objek lain di dalam blok?

let — Transformasi dengan Null Safety #

let memanggil blok dengan objek sebagai it dan mengembalikan hasil lambda. Ini menjadikannya pilihan utama untuk dua skenario: transformasi nilai dan eksekusi kondisional pada objek nullable.

Null Safety dengan let #

data class User(val nama: String, val email: String?)

val user: User? = dapatkanUser()

// ANTI-PATTERN: pengecekan null manual yang verbose
if (user != null) {
    val email = user.email
    if (email != null) {
        kirimEmail(email)
    }
}

// BENAR: let berantai dengan safe call
user?.let { u ->
    u.email?.let { email ->
        kirimEmail(email)
    }
}

// Atau lebih ringkas jika hanya satu level
user?.email?.let { kirimEmail(it) }

?.let adalah pola idiomatik Kotlin untuk “lakukan sesuatu jika tidak null”. Blok di dalam let hanya dieksekusi jika objek tidak null — dan di dalam blok, it sudah dijamin non-null sehingga tidak perlu ?. lagi.

Transformasi Nilai #

// let untuk transformasi: ubah satu tipe ke tipe lain
val panjangNama: Int? = user?.nama?.let { nama ->
    nama.trim().length
}

// Berguna untuk mengubah hasil satu operasi sebelum digunakan
val hasil = ambilDataMentah()
    .let { raw -> parseJson(raw) }
    .let { parsed -> validasi(parsed) }
    .let { valid -> simpanKeDisk(valid) }

// Membatasi scope variabel sementara
val pesanFormatted = buildString {
    val timestamp = System.currentTimeMillis()   // timestamp hanya dipakai di sini
    val prefix = "[${formatWaktu(timestamp)}]"
    append("$prefix Proses selesai")
}.let { pesan ->
    if (pesan.length > 100) pesan.substring(0, 97) + "..." else pesan
}

Mengganti Variabel Sementara #

// ANTI-PATTERN: variabel sementara yang hanya dipakai sekali
val input = editText.text.toString()
val inputBersih = input.trim()
val inputValid = if (inputBersih.isEmpty()) null else inputBersih
proses(inputValid)

// BENAR: let menghilangkan variabel sementara
editText.text.toString()
    .trim()
    .let { if (it.isEmpty()) null else it }
    ?.let { proses(it) }
Gunakan nama parameter eksplisit (u, email, item) daripada it saat blok let lebih dari satu baris atau saat ada blok let bersarang — ini mencegah kebingungan tentang it yang mana yang sedang dirujuk.

apply — Konfigurasi Objek #

apply memanggil blok dengan objek sebagai this dan mengembalikan objek itu sendiri. Ini menjadikannya pilihan sempurna untuk konfigurasi objek setelah pembuatan — kamu set properti, panggil method, lalu objek yang sudah dikonfigurasi dikembalikan langsung.

// ANTI-PATTERN: konfigurasi manual dengan variabel sementara
val intent = Intent(context, DetailActivity::class.java)
intent.putExtra("id", produkId)
intent.putExtra("nama", produkNama)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)

// BENAR: apply untuk konfigurasi bersih
val intent = Intent(context, DetailActivity::class.java).apply {
    putExtra("id", produkId)
    putExtra("nama", produkNama)
    flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)

apply sangat umum saat bekerja dengan builder pattern atau objek yang perlu banyak properti diset sebelum digunakan.

// Konfigurasi OkHttpClient
val client = OkHttpClient.Builder().apply {
    connectTimeout(30, TimeUnit.SECONDS)
    readTimeout(30, TimeUnit.SECONDS)
    addInterceptor(loggingInterceptor)
    addInterceptor(authInterceptor)
    if (BuildConfig.DEBUG) {
        addNetworkInterceptor(chuckerInterceptor)
    }
}.build()

// Konfigurasi objek data
data class Konfigurasi(
    var host: String = "localhost",
    var port: Int = 8080,
    var timeout: Int = 30,
    var maxRetries: Int = 3,
    var enableSsl: Boolean = false
)

val config = Konfigurasi().apply {
    host = "api.example.com"
    port = 443
    timeout = 60
    enableSsl = true
}

// Inisialisasi View di Android
val textView = TextView(context).apply {
    text = "Halo, Dunia!"
    textSize = 16f
    setTextColor(Color.BLACK)
    setPadding(16, 8, 16, 8)
    gravity = Gravity.CENTER
}

Karena apply mengembalikan objek itu sendiri, ia bisa langsung dirantai dengan operasi berikutnya:

val pesanan = Pesanan()
    .apply { tambahItem(laptop) }
    .apply { tambahItem(mouse) }
    .apply { setAlamat(alamatPengiriman) }
    .apply { terapkanDiskon(kodePromo) }

also — Efek Samping Tanpa Mengubah Alur #

also memanggil blok dengan objek sebagai it dan mengembalikan objek itu sendiri — sama seperti apply dalam hal nilai kembali, tapi berbeda dalam cara merujuk objek. Karena menggunakan it, also tidak mengaburkan this dari scope luar, menjadikannya ideal untuk efek samping seperti logging, validasi, atau debugging.

// Logging di tengah chain tanpa memutus alur
val hasil = repository.ambilData()
    .also { data -> log.debug("Data diterima: ${data.size} item") }
    .filter { it.aktif }
    .also { filtered -> log.debug("Setelah filter: ${filtered.size} item") }
    .sortedBy { it.nama }

// Validasi sambil lalu
fun simpanUser(user: User): User {
    return user
        .also { require(it.nama.isNotBlank()) { "Nama tidak boleh kosong" } }
        .also { require(it.email.contains("@")) { "Email tidak valid" } }
        .also { userRepository.save(it) }
}

// Debugging: lihat nilai di tengah chain tanpa mengubahnya
val total = daftarHarga
    .filter { it > 0 }
    .also { println("Harga valid: $it") }   // cetak untuk debug
    .sum()
    .also { println("Total: $it") }

Perbedaan apply vs also #

class Laporan {
    var judul = ""
    var konten = ""
    fun generate() = "$judul\n$konten"
}

// apply: this = objek, cocok untuk set properti
val laporan = Laporan().apply {
    judul = "Laporan Bulanan"    // this.judul = ...
    konten = "Isi laporan..."    // this.konten = ...
}

// also: it = objek, cocok untuk efek samping
val laporanDenganLog = Laporan().apply {
    judul = "Laporan Bulanan"
    konten = "Isi laporan..."
}.also {
    println("Laporan dibuat: ${it.judul}")   // it merujuk ke laporan
    auditLog.catat("Laporan baru: ${it.judul}")
}

run — Blok Kode dengan Hasil #

run hadir dalam dua bentuk: sebagai extension function (dipanggil pada objek) dan sebagai fungsi biasa (tanpa receiver). Keduanya mengeksekusi blok dan mengembalikan hasil lambda.

run sebagai Extension Function #

// run: this = objek, mengembalikan hasil lambda
// Cocok saat kamu butuh hasil komputasi dari objek

data class Koneksi(val host: String, val port: Int, val ssl: Boolean)

val urlKoneksi = Koneksi("api.example.com", 443, true).run {
    val protokol = if (ssl) "https" else "http"
    "$protokol://$host:$port"   // nilai ini yang dikembalikan
}
// "https://api.example.com:443"

// Menggantikan variabel sementara untuk komputasi kompleks
val pesanStatus = koneksi.run {
    val status = if (ssl) "aman" else "tidak aman"
    val info = "$host:$port ($status)"
    if (aktif) "Terhubung ke $info" else "Terputus dari $info"
}

run sebagai Fungsi Biasa #

run { } tanpa receiver berguna untuk mengelompokkan blok kode yang menghasilkan nilai, atau untuk memberi scope pada variabel sementara.

// ANTI-PATTERN: variabel sementara yang mencemari scope luar
val a = hitungA()
val b = hitungB(a)
val c = hitungC(a, b)
val hasil = a + b + c
// a, b, c masih accessible di luar, padahal hanya dibutuhkan untuk hasil

// BENAR: run membatasi scope variabel antara
val hasil = run {
    val a = hitungA()
    val b = hitungB(a)
    val c = hitungC(a, b)
    a + b + c  // ini yang dikembalikan ke 'hasil'
}
// a, b, c tidak accessible di sini

// Inisialisasi kompleks
val koneksiDb = run {
    val driver = loadDriver(config.driverClass)
    val props = Properties().apply {
        setProperty("user", config.username)
        setProperty("password", config.password)
        setProperty("ssl", config.ssl.toString())
    }
    driver.connect(config.url, props)
}

with — Operasi pada Objek yang Sudah Ada #

with adalah satu-satunya scope function yang bukan extension function — objek dimasukkan sebagai argumen, bukan dipanggil dengan titik. with mengeksekusi blok dengan objek sebagai this dan mengembalikan hasil lambda.

// Sintaks: with(objek) { ... }

data class Laporan(val judul: String, val data: List<String>, val penulis: String)

val laporan = Laporan("Laporan Q1", listOf("Item A", "Item B", "Item C"), "Andi")

// with untuk operasi banyak pada objek yang sudah ada
val output = with(laporan) {
    buildString {
        appendLine("=== $judul ===")
        appendLine("Penulis: $penulis")
        appendLine("Isi:")
        data.forEachIndexed { i, item ->
            appendLine("  ${i + 1}. $item")
        }
    }
}

// with untuk akses banyak property tanpa pengulangan nama objek
with(konfigurasi) {
    println("Host: $host")
    println("Port: $port")
    println("SSL: $enableSsl")
    println("Timeout: ${timeout}s")
}

with vs run #

Keduanya menggunakan this dan mengembalikan hasil lambda. Perbedaan utama adalah sintaks dan nuansa penggunaan:

val objek = DapatkanObjek()

// run: dipanggil pada objek (extension), lebih natural untuk chain
val hasil1 = objek.run {
    // this = objek
    lakukan()
}

// with: objek sebagai argumen, lebih ekspresif untuk "lakukan banyak hal pada X"
val hasil2 = with(objek) {
    // this = objek
    lakukan()
}

// with cocok saat nama variabel objeknya panjang atau tidak perlu di-chain
with(binding.recyclerViewProduk) {
    layoutManager = LinearLayoutManager(context)
    adapter = produkAdapter
    addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
    setHasFixedSize(true)
}

Memilih Scope Function yang Tepat #

Ini adalah decision tree yang bisa digunakan setiap kali kamu bingung memilih:

flowchart TD
    A{Apa yang ingin\nkamu lakukan?} --> B["Konfigurasi objek\n(set properti, panggil method)"]
    A --> C["Transformasi atau\nkomputasi dari objek"]
    A --> D["Efek samping\n(logging, validasi)"]
    A --> E["Null safety /\nexekusi jika tidak null"]
    A --> F["Operasi banyak\npada objek yang ada"]

    B --> G["apply\n(this, return objek)"]
    C --> H{Butuh kembalikan\nobjeknya?}
    H -- Ya --> I["Tidak ada yang cocok\n→ gunakan run + juga simpan referensi"]
    H -- Tidak --> J["run atau with\n(this, return lambda result)"]
    D --> K["also\n(it, return objek)"]
    E --> L["let dengan ?.\n(it, return lambda result)"]
    F --> M["with\n(this, return lambda result)"]

Ringkasan praktis yang mudah diingat:

Konfigurasi objek baru?           → apply   (this, return objek)
Efek samping / logging?           → also    (it,   return objek)
Transformasi / komputasi?         → let     (it,   return hasil)
Komputasi dengan akses penuh?     → run     (this, return hasil)
Banyak operasi pada objek ada?    → with    (this, return hasil)
Null safety?                      → ?.let   (it,   return hasil)

Anti-Pattern yang Harus Dihindari #

1. Scope Function Bersarang Terlalu Dalam #

// ANTI-PATTERN: tiga level bersarang, sulit dibaca
val hasil = objekA.let { a ->
    objekB.apply {
        nilai = a.run {
            hitungNilai()
        }
    }
}

// BENAR: pecah jadi variabel atau fungsi terpisah
val nilaiA = objekA.hitungNilai()
val hasil = objekB.apply {
    nilai = nilaiA
}

2. Pakai Scope Function Hanya untuk Terlihat Keren #

// ANTI-PATTERN: apply tidak menambah nilai di sini
val angka = 42.apply { println(this) }

// BENAR: cukup langsung
val angka = 42
println(angka)

// ANTI-PATTERN: let tidak diperlukan
val panjang = nama.let { it.length }

// BENAR: langsung akses
val panjang = nama.length

3. Mengaburkan Return Value #

// ANTI-PATTERN: tidak jelas apa yang dikembalikan apply
fun buatUser(): User {
    return User("Andi").apply {
        email = "[email protected]"
        // apply mengembalikan User — ini memang benar,
        // tapi developer baru mungkin mengira mengembalikan Unit
    }
}

// BENAR: eksplisit
fun buatUser(): User {
    val user = User("Andi").apply {
        email = "[email protected]"
    }
    return user
    // atau: return User("Andi").also { it.email = "[email protected]" }
}

4. Menggunakan it saat Konteks Tidak Jelas #

// ANTI-PATTERN: it dalam blok panjang — it merujuk ke apa?
daftarProduk.firstOrNull { it.aktif }?.let {
    tampilkan(it.nama)
    hitung(it.harga)
    if (it.stok > 0) {
        tambahKeKeranjang(it)
    }
}

// BENAR: beri nama eksplisit
daftarProduk.firstOrNull { it.aktif }?.let { produk ->
    tampilkan(produk.nama)
    hitung(produk.harga)
    if (produk.stok > 0) {
        tambahKeKeranjang(produk)
    }
}

Pola Idiomatik dalam Kode Produksi #

Beberapa kombinasi scope function yang sering muncul dalam kode Kotlin nyata.

apply + also untuk Konfigurasi dan Logging #

val httpClient = OkHttpClient.Builder()
    .apply {
        connectTimeout(30, TimeUnit.SECONDS)
        readTimeout(30, TimeUnit.SECONDS)
        addInterceptor(authInterceptor)
    }
    .build()
    .also {
        log.info("HTTP client dibuat dengan timeout 30s")
    }

let untuk Pipeline Transformasi #

// Pipeline: ambil → bersihkan → validasi → simpan
fun prosesInput(raw: String?): Result<String> {
    return raw
        ?.trim()
        ?.takeIf { it.isNotEmpty() }
        ?.let { input ->
            if (input.length > 255) input.substring(0, 255) else input
        }
        ?.let { input -> Result.success(input) }
        ?: Result.failure(IllegalArgumentException("Input tidak boleh kosong"))
}

run untuk Inisialisasi Kompleks #

// Inisialisasi yang butuh banyak langkah
val repositori: UserRepository = run {
    val dataSource = HikariDataSource().apply {
        jdbcUrl = config.dbUrl
        username = config.dbUser
        password = config.dbPassword
        maximumPoolSize = 10
    }
    val mapper = ObjectMapper().apply {
        registerModule(KotlinModule.Builder().build())
        configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    }
    UserRepositoryImpl(dataSource, mapper)
}

with untuk Konfigurasi View (Android) #

// Pola umum di Android untuk konfigurasi RecyclerView
with(binding) {
    recyclerView.apply {
        layoutManager = LinearLayoutManager(requireContext())
        adapter = itemAdapter
        addItemDecoration(dividerDecoration)
    }
    searchInput.addTextChangedListener { text ->
        viewModel.cariItem(text.toString())
    }
    fabTambah.setOnClickListener {
        navigasiKeTambahItem()
    }
}

also untuk Validasi Berurutan #

fun validasiDanSimpanPesanan(pesanan: Pesanan): Pesanan {
    return pesanan
        .also { require(it.items.isNotEmpty()) { "Pesanan harus punya minimal 1 item" } }
        .also { require(it.totalHarga > 0) { "Total harga harus lebih dari 0" } }
        .also { require(it.alamatPengiriman.isNotBlank()) { "Alamat pengiriman wajib diisi" } }
        .also { pesananRepository.simpan(it) }
        .also { notifikasiService.kirim(it.userId, "Pesanan #${it.id} berhasil dibuat") }
}

Kapan Tidak Menggunakan Scope Function #

Scope function bukan solusi untuk semua situasi. Ada kondisi di mana kode lebih jelas tanpa mereka.

Tetap gunakan scope function jika:
  ✓ Butuh null safety dengan ?.let
  ✓ Konfigurasi objek dengan banyak properti (apply)
  ✓ Logging/debugging di tengah chain (also)
  ✓ Membatasi scope variabel sementara (run)
  ✓ Banyak operasi pada satu objek (with)

Hindari scope function jika:
  ✗ Blok hanya satu baris yang bisa ditulis langsung
  ✗ Menghasilkan nesting lebih dari dua level
  ✗ Tujuannya hanya agar kode terlihat "lebih Kotlin"
  ✗ Tim tidak familiar dan kode jadi lebih sulit dibaca
  ✗ Return value tidak jelas atau membingungkan

Ringkasan #

  • let — referensi objek sebagai it, mengembalikan hasil lambda. Paling cocok untuk null safety (?.let) dan transformasi nilai. Gunakan nama eksplisit (nama, item) daripada it jika blok lebih dari satu baris.
  • apply — referensi objek sebagai this, mengembalikan objek itu sendiri. Pilihan utama untuk konfigurasi objek: set properti, panggil method pengaturan, lalu objek siap digunakan.
  • also — referensi objek sebagai it, mengembalikan objek itu sendiri. Sempurna untuk efek samping seperti logging, validasi, atau debugging tanpa mengaburkan this dari scope luar.
  • run — referensi objek sebagai this, mengembalikan hasil lambda. Cocok untuk komputasi yang butuh akses penuh ke objek, atau sebagai fungsi biasa run { } untuk membatasi scope variabel sementara.
  • with — bukan extension function, objek sebagai argumen, this di dalam blok. Paling natural untuk banyak operasi pada objek yang sudah ada, terutama saat nama objeknya panjang.
  • Dua pertanyaan pemandu: (1) butuh hasilnya atau objeknya? → let/run/with vs apply/also. (2) butuh merujuk objek lain di dalam blok? → pakai it (let, also) bukan this (run, apply, with).
  • Anti-pattern utama: nesting terlalu dalam, memakai it tanpa nama eksplisit di blok panjang, dan menggunakan scope function hanya untuk terlihat idiomatik tanpa manfaat nyata.
  • Scope function terbaik adalah yang membuat kode lebih mudah dibaca, bukan lebih pendek. Jika kode lebih jelas tanpa scope function, jangan pakai.

← Sebelumnya: Collections   Berikutnya: Ranges & Progressions →

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