1. Introducción al lenguaje Kotlin
Puedes usar IntelliJ IDEA o https://play.kotlinlang.org para probar código.
1.1 Sintaxis básica
1.1.1 Constantes
Para definir una constante en kotlin se usa la palabra clave val (value o valor). Estamos creando un "valor" constante:
Todo lo siguiente es inmutable:
fun main() {
val nombre = "Marcos"
val edad = 38
//val edad = 39 // ERROR si descomento esta línea: no puedo modificarlo ya que es una constante!
// Muestro el nombre por pantalla
println(nombre)
// Muestro la edad por pantalla
println(edad)
}
1.1.2 Función de imprimir por pantalla (print y println)
- La función
print(mensaje)nos permite mostrar por pantalla sin salto de línea. - La función
println(mensaje)nos permite mostrar por pantalla (con salto de línea final).
fun main() {
println("Esto es un mensaje.")
val mensaje = "Hola mundo"
println(mensaje)
println()
println()
print("Hola")
print(" ")
println("mundo")
}
Podemos concatenar los mensajes con el operador "+" (igual que haciendo una suma de números):
fun main() {
val nombre: String = "Lucía"
val edad: Int = 17
println("El nombre es: " + nombre)
println("La edad es: " + edad)
println()
println("El nombre es " + nombre + " y su edad es: " + edad + " años.")
}
Otra manera más sencilla (si eres nuevo en programación te recomiendo practicar antes la anterior) es usar el caracter $.
fun main() {
val nombre: String = "Lucía"
val edad: Int = 17
println("El nombre es: $nombre")
println("La edad es: $edad")
println()
println("El nombre es $nombre y su edad es: $edad años.")
}
Aunque esta forma es más simple y entendible, asegúrate también de estar adaptado a la primera si estás empezando a programar. Yo las variaré a lo largo de los ejemplos.
1.1.3 Variables: declaración y asignación
Para declarar variables se usa la palabra clave var. La sintaxis es la siguiente:
// Declaración
var nombreVariable: Tipo
// Asignación
nombreVariable = Valor
// Declaración y asignación todo a la vez
var nombreVariable = Valor // No haría falta el tipo, ya que al asignarle el valor directamente ya lo infiere
Veámoslo con un ejemplo real:
fun main() {
// Si declaro algo pero no asigno un valor hay que ponerle el tipo de dato que voy a usar!
var numero: Int
// Asigno despues
numero = 10
// Ahora no hace falta poner tipo, porque ya asigno un "entero" (Int). Por tanto Kotlin ya que su tipo es Int.
var otroNumero = 38
// Cambio la edad (puedo, ya que es una variable y puedo modificarla)
otroNumero = 39
println("El primer numero es: " + numero + " y el segundo numero es: " + otroNumero + ".");
}
Para crear variables a las que no les asigno nada tengo que indicar su tipo (Int, String, Long, Float, Double...). En el caso de String no te funcionará (pronto lo veremos).
1.1.3.1 Ejercicio
Intenta hacer el siguiente ejercicio (cíñete a lo que pide cada comentario y resuelve debajo del mismo):
fun main() {
// 1. Declara una variable "ciudad" y asígnale tu ciudad de nacimiento
// 2. Muestra la ciudad por pantalla
// 3. Crea una constante "añoActual" de tipo entero.
// 4. Asígnale a la variable el año en el que estamos: 2026.
// 5. Muestra por pantalla el año actual.
}
Bien, ya has interiorizado un poco la declaración y asignación de variables en Kotlin. Vamos a ver ahora como tratar los nulos.
1.1.4 Variables: seguridad ante nulos
El lenguaje Kotlin es un lenguaje seguro ante nulos. Por defecto, en sus variables, no permite valores nulos. Esta es la razón por la que esto no funciona:
fun main() {
// Ninguna de estas líneas es válida, ya que las variables no pueden tener valores nulos
var miCiudad: String
miCiudad = ""
var ciudad2: String = null
val ciudad3: String = null
}
Los tipos en Kotlin no son nullables
Ningún tipo de dato en Kotlin puede ser nulo. Si queremos que un tipo de dato pueda ser nulo se hace con el caracter "?". Por ejemplo: Int?, Double?, String?...
No se permiten valores nulos en Kotlin por defecto. Pero hay una manera de decirle a Kotlin que queremos dar un permiso especial a la variable para que sí contenga valores nulos (podemos pensar que le damos consentimiento):
- Operador de tipo nullable: ?
- Operador de llamada segura: ?.
- Operador de aserción de nulos: !!
Veamos para que sirven estos operadores. Empecemos por el operador "?":
fun main() {
// No funciona: "Property must be initialized."
var ciudad: String
// No funciona: "Null cannot be a value of a non-null type 'String'."
var ciudad2: String = null
}
"Null cannot be a value of a non-null type 'String'. Esto significa que no solo ciudad es de tipo String, sino que en Kotlin, todos estos tipos son NO NULLABLES (vale para cualquier otro tipo de dato).
¿Cómo hacemos entonces que funcione? Pues tenemos que decirle que la variable no solo es de tipo String, sino que es de tipo "String nullable". Esto se escribe como "String?":
// FUNCIONA! Le indico que es un String nullable
fun main() {
var ciudad: String? = null
println("Ciudad tiene el valor: " + ciudad)
ciudad = "Lugo"
println("Ahora ciudad tiene el valor: " + ciudad)
}
Ejercicio
Modifica los códigos de los 2 programas anteriores que no funcionaban añadiéndoles al tipo el operador "?". Con esto deberían compilar.
1.1.5 Operador de llamada segura: ?.
En el ejemplo anterior vemos que ciudad ya no es de tipo String, sino que es de tipo String?. Para hacer llamadas a métodos de un objeto nullable, tenemos que usar el operador de llamada segura: "?.". Por ejemplo:
// FUNCIONA! Le indico que es un String nullable
fun main() {
var ciudad: String? = "Lugo"
println("Longitud del string: " + ciudad?.length)
}
Ejercicio
- Ejecuta el código anterior sin usar el ?. (solo .). Observarás que saltará un error. Luego vuelve a dejarlo funcionando.
- Modifica el programa para que en lugar de asignar Lugo a
ciudad, esta sea nula. Después, piensa lo siguiente: - ¿Qué ocurriría en Java si ejecutas la línea del println siendo nulo el valor de "ciudad"?
- ¿Qué ocurre aquí al ejecutarlo?
En Java, si ejecutas un método sobre cualquier objeto nulo. (por ejemplo: NULL.toString()) saltará una NullPointerException(). Los tipos con "?" en Kotlin permiten evitar esta excepción.
1.1.6 Operador !!
Este operador no está recomendado, pero si quisieses ejecutar código como lo harías en Java (haciendo que Kotlin ignore la posible NullPointerException y te permita compilar el código), puedes usar el operador !!:
fun main() {
var ciudad: String? = "Lugo"
println("Longitud del string: " + ciudad!!.length)
}
Ejercicio
Prueba a cambiar el valor de ciudad a nulo y observa lo que ocurre. Es lo mismo que ocurriría en un programa cualquiera en Java o C#, ¿entiendes ahora el valor de estos operadores?
1.1.7 Smart casting
Si haces una comprobación de nulos antes de la llamada a las propiedades del objeto (en el caso anterior a length) ya no haría falta usar ?.. Esto se denomina smart casting.
fun main() {
var ciudad: String? = null
// Comprobamos si la ciudad tiene un valor: en ese caso ya no necesito el operador "?."
if (ciudad != null) {
println("Longitud del string: " + ciudad.length)
}
}
1.1.8 Operador Elvis ?:
Esto es lo último que mostramos de ejemplo relacionado con los nulos.
fun main() {
val nombre: String? = null
val longitud = nombre?.length ?: 0 // Si el resultado es nulo devuelve 0
println("Longitud: $longitud")
}
La variable longitud necesita coger un valor, lo que hace ?: es que, si el operando de la izquierda es NO NULO, entonces lo usará, pero si es NULO devolverá 0.
Ejercicio
Prueba a cambiar el valor anterior de "nombre" a null y ejecuta el código.
1.1.9 Función de leer teclado: readln()
Si quieres leer de teclado puedes usar la función readln(). Posteriormente, puedes convertir los datos obtenidos en lo que quieras con las funciones de conversión de tipos: toInt(), toDouble(), toString()...
La función readln() no funciona en esta web
La función readln() no funcionará en los fragmentos de código mostrados en esta web. Si quieres usarla, tendrás que hacer el programa en tu equipo.
1.2 Funciones
Las funciones es el concepto más importante que tienes que entender en programación funcional y para desarrollar interfaces en Android con Compose.
1.2.1 Sintaxis básica
Esta es la sintaxis básica de las funciones:
fun nombreFuncion(param1: Tipo1, param2: Tipo2, ...): TipoRetorno {
// cuerpo de la función
return valor
}
fun→ palabra clave para declarar funciónnombreFuncion→ identificador de la función(param1: Tipo1, ...)→ parámetros con nombre y tipo: TipoRetorno→ tipo de retorno de la función (obligatorio si devuelve algo){ ... }→ cuerpo de la funciónreturn→ devuelve un valor (obligatorio si el tipo de retorno no esUnit)
Veamos algunos ejemplos:
// Funcion que recibe 2 enteros como parámetro y devuelve otro entero
fun sumar(a: Int, b: Int): Int {
return a + b
}
fun main() {
val num1 = 10
val num2 = 20
// Podemos llamar a la función dentro de un template string con ${}
println("La suma de $num1 y $num2 es ${sumar(num1, num2)}.")
}
Otro ejemplo ahora de funciones que no devuelven nada (Unit):
// Funcion que no devuelve nada (Unit)
fun imprimirSaludo(nombre: String): Unit {
println("Hola $nombre")
}
// Escribir unit es opcional
fun imprimirSaludo2(nombre: String) {
println("Hola $nombre")
}
fun main() {
imprimirSaludo("Lucía")
imprimirSaludo2("Ángel")
}
1.2.2 Funciones como expresiones
Al crear funciones podemos no escribir las llaves "{}" ni el "return". Es una forma más compacta y simple de escribir funciones pequeñas:
// Forma normal
fun saludarVersionNormal(nombre: String): String {
return "Hola $nombre"
}
// Forma compacta (función como expresión)
fun saludarVersionCorta(nombre: String) = "Hola $nombre"
La última forma es más simple. En ella ignoramos el tipo devuelto (ya supone que es un String), el return y las llaves. Las funciones se pueden definir de la forma normal (imperativa) o como expresiones. En Kotlin es muy habitual usarlas en forma de expresiones.
Ejercicio
- Modifica el código anterior para que tenga una función main donde pruebes ambas versiones.
- Crea una función como expresión "saludar(nombre) que diga: "Hola <\nombre>".
1.2.3 Parámetros por defecto
Se pueden pasar parámetros por defecto a las funciones. En Kotlin puedes darle un valor inicial a los parámetros. Si el usuario no pasa nada, se usa ese valor.
fun saludar(nombre: String = "Invitado") = "Hola $nombre"
fun main() {
// Llamadas válidas
println(saludar()) // usa "Invitado"
println(saludar("Marcos")) // usa "Marcos"
}
En caso de tener varios parámetros por defecto :
fun crearUsuario(
nombre: String,
edad: Int = 0,
esAdmin: Boolean = false
) {
println("Usuario: $nombre, edad $edad, admin: $esAdmin")
}
fun main() {
// Llamadas válidas
crearUsuario("Marcos")
crearUsuario("Ana", 25)
crearUsuario("Luis", 30, true)
}
Parámetros por defecto en compose...
En Android, existen funciones como Text("Hola mundo") que reciben un texto y lo muestran. Estas funciones no reciben solo 1 parámetro sino muchísimos más que tienen asignado un valor por defecto (alineación del texto, fuente, tamaño...). Por esta razón lo anterior funciona, pero podemos reutilizar la función pasando muchas variedades distintas de parámetros.
1.2.4 Parámetros nombrados
Se pueden pasar parámetros por su nombre (en ese caso no importaría el orden que uses para pasarlos):
fun restar(a: Int, b: Int) = a - b
fun main() {
// Llamada típica (pasando los parámetros por orden)
val resta = restar(10, 3)
// Llamada con los parámetros nombrados
val resta2 = restar(a = 10, b = 3)
println("El resultado de la resta es: $resta2")
}
Puedo pasar los parámetros desordenados siempre y cuando sean nombrados:
fun crearUsuario(
nombre: String,
edad: Int = 0,
esAdmin: Boolean = false
) {
println("Usuario: $nombre, edad $edad, admin: $esAdmin")
}
fun main() {
// TODO: llama a crear usuario de 2 formas distintas:
// 1. Pasando los tres parámetros por posición.
// 2. Pasando solo dos parámetros de la manera que quieras.
// 3. Pasando los parámetros desordenados.
}
Ejercicio
Completa el main anterior.
1.2.5 Parámetros mezclados por posición y por nombre
Se pueden mezclar los parámetros por posición y nombre. Eso sí, una vez pasas el primer parámetro por nombre ya no puedes seguir pasando por posición.
Como ejemplo, te muestro dos llamadas (una que funcionaría y otra que no.):
// Funciona
crearUsuario("Marcos", esAdmin = true)
// No funciona
crearUsuario(nombre = "Marcos", 25)
Ejercicio
Prueba las llamadas anteriores en el main del código anterior. Asegúrate de entender por qué funcionan o no lo hacen.
1.2.6 Paso de parámetros en Android con Compose
Usar parámetros nombrados (y los valores por defecto que ya hemos visto) son muy prácticos para diseñar interfaces:
Interfaces en Android con funciones
La función Text anterior es un ejemplo de como podríamos mostrar un texto en Android que no permita más de 1 línea. Lo estamos llamando usando parámetros nombrados, ¿te fijas en que es muy legible?
1.3 Clases y objetos
TODO
1.4 Data classes
Las data classes nos autogeneran los métodos toString() equals() hashCode() y copy(). Se crean de la siguiente forma:
// Este data class genera: toString() equals() hashCode() y copy(). En android es importante que los modelos sean inmutables (val)
data class Usuario(
val nombre: String,
val edad: Int
)
fun main() {
val user = Usuario("Daniel", 17)
println(user)
}
Esta clase tiene automáticamente todos los getters y setters, el toString(), copy(), equals() y hashCode().
1.5 Colecciones
1.5.1 Arrays
Permite modificar los valores, pero no puedes cambiar el tamaño del array. Se usan en estos casos:
- Cuando necesitas interoperabilidad con Java.
- Cuando el tamaño no va a cambiar.
- Cuando buscas rendimiento un poco mayor que una lista.
fun main() {
// Creamos un array de números
val numeros = arrayOf(1, 2, 3)
// Cambio el número de la segunda posición por un 10
numeros[1] = 10
// Imprimo el número de la segunda posición
print(numeros[1])
// Esto no funcionaría (no puedo añadir otro número al array)
// numeros += 4 // no se puede: no crece
}
Las listas son más prácticas y habituales en Android. No usarás muchos arrays de este tipo.
1.5.2 Listas inmutables (List<T>)
Existen listas mutables (el tipo es (MutableList<T>)) y listas inmutables (el tipo es (List<T>)). Las inmutables se pueden hacer con listOf. Se usan cuando:
- Para datos que no deben modificarse
- Ideal para modelos, configuraciones, constantes
- En Kotlin y Android lo más habitual es usar listas inmutables.
fun main() {
val letras: List<String> = listOf("a", "b", "c")
letras.add("d") // error
}
Ejercicio
Intenta modificar el main anterior para que letras.add("d") funcione. Para ello tendrás que convertir la lista en "mutable" (se hace con mutableListOf() en lugar de listOf()).
Recuerda cambiar también el tipo de la lista (o quitarlo y dejar que lo infiera Kotlin).
1.5.3 Listas mutables
Se puede añadir, eliminar y modificar.
fun main() {
val letras = mutableListOf("a", "b", "c")
letras.add("d") // Funciona
letras.remove("b") // Funciona
letras[0] = "z"
}
1.5.4 Métodos de listas
A continuación de muestran varios ejemplos de métodos que puedes usar sobre una lista:
fun main() {
// Declaración de arrays y listas
val arr = arrayOf(10, 20, 30)
val listaInmutable = listOf("A", "B", "C")
val listaMutable = mutableListOf("A", "B", "C")
listaMutable.add("D")
// Acceso a elementos
val nombres = listOf("Ana", "Luis", "Eva")
// Acceso por índice
println("Nombre en posición 2: ${nombres[1]}") // Luis
// Usando otros métodos útiles
val primerNombre = nombres.first()
val ultimoNombre = nombres.last()
val tamanhoLista = nombres.size
println("Primer nombre: $primerNombre.")
println("Último nombre: $ultimoNombre.")
println("Tamaño de la lista: $tamanhoLista.")
// Añadido y eliminación de elementos (para listas mutables)
val nums = mutableListOf(1, 2, 3)
nums.add(4)
nums.remove(2)
nums.removeAt(0)
nums.clear() // deja la lista vacía
}
1.6 Iteración sobre listas con bulces
fun main() {
val numeros = mutableListOf(1, 2, 3)
// Iteración sobre listas
for (n in numeros) {
println(n)
}
for ((indice, valor) in numeros.withIndex()) {
println("$indice -> $valor")
}
for ((indice, valor) in numeros.withIndex()) {
println("$indice -> $valor")
}
numeros.forEachIndexed { index, value ->
println("$index : $value")
}
}
1.7 Iteración sobre listas usando programación funcional
fun main() {
// Métodos de alto nivel
val lista = listOf(1, 2, 3, 4, 5)
val listaPor2 = lista.map { it * 2 } // [2, 4, 6, 8, 10]
println("Lista multiplicada por 2: $listaPor2")
val listaPares = lista.filter { it % 2 == 0 } // [2, 4]
println("Lista de pares: $listaPares")
val listaOrdenada = lista.sorted() // [1, 2, 3, 4, 5]
println("Lista ordenada: $listaOrdenada")
val hayElementosMayoresQue3: Boolean = lista.any { it > 3 } // true
println("Hay elementos mayores que 3 en la lista? $hayElementosMayoresQue3")
val todosSonPositivos: Boolean = lista.all { it > 0 } // true
println("Todos son positivos? $todosSonPositivos.")
val sumaTotal = lista.sum()
println("Suma de todos los valores de la lista: $sumaTotal")
}