Las cinco scope functions

Las scope functions ejecutan un bloque de código en el contexto de un objeto. La diferencia entre ellas es cómo se accede al objeto dentro del bloque (it vs this) y qué retornan (el objeto o el resultado del bloque):

// Resumen rápido:
// ┌──────────┬────────────────┬──────────────────────────┐
// │ Función  │ Objeto dentro  │ Retorna                  │
// ├──────────┼────────────────┼──────────────────────────┤
// │ let      │ it             │ resultado del bloque     │
// │ run      │ this           │ resultado del bloque     │
// │ apply    │ this           │ el objeto mismo          │
// │ also     │ it             │ el objeto mismo          │
// │ with     │ this           │ resultado del bloque     │
// └──────────┴────────────────┴──────────────────────────┘

let — transformar o ejecutar si no es null

// Uso 1: ejecutar un bloque solo si no es null
val nombre: String? = obtenerNombre()
nombre?.let { n ->
    println("El nombre tiene ${n.length} caracteres")
    enviarSaludo(n)
}
// Sin let necesitarías: if (nombre != null) { ... }

// Uso 2: transformar un valor en otro
val longitud = "Kotlin".let { it.length * 2 }  // 12

// Uso 3: limitar el scope de una variable temporal
val resultado = baseDeDatos.ejecutarQuery("SELECT...").let { filas ->
    // filas solo existe dentro de este bloque
    filas.map { it.toProducto() }.filter { it.activo }
}
// filas no existe acá afuera

// Uso 4: encadenar operaciones
val procesado = obtenerTexto()
    ?.let { it.trim() }
    ?.let { if (it.isBlank()) null else it }
    ?.uppercase()
    ?: "VACÍO"

run — configurar y transformar

// Uso 1: ejecutar un bloque en el contexto del objeto (this = el objeto)
val resumen = usuario.run {
    // this = usuario, podés acceder a sus propiedades directamente
    "Nombre: $nombre, Edad: $edad, Admin: $esAdmin"
}

// Uso 2: inicializar y calcular en un bloque
val conexion = run {
    val host = config.getString("host")
    val puerto = config.getInt("puerto")
    Conexion(host, puerto).apply { conectar() }
}

// Uso 3: run con safe call — igual que let pero con this
val longitud = nombre?.run { length }  // this = nombre dentro del bloque

apply — configurar un objeto, retornar el objeto

apply es perfecta para inicializar o configurar un objeto — retorna el objeto mismo, lo que permite el patrón builder:

// El caso de uso más común: configurar un objeto nuevo
val intent = Intent(context, DetalleActivity::class.java).apply {
    putExtra("ID", producto.id)
    putExtra("NOMBRE", producto.nombre)
    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(intent)

// Bundle — clásico en Android
val bundle = Bundle().apply {
    putInt("id", 42)
    putString("nombre", "Carlos")
    putBoolean("admin", true)
}

// OkHttpClient builder
val client = OkHttpClient.Builder().apply {
    connectTimeout(30, TimeUnit.SECONDS)
    readTimeout(30, TimeUnit.SECONDS)
    addInterceptor(loggingInterceptor)
}.build()

// Data class con apply para mutación en tests
val usuario = usuarioBase.copy().apply {
    // (si los campos son var)
    nombre = "Modificado"
}

apply es la más usada en AndroidCada vez que configurás un Intent, Bundle, AlertDialog.Builder, RecyclerView o cualquier objeto que requiere múltiples setters, apply hace el código significativamente más limpio.

also — efecto secundario, retornar el objeto

also es como apply pero accede al objeto como it. Ideal para efectos secundarios que no deben modificar el objeto (logging, validación, debugging):

// Loggear sin interrumpir la cadena
val productos = repository.getProductos()
    .also { Log.d("TAG", "Cargados ${it.size} productos") }
    .filter { it.activo }
    .sortedBy { it.nombre }

// Validar en una cadena
fun crearUsuario(nombre: String, email: String) = Usuario(nombre, email)
    .also { require(it.nombre.isNotBlank()) { "Nombre requerido" } }
    .also { require(it.email.contains("@")) { "Email inválido" } }

// Debugging — insertar un println sin romper la cadena
val resultado = calcularPrecio()
    .also { println("Precio calculado: $it") }
    .let { aplicarDescuento(it) }

with — trabajar con un objeto sin receptor

with no es una extension function — se llama diferente. Útil cuando no necesitás la safe call y querés acceder a múltiples propiedades del objeto:

// with(objeto) { bloque con this = objeto }
val descripcion = with(usuario) {
    """
    Nombre: $nombre
    Email: $email
    Edad: $edad
    Rol: ${if (esAdmin) "Administrador" else "Usuario"}
    """.trimIndent()
}

// Acceder a múltiples propiedades de una vista
with(binding) {
    tvTitulo.text = producto.nombre
    tvPrecio.text = "$${"%.2f".format(producto.precio)}"
    tvStock.text = "${producto.stock} unidades"
    btnComprar.isEnabled = producto.stock > 0
}

Cuándo usar cada una — guía práctica

// ¿Necesitás ejecutar código SOLO si el objeto no es null?
objeto?.let { /* usá let */ }

// ¿Estás CONFIGURANDO un objeto y querés retornarlo?
val obj = Objeto().apply { /* configuración con this */ }

// ¿Querés hacer un EFECTO SECUNDARIO (log, debug) sin cambiar el flujo?
objeto.also { /* log con it */ }

// ¿Querés TRANSFORMAR el objeto en otra cosa?
val resultado = objeto.run { /* transformación con this */ }
val resultado = objeto.let { /* transformación con it */ }
// run cuando preferís this, let cuando preferís it o safe call

// ¿Tenés un objeto NON-NULL y querés operar en él con acceso limpio?
with(objeto) { /* múltiples operaciones con this */ }

// Regla de oro:
// apply  → builders y configuración (retorna el objeto)
// let    → null safety y transformación (retorna resultado)
// also   → logging y debugging (retorna el objeto)
// run    → transformación con this (retorna resultado)
// with   → múltiples accesos a un objeto ya definido