Lenguaje Kotlin, Repaso Avanzado – Curso gratis de Kotlin avanzado (Capítulo 1)

« Volver al inicio del curso

Pese a que este curso es principalmente de temas avanzados, en estos primeros capítulos veremos un pequeño repaso de los conceptos fundamentales de Kotlin. De este modo no te cogerán por sorpresa cuando los veamos a lo largo del curso.

Muchas veces compararé el lenguaje con Java, que es donde más tiempo he dedicado en mi carrera como programador.

Estas son las diferencias principales respecto a Java:

Las variables pueden ser mutables e inmutables (similar a `final` en Java)

var x = 5 // Mutable
val y = 6 // Inmutable

Si el compilador puede inferir con qué tipo de datos estamos trabajando no es necesario indicar el tipo:

var x = 1 // x es un Int

No existen tipos básicos o primitivos. Todo son objetos.
– No existe `void`. void = Unit
– No existe int, long, etc como primitiva. Sólo como objeto

Se necesita casting explícito para pasar de números simples a complejos. Ejemplo:

val x: Int = 5
val y: Long = x.toLong()

Clases y objetos

Una clase se define con la palabra reservada class. Por defecto las clases serán públicas.

No existen los campos de una clase, sino que tiene properties

class Person {
  var name = "Name"
  var surname = "Surname"
}

Las clases nos permiten utilizar directamente las properties sin necesidad de un getter o un setter:

persona1.name = “Nuevo nombre” // equivalente en Java a persona1.setName(“Nuevo nombre”)

Aunque también se podría modificar el método setter o getter del siguiente modo:

var name = "Name"
  set(value) {
    name = "Name: $value"
  }

Generalmente una clase solamente suele tener un constructor, definido a la derecha del nombre de la clase. Este es el constructor principal:

class Person(val name: String, val surname: String)

Definiendo el constructor principal no es necesario declarar las properties entre los corchetes de la clase.

Aunque también puede haber constructores adicionales o secundarios con el siguiente formato:

constructor(name: String) {
  this.name = firstName
  this.surname = “Default”
}

Herencia

Por defecto, en Kotlin las clases extienden de Any (equivalente a Object en Java), pero no pueden ser extendidas por otras clases. Las clases son cerradas por defecto (final) y sólo pueden ser extendidas si se declaran específicamente como open o abstract.

open class Animal(name: String)
class Person(firstName: String, lastName: String) : Animal(firstName)

Cabe destacar que cuando se usa la estructura de constructor primario necesitamos especificar los parámetros usados al constructor padre. Esto es equivalente al super() de Java.

Data Class

Las data class son un tipo especial de clase creadas para almacenar datos. Sería el equivalente a un POJO en Java.

Se utilizan habitualmente por ejemplo para representar una estructura en una base de datos o en un API REST. Ejemplo:

data class User(val name: String, val age: Int)

Lo que conseguimos con esto es que se creen automáticamente los métodos:

  • equals()
  • hashCode()
  • toString()
  • componentN() para cada uno de las propiedades en el orden de declaración (en el ejemplo
    de User component1() nos devolvería el nombre de la persona, component2() la edad)
  • copy()

Para copiar objetos modificando solo algunas propiedades se puede utilizar el siguiente código:

val jack = User(name = "Jack", age = 10)
val olderJack = jack.copy(age = 20)

Las data classes también nos permiten “desestructurar” declaraciones:

val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age") // prints "Jane, 35 years of age"

La forma de desestructurar es también por orden de declaración de propiedades.

Objetos

Existe otro tipo especial de clases: los objetos.

Un objeto es como una clase pero que no puede ser instanciada. Por ello deben ser llamadas por nombre con nomenclatura de punto (equivalente a una clase estática).

Sin embargo, pese a no poder ser instanciada SÍ EXISTE UNA INSTANCIA. Esto quiere decir que las propiedades definidas en un objeto van a ser compartidas a lo largo de la aplicación. O lo que es lo mismo, los objetos son una forma de declarar un Singleton en una sola línea:

Este código en Java:

public final class ExampleObject {
  public static final ExampleObject INSTANCE = new ExampleObject();

  private ExampleObject() { }

  public final void example() {
  }
}

Lo podríamos reemplazar por lo siguiente en Kotlin:

object ExampleObject {
  fun example() {
  }
}

En Java lo utilizaríamos como:

ExampleObject.INSTANCE.example();

Mientras que en Kotlin lo utilizamos directamente:

ExampleObject.example()

