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