Fundamentos de Kotlin aplicados a Android

Capítulo 2

Tiempo estimado de lectura: 8 minutos

+ Ejercicio

Tipos en Kotlin: lo esencial para Android

En Android, Kotlin se usa para modelar datos (por ejemplo, lo que llega de una API), transformar listas para mostrarlas en pantalla y escribir lógica segura (sin errores por nulos). Para eso necesitas dominar el sistema de tipos.

Tipos básicos y conversión

Kotlin tiene tipos como Int, Long, Double, Boolean y String. A diferencia de otros lenguajes, Kotlin no hace conversiones numéricas implícitas: si necesitas convertir, lo haces explícitamente.

val edad: Int = 28
val puntos: Long = edad.toLong()
val precioTexto = "19.99"
val precio: Double = precioTexto.toDouble()

Inferencia de tipos y legibilidad

Kotlin suele inferir el tipo, pero en Android conviene declarar tipos cuando ayuda a documentar el modelo o evitar ambigüedades.

val nombre = "Ana"          // String inferido
val stock: Int = 10          // explícito por claridad

Inmutabilidad: val por defecto

Una buena práctica clave es preferir val (inmutable) y usar var solo cuando sea necesario. Esto reduce errores al actualizar estado en pantallas y facilita razonar sobre el flujo de datos.

val impuesto = 0.21
var total = 0.0
total += 10.0

Null safety: manejo seguro de nulos (imprescindible en Android)

En Android es común recibir valores nulos (campos opcionales de API, extras de Intent, argumentos de navegación). Kotlin separa tipos no nulos (String) de tipos anulables (String?).

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

Operadores principales

  • ?. llamada segura: ejecuta solo si no es nulo.
  • ?: Elvis: valor por defecto si es nulo.
  • !! aserción no nula: evita usarlo salvo casos muy controlados.
val nickname: String? = null
val longitud = nickname?.length              // Int?
val longitudSegura = nickname?.length ?: 0   // Int

Guía práctica: normalizar datos opcionales

Supón que recibes un nombre opcional y quieres mostrarlo en UI sin romper la app.

  1. Define el tipo anulable.
  2. Normaliza con trim() si existe.
  3. Aplica un valor por defecto con ?:.
fun nombreParaMostrar(nombre: String?): String {
    val limpio = nombre?.trim()
    return if (limpio.isNullOrEmpty()) "Invitado" else limpio
}

Funciones: reutilización y claridad

En Android, las funciones te ayudan a separar lógica de transformación (por ejemplo, preparar listas para un RecyclerView) de la capa de UI. Prioriza funciones pequeñas, puras (sin efectos secundarios) y con nombres descriptivos.

Parámetros con valores por defecto y nombrados

fun precioConImpuesto(precio: Double, iva: Double = 0.21): Double {
    return precio * (1 + iva)
}
val total = precioConImpuesto(precio = 100.0)

Funciones de extensión (muy útiles)

Permiten “añadir” funciones a tipos existentes sin herencia. Útiles para formateo y validaciones.

fun String?.orDash(): String = if (this.isNullOrBlank()) "-" else this

val ciudad: String? = null
val texto = ciudad.orDash()  // "-"

Ejercicio corto 1: transformar una lista de precios

Objetivo: dado un listado de precios (algunos nulos), obtener una lista final con impuesto aplicado y redondeo a 2 decimales, ignorando nulos.

fun Double.round2(): Double = kotlin.math.round(this * 100) / 100

fun preciosFinales(precios: List, iva: Double = 0.21): List {
    return precios
        .filterNotNull()
        .map { (it * (1 + iva)).round2() }
}

val entrada = listOf(10.0, null, 25.5)
val salida = preciosFinales(entrada) // [12.1, 30.86]

Clases, propiedades y encapsulación

Las clases modelan entidades que luego se reutilizan en pantallas (por ejemplo, un Usuario para un perfil o un Producto para un catálogo). En Kotlin, las propiedades se declaran en el constructor primario, y puedes controlar mutabilidad con val/var.