Interfaces

Las interfaces permiten una mayor reutilización de código.

La forma de declarar una interfaz es simplemente utilizar la palabra reservada `interface`.

Una diferencia importante respecto a Java (al menos en versiones anteriores a Java 8) es que en Kotlin las interfaces pueden tener código:

interface Sample {
  fun function1() {
      Log.d("Sample", "function1 called")
  }
}

Una clase que implemente esta interfaz podrá utilizar esta función.

La forma de indicar que una clase implementa una interfaz es con dos puntos

class MyClass : Sample {
  fun myFunction() {
    function1()
  }
}

Sin embargo las interfaces no pueden tener estado. Esto es, no podemos crear una property y utilizar su valor sin más, por ejemplo:

interface Toaster {
  val context: Context
  fun toast(message: String) {
  Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
  }
}

Este código se encarga de mostrar un mensaje en un toast. Para poder utilizar dicha función necesitamos definir un contexto (por ejemplo una Activity). La forma de definir este contexto en la interfaz necesitaremos sobrescribirlo en la implementación. Ejemplo:

class MyActivity : AppCompatActivity(), Toaster {
  override val context = this

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    toast("onCreate")
  }
}

Clases selladas

Una clase sellada nos permite representar una jerarquía restringida a una lista limitada de subtipos. Ejemplo:

sealed class Expresion {
  class Constante(val number: Double) : Expresion()
  class Suma(val e1: Expresion, val e2: Expresion) : Expresion()
  object NotANumber : Expresion()
}

Una clase sellada es abstracta, por lo que no puede ser instanciada.

Las clases selladas tienen sentido cuando se utilizan dentro de una sentencia when:

private fun eval(expr: Expresion): Double {
  return when (expr) {
    is Expresion.Constante -> expr.number
    is Expresion.Suma -> eval(expr.e1) + eval(expr.e2)
    Expresion.NotANumber -> Double.NaN
    // Si se queda alguna operación fuera, el código no compila
    // En este caso no necesitamos un else porque hemos cubierto todos los casos
  }
}

Delegados y propiedades delegadas

La delegación es un patrón que nos permite extraer (delegar) responsabilidad de una clase o de una propiedad.

En Kotlin no se nos permite crear una propiedad fuera de un constructor sin asignarle un valor. Por ejemplo:

class MainActivity : AppCompatActivity()
var tv: TextView // Esto falla en compilación
}

Una de las formas de evitar esto podría ser utilizar el valor null, pero esto es una práctica poco recomendada.

Una opción mucho mejor es utilizando la delegación de propiedades.

Para ello marcaríamos la propiedad tv con un lateinit

lateinit var tv:TextView

De este modo le estamos diciendo a esa property que no va a estar vacía, pero que todavía no tenemos su valor.

En el caso de las activities podríamos rellenar su valor en el onCreate()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView = findViewById(R.id.welcomeMessage)
}

El problema del código anterior es que debemos marcar la propiedad como mutable.

lazy

Otra opción podría ser utilizar el delegado lazy. La forma de utilizarlo es:

val textView by lazy { findViewById(R.id.action_bar) }

De este modo el código dentro del bloque lazy no se ejecutará hasta que no se llame al get() del textView por primera vez.

observable

Otro de los delegados que se suele utilizar en Kotlin es el observable. Nos sirve para ejecutar un código cuando suceda un cambio en una lista. Por ejemplo el siguiente delegado establece un valor inicial a la lista (vacía) y ejecuta el código tras cada modificación. En este caso simplemente
estamos actualizando la IU de un adapter:

var items: List by Delegates.observable(emptyList()) {
property, old, new -> notifyDataSetChanged()
}

vetoable

Este delegado es similar al observable pero se ejecuta antes de asignar el valor. De este modo podemos decidir si almacenamos el nuevo valor o no:

var positiveNumber by Delegates.vetoable(0) {
_, _, new →
new > 0
}

Extraer valores de un mapa usando delegados

Otra forma de utilizar los delegados es para extraer valores de un mapa en distintas propiedades utilizando para ello la clave del mapa:

class Configuration(map: Map<String, Any>) {
  val width: Int by map
  val height: Int by map
  val dp: Int by map
  val deviceName: String by map
}

Básicamente sería lo contrario a lo que hace el mapOf:

val conf = Configuration(mapOf(
  "width" to 1080,
  "height" to 720,
  "dp" to 240,
  "deviceName" to "mydevice"
))

Funciones y lambdas

Funciones

