class — constructor primario

En Kotlin el constructor primario va en la misma línea que el nombre de la clase. Las propiedades se pueden declarar directamente en él con val/var:

// Java necesita 20+ líneas para esto. Kotlin: 1.
class Usuario(val nombre: String, val edad: Int, var email: String)

val u = Usuario("Carlos", 30, "[email protected]")
println(u.nombre)   // Carlos
u.email = "[email protected]"  // OK — es var
u.nombre = "Ana"    // ERROR — es val

// Con lógica de inicialización — bloque init
class Usuario(val nombre: String, val edad: Int) {
    val nombreMayus: String

    init {
        require(edad >= 0) { "La edad no puede ser negativa" }
        nombreMayus = nombre.uppercase()
    }
}

// Constructor secundario — menos común, preferí default args
class Conexion(val host: String, val puerto: Int) {
    constructor(host: String) : this(host, 8080)
}

data class — el más usado en Android

Una data class es una clase cuyo propósito es solo contener datos. Kotlin genera automáticamente equals(), hashCode(), toString() y copy():

data class Producto(
    val id: Int,
    val nombre: String,
    val precio: Double,
    val stock: Int = 0
)

val p1 = Producto(1, "Tablet", 299.99)
val p2 = Producto(1, "Tablet", 299.99)

// equals() compara por contenido, no por referencia
println(p1 == p2)   // true (en Java sería false con ==)

// toString() legible automáticamente
println(p1)  // Producto(id=1, nombre=Tablet, precio=299.99, stock=0)

// copy() — crear una copia modificando solo algunos campos
val p3 = p1.copy(precio = 249.99)
println(p3)  // Producto(id=1, nombre=Tablet, precio=249.99, stock=0)
println(p1)  // El original no cambió

// Destructuring — extraer propiedades por posición
val (id, nombre, precio) = p1
println("$nombre cuesta $$precio")  // Tablet cuesta $299.99

copy() en MVVMcopy() es fundamental cuando trabajás con StateFlow inmutable. En lugar de mutar el estado directamente, creás una copia con el campo modificado: _uiState.update { it.copy(isLoading = false) }. El estado anterior queda intacto.

Herencia

En Kotlin las clases son final por default — no se pueden heredar a menos que las marques con open:

// open — permite heredar
open class Animal(val nombre: String) {
    open fun sonido(): String = "..."  // open — permite sobreescribir

    fun describir() = "$nombre hace ${sonido()}"  // final — no se puede sobreescribir
}

class Perro(nombre: String) : Animal(nombre) {
    override fun sonido() = "Guau"
}

class Gato(nombre: String, val raza: String) : Animal(nombre) {
    override fun sonido() = "Miau"
}

val perro = Perro("Rex")
println(perro.describir())  // Rex hace Guau

// abstract — igual que en Java
abstract class Figura {
    abstract fun area(): Double
    fun descripcion() = "Área: ${area()}"
}

class Circulo(val radio: Double) : Figura() {
    override fun area() = Math.PI * radio * radio
}

Interfaces

interface Serializable {
    fun serializar(): String
    fun log() = println("Serializando...")  // implementación default
}

interface Validable {
    fun esValido(): Boolean
}

// Kotlin permite implementar múltiples interfaces (pero solo una clase)
data class Formulario(val nombre: String, val email: String) : Serializable, Validable {
    override fun serializar() = """{"nombre":"$nombre","email":"$email"}"""
    override fun esValido() = nombre.isNotBlank() && email.contains("@")
}

object — singleton

object declara una clase y crea su única instancia al mismo tiempo. Es el singleton idiomático de Kotlin, thread-safe por definición:

// Singleton — una sola instancia en toda la app
object BaseDeDatos {
    private var conexion: Conexion? = null

    fun conectar(url: String) {
        conexion = Conexion(url)
    }

    fun ejecutar(query: String): List {
        return conexion?.ejecutar(query) ?: emptyList()
    }
}

// Se accede directamente por nombre — no hay instancia que crear
BaseDeDatos.conectar("jdbc:sqlite:app.db")
val filas = BaseDeDatos.ejecutar("SELECT * FROM usuarios")

// Object expression — equivalente a clase anónima en Java
val comparador = object : Comparator {
    override fun compare(a: String, b: String) = a.length - b.length
}

companion object

El companion object vive dentro de una clase y contiene sus miembros estáticos — constantes, factory methods y utilidades relacionadas:

class Usuario private constructor(val nombre: String, val rol: Rol) {

    companion object {
        // Constantes
        const val ROL_DEFAULT = "usuario"
        const val MAX_NOMBRE = 50

        // Factory methods — el patrón más común
        fun admin(nombre: String) = Usuario(nombre, Rol.ADMIN)
        fun regular(nombre: String) = Usuario(nombre, Rol.USUARIO)
        fun invitado() = Usuario("Invitado", Rol.INVITADO)
    }
}

// Uso:
val admin = Usuario.admin("Carlos")
val invitado = Usuario.invitado()
println(Usuario.ROL_DEFAULT)   // "usuario"

// En Android — muy usado en Fragments:
class DetalleFragment : Fragment() {
    companion object {
        private const val ARG_ID = "producto_id"

        fun newInstance(id: Int) = DetalleFragment().apply {
            arguments = Bundle().apply { putInt(ARG_ID, id) }
        }
    }
}

Propiedades computadas y backing fields

class Rectangulo(val ancho: Double, val alto: Double) {

    // Propiedad computada — se recalcula en cada acceso
    val area: Double get() = ancho * alto
    val perimetro: Double get() = 2 * (ancho + alto)
    val esCuadrado: Boolean get() = ancho == alto

    // Propiedad con lógica en el setter
    var nombre: String = ""
        set(value) {
            field = value.trim()  // field referencia el backing field real
        }
        get() = field.uppercase()
}

val r = Rectangulo(4.0, 3.0)
println(r.area)        // 12.0
println(r.esCuadrado)  // false