Modelo base: Usuario

class Usuario(
    val id: String,
    val nombre: String,
    val email: String?
) {
    fun iniciales(): String {
        val partes = nombre.trim().split(" ").filter { it.isNotBlank() }
        val first = partes.getOrNull(0)?.firstOrNull()?.uppercaseChar()
        val second = partes.getOrNull(1)?.firstOrNull()?.uppercaseChar()
        return listOfNotNull(first, second).joinToString("")
    }
}

Buenas prácticas: usa val para que el modelo sea inmutable; si necesitas “actualizar” algo, crea una nueva instancia (especialmente útil con estados de UI).

Data classes: modelos ideales para UI y estado

Las data class generan automáticamente equals, hashCode, toString y copy. Son perfectas para representar estado de pantalla y elementos de listas.

Modelo: Producto

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

Guía práctica: actualizar inmutablemente con copy

Si un producto recibe un descuento, no mutas el objeto: creas uno nuevo.

fun aplicarDescuento(p: Producto, descuento: Double): Producto {
    return p.copy(descuento = descuento)
}

Ejercicio corto 2: modelar y derivar datos para UI

Objetivo: crear una función que convierta una lista de Producto en textos listos para mostrar (por ejemplo, en una lista), manejando descuento opcional.

fun Producto.precioFinal(): Double {
    val d = descuento ?: 0.0
    return (precio * (1 - d)).let { kotlin.math.round(it * 100) / 100 }
}

fun productosParaLista(productos: List): List<String> {
    return productos.map { p ->
        val stockTexto = if (p.stock > 0) "Stock: ${p.stock}" else "Sin stock"
        "${p.nombre} - ${p.precioFinal()} (${stockTexto})"
    }
}

Colecciones: listas y transformaciones (base de la UI)

En Android, gran parte del trabajo es transformar colecciones: filtrar, ordenar, agrupar y mapear para mostrar en pantalla. Prioriza colecciones inmutables (List, Map) y evita mutarlas dentro de funciones.

Operaciones más usadas

  • map: transforma cada elemento.
  • filter: conserva los que cumplen una condición.
  • sortedBy/sortedByDescending: ordena.
  • groupBy: agrupa por clave.
  • associateBy: crea un mapa por id.
  • distinctBy: elimina duplicados por clave.
val productos = listOf(
    Producto("p1", "Teclado", 25.0, 10),
    Producto("p2", "Mouse", 15.0, 0, descuento = 0.10),
    Producto("p3", "Monitor", 199.99, 3)
)

val disponibles = productos.filter { it.stock > 0 }
val ordenados = disponibles.sortedBy { it.precioFinal() }
val porId = productos.associateBy { it.id }

Ejercicio corto 3: pipeline de transformación para catálogo

Objetivo: a partir de una lista de productos, obtener los nombres en mayúsculas de los 2 más baratos disponibles (stock > 0), considerando descuento.

fun top2BaratosDisponibles(productos: List<Producto>): List<String> {
    return productos
        .asSequence()
        .filter { it.stock > 0 }
        .sortedBy { it.precioFinal() }
        .take(2)
        .map { it.nombre.uppercase() }
        .toList()
}

asSequence() es útil cuando encadenas muchas operaciones y quieres evitar listas intermedias.

Lambdas: funciones como valores (eventos y callbacks)

Las lambdas son funciones anónimas. En Android aparecen en listeners, callbacks y transformaciones de colecciones.

val duplicar: (Int) -> Int = { it * 2 }
val resultado = duplicar(21) // 42

Lambdas con múltiples parámetros

val formatear: (String, Double) -> String = { nombre, precio ->
    "$nombre: $precio"
}

Ejercicio corto 4: callback para seleccionar un producto

Objetivo: simular una acción de selección (como al tocar un item en una lista) usando una lambda.

fun seleccionarProducto(productos: List<Producto>, id: String, onSeleccion: (Producto) -> Unit) {
    val encontrado = productos.firstOrNull { it.id == id } ?: return
    onSeleccion(encontrado)
}