Se declaran como ya hemos visto con la palabra reservada fun

Las funciones en Kotlin siempre retornan un valor. Si no especificamos un valor de retorno estará retornando Unit *

Si una función puede ser calculada utilizando una sola expresión podemos eliminar las llaves y utilizar el símbolo igual:

fun add(x: Int, y: Int): Int {
return x + y
}

// equivalente a:
fun add(x: Int, y: Int) : Int = x + y

* en la función anterior no es necesario establecer el tipo devuelto porque lo puede inferir de la expresión.

Podemos establecer un parámetro como opcional asignando un valor por defecto.

fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, length).show()
}

Esto previene la necesidad de utilizar la sobrecarga de métodos:

toast("Hello")
toast("Hello", Toast.LENGTH_LONG)

Lambdas

Una lambda es una forma de representar una función.

Por ejemplo si declaramos un evento clic sobre un botón:

view.setOnClickListener ({ v: View -> doSomething(v) })

Estamos declarando como argumento del setOnClickListener una lambda con valores de entrada a la izquierda de la flecha y la operación a realizar a la derecha.

Si el último argumento de una función es otra función, podemos sacar este código del paréntesis:

view.setOnClickListener() { v: View -> doSomething(v) }

Y si sólo hay una función como parámetro aún podemos acortarlo más eliminando los paréntesis:

view.setOnClickListener { v: View -> doSomething(v) }

También podemos omitir el tipo de dato:

view.setOnClickListener { v -> doSomething(v) }

Adicionalmente si la función que representa la lambda tiene un parámetro podemos utilizar la palabra reservada “it” para referirnos al mismo:

  view.setOnClickListener { doSomething(it) }

Para definir nosotros mismos una función que acepte una lambda en Java necesitábamos una interfaz funcional (con un sólo método), pero en Kotlin lo hacemos del siguiente modo:

fun showItem( onShow: () -> Unit ) {
  // ... operaciones ...
  // invocar el código proporcionado en onShow()
  onShow()
}

Es decir, en lugar de declarar el listener con un tipo determinado (Int, String, etc) lo que hacemos es declararlo como una función, que no toma argumentos y retorna un void.

Estamos delegando la creación de esa función a quien utilice esta clase.

Otro ejemplo, declarando la lambda directamente en el constructor de una clase:

class MyList(
  val items: List,
  val itemClickListener: (item: MyRow) -> Unit) {</pre>
<pre>  fun click(position: Int) {
    itemClickListener(items[position]) // ejecutamos la función con el parámetro deseado
  }
}

Cuando alguien llame a la función click() se ejecutará el código que haya decidido definir en el constructor de MyList.

Funciones en línea

Lo malo de crear funciones que reciban funciones como argumento es que el compilador necesita para ello crearse clases anónimas consumiendo recursos. Una forma de evitar esto es marcar la función como `inline`

inline fun showItem( onShow: () -> Unit ) {
  // ... operaciones ...
  // invocar el código proporcionado en onShow()
  onShow()
}

Lo que ocurre cuando marcamos una función con inline es que el compilador internamente copia esa función por su código en los sitios donde se llame.

Esto solo tiene sentido en funciones que tengan otras funciones como parámetro.

Operaciones funcionales sobre colecciones

Las operaciones funcionales funcionan de forma muy similar a los streams de Java 8.

Son una serie de métodos que generalmente toman como parámetro una función, por lo que vamos a utilizar lambdas para tratarlos.

Vamos a ver algunas de las operaciones funcionales más importantes tomando como ejemplo la siguiente lista:

val items = listOf("one", "two", "three")

forEach

Itera sobre una lista:

items.forEach { cadena -> Log.d("Sample", cadena) }

O lo que es lo mismo:

items.forEach { Log.d("Sample", it) }

map

La operación map modifica el tipo de dato de entrada por un nuevo tipo de dato para cada elemento.

Por ejemplo, en nuestra lista de strings podemos mapear los elementos como enteros:

val longitudes = items.map { it.length } // longitudes es una List

En el código anterior lo que estamos haciendo es pasar de tener un String a tener el tamaño de cada uno de los strings en una nueva colección.

filter

Filter va a permitirnos filtraar los elementos dada una condición. Por ejemplo si queremos quedarnos únicamente con los elementos de más de 3 caracteres:

val soloMenorDe3 = items.filter { it.length > 3 } // solo three cumple

any

Retorna true si algún elemento cumple la condición:

val hayMayorQue3 = items.any { it.length > 3 } // true, alguno la cumple

