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))