enum class
Un enum define un conjunto fijo de constantes. En Kotlin los enums pueden tener propiedades y métodos:
// Enum simple
enum class Direccion { NORTE, SUR, ESTE, OESTE }
// Enum con propiedades
enum class HttpStatus(val codigo: Int, val descripcion: String) {
OK(200, "Éxito"),
CREATED(201, "Creado"),
NOT_FOUND(404, "No encontrado"),
SERVER_ERROR(500, "Error del servidor");
val esExito: Boolean get() = codigo in 200..299
}
val status = HttpStatus.OK
println(status.codigo) // 200
println(status.esExito) // true
println(HttpStatus.NOT_FOUND.descripcion) // No encontrado
// Iterar sobre todos los valores
HttpStatus.values().forEach { println("${it.codigo}: ${it.descripcion}") }
// Desde un String
val estado = HttpStatus.valueOf("OK") // HttpStatus.OK
sealed class — el patrón que usás en todo Android
Una sealed class restringe la jerarquía de herencia — solo las subclases definidas en el mismo archivo pueden existir. Esto le da al compilador información completa sobre todos los casos posibles:
// El UiState que ya conocés del curso Intermedio
sealed class UiState {
object Loading : UiState()
data class Success(val data: T) : UiState()
data class Error(val message: String) : UiState()
}
// Cada subclase puede tener datos propios
sealed class Resultado {
data class Exitoso(val datos: List) : Resultado()
data class Error(val codigo: Int, val mensaje: String) : Resultado()
object SinConexion : Resultado()
object Cargando : Resultado()
}
// when con sealed es exhaustivo — el compilador verifica que cubrís todo
fun manejarResultado(resultado: Resultado): String = when (resultado) {
is Resultado.Exitoso -> "Se encontraron ${resultado.datos.size} productos"
is Resultado.Error -> "Error ${resultado.codigo}: ${resultado.mensaje}"
Resultado.SinConexion -> "Sin conexión a internet"
Resultado.Cargando -> "Cargando..."
// No necesitás else — el compilador sabe que estos son TODOS los casos
}
// Si agregás un nuevo subtipo y olvidás manejarlo en el when:
// ERROR de compilación — "when expression must be exhaustive"
sealed class en AndroidSon la base del patrón UiState, los eventos del ViewModel y los resultados de API. Cada vez que modelás "esto puede ser una de N cosas distintas", una sealed class es la respuesta correcta.
enum vs sealed — cuándo usar cada una
// Usá ENUM cuando:
// - Todos los casos son instancias sin datos propios
// - Necesitás iterar sobre todos los valores (values())
// - Necesitás convertir desde String (valueOf())
enum class Tema { CLARO, OSCURO, SISTEMA }
enum class DiaSemana { LUNES, MARTES, MIERCOLES, JUEVES, VIERNES, SABADO, DOMINGO }
// Usá SEALED CLASS cuando:
// - Los casos tienen datos distintos entre sí
// - Necesitás subtipos con estructura diferente
// - Modelás estados de UI, resultados de operaciones, eventos
sealed class NavEvento {
data class IrADetalle(val id: Int) : NavEvento()
data class MostrarError(val mensaje: String) : NavEvento()
object Atras : NavEvento()
object CerrarSesion : NavEvento()
}
sealed interface — más flexible
Desde Kotlin 1.5, sealed interface permite que los subtipos implementen múltiples interfaces selladas:
sealed interface Accion
sealed interface ConId { val id: Int }
data class Editar(override val id: Int, val nombre: String) : Accion, ConId
data class Eliminar(override val id: Int) : Accion, ConId
object Cancelar : Accion
// Un subtipo puede implementar múltiples sealed interfaces
fun manejar(accion: Accion) = when (accion) {
is Editar -> "Editando ${accion.id}"
is Eliminar -> "Eliminando ${accion.id}"
Cancelar -> "Cancelado"
}
Generics básicos
Los generics permiten escribir código que funciona con cualquier tipo manteniendo type-safety:
// Clase genérica
class Caja(val contenido: T) {
fun obtener(): T = contenido
override fun toString() = "Caja($contenido)"
}
val cajaInt = Caja(42)
val cajaString = Caja("Hola")
val cajaProducto = Caja(Producto(1, "Tablet", 299.0))
println(cajaInt.obtener()) // 42
println(cajaString.obtener()) // Hola
// Función genérica
fun intercambiar(a: T, b: T): Pair = Pair(b, a)
val (x, y) = intercambiar(1, 2) // Pair(2, 1)
val (s, t) = intercambiar("a", "b") // Pair("b", "a")
// Restricción de tipo genérico
fun > mayor(a: T, b: T): T = if (a > b) a else b
mayor(10, 20) // 20
mayor("Ana", "Carlos") // Carlos (orden alfabético)
// mayor(Caja(1), Caja(2)) ERROR — Caja no implementa Comparable
// Múltiples restricciones
fun procesarSerializable(item: T) where T : Serializable, T : Comparable {
// T debe ser Serializable Y Comparable
}
in, out y varianza
// out (covariante) — solo podés LEER el tipo, no escribir
// "Productor de T"
interface Productor {
fun producir(): T
}
// Si Perro extiende Animal, Productor es subtipo de Productor
val productorPerros: Productor = PerroFactory()
val productorAnimales: Productor = productorPerros // OK con out
// in (contravariante) — solo podés ESCRIBIR el tipo, no leer
// "Consumidor de T"
interface Consumidor {
fun consumir(item: T)
}
// Si Perro extiende Animal, Consumidor es subtipo de Consumidor
val consumidorAnimales: Consumidor = AnimalProcessor()
val consumidorPerros: Consumidor = consumidorAnimales // OK con in
// Caso real que ya usás: StateFlow es covariante (out)
// StateFlow se puede asignar a StateFlow
val flowPerros: StateFlow = MutableStateFlow(Perro("Rex"))
val flowAnimales: StateFlow = flowPerros // OK porque StateFlow es out T
reified — acceder al tipo en runtime
Normalmente los tipos genéricos se borran en runtime (type erasure). Con reified en funciones inline, podés acceder al tipo real:
// Sin reified — no podés hacer esto
fun parsear(json: String): T {
return gson.fromJson(json, T::class.java) // ERROR — T no existe en runtime
}
// Con reified + inline — T es accesible en runtime
inline fun parsear(json: String): T {
return gson.fromJson(json, T::class.java) // OK
}
// Uso limpio:
val producto: Producto = parsear(jsonString)
val lista: List = parsear(jsonArray)
// Otro ejemplo común en Android:
inline fun Context.iniciar() {
startActivity(Intent(this, T::class.java))
}
// Uso:
iniciar() // en lugar de startActivity(Intent(this, DetalleActivity::class.java))