all

Retorna true si TODOS los elementos cumplen la condición:

val todosMayorQue3 = items.all { it.length > 3 } // false, alguno NO la cumple

count

Número de elementos que cumplen la condición:

val cuantosMayorQue3 = items.count { it.length > 3 } // 1 lo cumple
val cuantos = items.count() // sin parámetros devuelve el total de resultados
[/javascript]

max / min

Devuelve el mayor/menor elemento o null si no hay elementos:

val elMayor = items.max() // en strings por ordenación natural el mayor es “two”
val elMenor = items.min() // en strings por ordenación natural el menor es “one”

maxBy / minBy

Devuelve el mayor/menor elemento dada una función o null si no hay elementos:

val elMayorPorLongitud = items.maxBy { it.length } // el mayor es three
val elMenorPorLongitud = items.minBy { it.length } // el menor es one (por ser el primero)

first / last / firstOrNull / lastOrNull

First y last nos devuelven el primer o el último elemento que cumplan cierta condición. Pueden lanzar un NoSuchElementException. Para evitarlo están las alternativas OrNull

var firstStartingWithT = items.firstOrNull { it.startsWith("t") } // two
var lastStartingWithT = items.lastOrNull { it.startsWith("t") } // three

También se pueden ejecutar sin parámetros:

items.first()

sortedBy

Como su nombre indica, sorted by nos permite ordenar según una condición, por ejemplo de menor a mayor longitud (el menor número tiene más prioridad):

val ordered = items.sortedBy { it.length }

Combinación de operaciones

Podemos ir concatenando tantas operaciones sobre colecciones como queramos, por ejemplo:

val allInOne = items
  .map { it.length } // mapear a lista de enteros
  .sortedBy { it * -1 } // ordenar por el número de caracteres a la inversa
  .forEach { Log.d("Sample", "$it") } // imprimir el valor
  .first() // nos quedamos con el primer elemento de la lista, que en este caso es 5 (el número de caracteres que tiene “three”)

Extensiones

Funciones de extensión

Las funciones de extensión nos permiten extender la funcionalidad de las clases sin tocar su código.

Esto es útil para incorporar funcionalidad a clases que no son nuestras.

Para ello sólo hay que escribir la función y poner delante del nombre la clase sobre la que quieres extender, separado por un punto.

Ejemplo: añadir un método visible() a la clase View que cambie su visibilidad:

fun View.visible() {
  this.visibility = View.VISIBLE
}

Como veis se puede utilizar “this” como si estuviéramos dentro de la propia clase.

Otro ejemplo: Para inflar una vista dentro de un adapter haríamos algo como:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
  val v = LayoutInflater.from(parent.context).inflate(R.layout.view_item, parent, false)
  return ViewHolder(v)
}

Podríamos crear una función de extensión para que los ViewGroup pudieran inflar vistas con un sólo método, por ejemplo:

fun ViewGroup.inflate(layoutRes: Int): View {
return LayoutInflater.from(context).inflate(layoutRes, this, false)
}

Y el código anterior se transformaría en:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
  val v = parent.inflate(R.layout.view_item)
  return ViewHolder(v)
}

Kotlin Android Extensions

Se trata de un plugin de gradle. Son una serie de utilidades creadas por Kotlin para hacer el desarrollo de aplicaciones Android más simple.

El plugin crea automáticamente una serie de propiedades que nos dan acceso a las vistas del XML. De este modo nos evitamos los famosos findViewById.

Los nombres de las propiedades se cogen del XML, así que deberíamos utilizar nomenclatura de código a la hora de asignar identificadores: nombreDeVista

Además, utiliza un sistema de caché que previene de realizar internamente un findViewById cada vez que una propiedad se usa.

Para utilizarlo: en app/build.gradle

apply plugin: 'kotlin-android-extensions'

Ahora si tenemos por ejemplo un layout:

Podemos acceder a la vista con el siguiente código:

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)
  welcomeMessage.text = "Hello Kotlin!"
}

Para utilizarlo habremos necesitado el siguiente import especial:

import kotlinx.android.synthetic.main.activity_main.*

Si queremos utilizar esta extensión en adapters (patrón ViewHolder), para poder cachear las vistas debemos hacer un paso adicional, extender de LayoutContainer:

class ViewHolder(override val containerView: View)
      : RecyclerView.ViewHolder(containerView), LayoutContainer {
  fun bind(title: String) {
    itemTitle.text = "Hello Kotlin!"
  }
}