seleccionarProducto(productos, "p3") { p ->
    val texto = "Seleccionado: ${p.nombre} (${p.precioFinal()})"
    // En una pantalla Android, este texto podría ir a un TextView o estado de UI
}

Scope functions: let, run, apply, also, with

Las scope functions ayudan a escribir código más expresivo al operar sobre un objeto dentro de un bloque. En Android se usan mucho para: encadenar transformaciones, manejar nulos, configurar objetos y evitar variables temporales.

FunciónReceiverRetornaUso típico
letitresultado del bloquetransformar y null-safety
runthisresultado del bloquecomputar con un objeto
applythisel objetoconfigurar (builder-like)
alsoitel objetoside-effects (logs, métricas)
withthisresultado del bloqueoperar sobre un objeto externo

let para nulos: patrón recomendado

val email: String? = "ana@correo.com"
val dominio: String? = email?.let { it.substringAfter("@") }

apply para configurar sin perder el objeto

Útil al construir modelos o configurar objetos (sin entrar en detalles de UI).

val p = Producto(
    id = "p10",
    nombre = "Auriculares",
    precio = 49.99,
    stock = 5
).copy().also {
    // side-effect controlado (por ejemplo, registrar evento)
}

run para calcular un valor a partir de un objeto

fun etiquetaStock(p: Producto): String = p.run {
    if (stock > 0) "Disponible" else "Agotado"
}

Ejercicio corto 5: pipeline seguro con let y takeIf

Objetivo: dado un Usuario con email opcional, obtener un email “válido para mostrar” solo si contiene @; si no, devolver null (para que la UI decida ocultarlo).

fun emailMostrable(u: Usuario): String? {
    return u.email
        ?.trim()
        ?.takeIf { it.contains("@") }
}

val u1 = Usuario("1", "Ana Pérez", " ana@correo.com ")
val u2 = Usuario("2", "Luis", "sin-arroba")

val e1 = emailMostrable(u1) // "ana@correo.com"
val e2 = emailMostrable(u2) // null

Buenas prácticas aplicadas: inmutabilidad y nulos en modelos reutilizables

Reglas prácticas para tus entidades (Usuario, Producto)

  • Prefiere data class para modelos de UI/estado.
  • Usa val en propiedades; evita var en entidades compartidas.
  • Representa opcionales con ? y normaliza en funciones puras.
  • Evita !!; usa ?., ?:, takeIf, requireNotNull solo cuando sea un contrato claro.
  • Centraliza transformaciones: por ejemplo, precioFinal(), emailMostrable(), nombreParaMostrar().

Mini práctica integradora: preparar datos para una pantalla

Objetivo: con una lista de productos, construir un “modelo de item” listo para UI (texto principal y secundario), sin nulos peligrosos y sin mutar la lista original.

data class ProductoItemUi(
    val id: String,
    val titulo: String,
    val subtitulo: String
)

fun aItemsUi(productos: List<Producto>): List<ProductoItemUi> {
    return productos.map { p ->
        val precio = p.precioFinal()
        val subtitulo = buildString {
            append("Precio: ")
            append(precio)
            append(" | ")
            append(if (p.stock > 0) "Stock: ${p.stock}" else "Sin stock")
        }
        ProductoItemUi(
            id = p.id,
            titulo = p.nombre,
            subtitulo = subtitulo
        )
    }
}

Ahora responde el ejercicio sobre el contenido:

Al transformar una lista de precios que puede contener valores nulos para calcular precios finales con impuesto y redondeo, ¿qué enfoque evita errores y mantiene el resultado como lista no anulable?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Filtrar nulos con filterNotNull() evita fallos por null y permite transformar con map a una List<Double> (no anulable), aplicando impuesto y redondeo de forma segura.

Siguiente capítulo

Estructura de una app Android: Activities, ciclo de vida y navegación

Arrow Right Icon
Portada de libro electrónico gratuitaAndroid desde Cero con Kotlin
14%

Android desde Cero con Kotlin

Nuevo curso

14 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.