Funciones Ninja en Kotlin. Entendiendo la potencia de los genéricos (KDA 12)
Antonio Leiva

El uso simultáneo de varias funcionalidades de Kotlin unidas al uso de genéricos permiten crear funciones que nos simplificarán mucho el código, mientras mantenemos su legibilidad.

Hay varias funciones incluidas en la librería de Kotlin que son realmente útiles, pero que una vez dominados algunos conceptos son muy sencillas de utilizar.

Si quieres empezar hoy, te recomiendo que le eches un vistazo a mi training gratuito, donde tendrás una hora y media de contenido para saber cuáles son tus siguientes pasos a dar para convertirte en un experto en Kotlin.

Función with

Aunque existen varias funciones similares, me voy a centrar en with para destriparla.

¿Qué nos permite esta función? Con ella podemos hacer bloques de código en los que no tengamos que repetir innecesariamente la misma variable x veces.

Pueden venir a sustituir a los builders, sin necesidad de crear uno específico para cada clase.

Por ejemplo, volviendo al caso del ViewGroup que teníamos en el artículo anterior, podríamos convertir este código:

val childViews = (0..viewGroup.childCount - 1).map { viewGroup.getChildAt(it) }

en esto:

with(viewGroup) {
    val childViews = (0..childCount - 1).map { getChildAt(it) }
}

Como ves, el código dentro de los corchetes se comporta como si fuera código dentro de la propia clase.

¿Cómo conseguimos esto? Ya lo hemos visto anteriormente: con una función de extensión.

Funciones de extensión como parámetros

Ya estamos rizando el rizo, pero esto es tan útil que necesitas conocerlo.

Puedes definir una función de extensión como parámetro de otra función.

¿Cómo implementarías la función with para poder ejecutar el ejemplo anterior? Lo más sencillo sería esto:

inline fun with(view: ViewGroup, f: ViewGroup.() -> Unit) {
    view.f()
}

Como ves, recibe un ViewGroup como parámetro, y una función de extensión sobre ViewGroup. Nada limita por tanto que ese ViewGroup ejecute esa función.

Pero esto es muy poco flexible. ¿Vamos a necesitar una función similar para cada tipo de datos?

Por supuesto que no.

Tipos genéricos

Podemos hacer la función anterior genérica de forma muy sencilla. Tan solo hay que sustituir ViewGroup por T:

inline fun  with(obj: T, f: T.() -> Unit) {
    obj.f()
}

Ahora ya es útil para cualquier tipo. Un ejemplo:

with(textView) {
    text = "Hello World"
    visibility = View.VISIBLE
    textSize = sp(14).toFloat()
}

Pero aquí nos estamos perdiendo un potencial importante que hablábamos al principio: el de actuar como builder.

Devolver un valor de tipo genérico

Si queremos que actúe como un builder de verdad, necesitamos que el valor construido se devuelva de alguna forma:

inline fun  with(obj: T, f: T.() -> Unit): T {
    obj.f()
    return obj
}

De esta forma, nuestro código quedaría así:

val textView = with(TextView(this)) {
    text = "Hello World"
    visibility = View.VISIBLE
    textSize = sp(14).toFloat()
}

sp() es una función de la librería de Anko, de la que ya hemos hablado en alguna ocasión en esta serie.

Si te fijas en la definición oficial de la función, es muy parecida a lo que hemos hecho:

public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()

La principal diferencia es que la función de extensión que se pasa por parámetro retorna un valor que puede ser diferente del que se está pasando por parámetro.

Para conseguir el resultado anterior necesitaríamos hacer lo siguiente:

val textView = with(TextView(this)) {
    text = "Hello World"
    visibility = View.VISIBLE
    textSize = sp(14).toFloat()
    this
}

La última línea implica que se devolverá el objeto que está ejecutando la función de extensión.

Otras funciones interesantes

Hay una función que funciona muy parecido a lo que queríamos conseguir en el apartado anterior, y que se llama apply.

apply

En vez de pasarse el objeto por parámetro, esta función actúa como función de extensión de dicho objeto:

val textView = TextView(this).apply {
    text = "Hello World"
    visibility = View.VISIBLE
    textSize = sp(14).toFloat()
}

let

Otra función muy útil es let, que yo la encuentro particularmente útil para tratar con null encadenados.

Si el objeto correspondiente no es nulo, ejecutará el contenido dentro de la función de extensión:

textView?.text?.let { toast(it) }

El siguiente texto sólo se mostrará en un toast si el TextView no es null.

Conclusión

Utilizando la potencia de los tipos genéricos con funciones de extensión, podemos hacer cosas muy interesantes.

Te animo a que crees tus propias funciones que te permitan simplificar la tarea.

Si todo esto te apasiona tanto como a mí, te animo a que te apuntes a mi training gratuito donde te contaré todo lo que necesitas para aprender a crear tus Apps Android en Kotlin desde cero.

Quizá también te interese…

2 formas de recolectar Flows en la UI que SÍ funcionan

2 formas de recolectar Flows en la UI que SÍ funcionan

En la serie de artículos sobre Programación Reactiva con Flow hemos visto muchos conceptos, y hemos aprendido cómo aplicarlos al desarrollo Android. Pero hay algo que no hemos hecho del todo bien. Esto es la recolección de Flows desde la Activity (o el Fragment, en...

Cómo hacer tests de Corrutinas y Flows – Paso a Paso

Cómo hacer tests de Corrutinas y Flows – Paso a Paso

¡Vaya viaje por el que hemos pasado en estos artículos! Hace ya varios de ellos empezamos hablando sobre la programación reactiva con Flow, y hemos aprendido un montón de conceptos e ideas sobre cómo aplicarlos en el día a día. Pero nada de esto está completo si no...

Convertir cualquier callback en un Flow con CallbackFlow

Convertir cualquier callback en un Flow con CallbackFlow

Existen varios tipos de Flows muy particulares que nos van a solucionar la vida cuando tengamos que hacer cosas muy concretas. Ya vimos StateFlow en un artículo anterior, y en esta ocasión hablamos de CallbackFlow ¿Qué es CallbackFlow? Es un tipo de Flow que nos...