Principios básicos de Compose (Parte 2)

Yago Rey
3 min readMay 27, 2023

--

Interfaz de usuario como una función de transformación

Un principio rector común para estructurar una aplicación es separar el concepto de “modelo” del de “IU”.

Supongamos que nuestro “modelo” es una lista de TodoItem. Una forma de separar nuestro modelo de negocio de nuestra UI es crear una función que simplemente transforme nuestra lista de elementos en un árbol de Node:

fun TodoApp(items: List<TodoItem>): Node {
return Stack(Orientation.Vertical).apply {
for (item in items) {
children.add(Stack(Orientation.Horizontal).apply {
children.add(Text(if (item.completed) "x" else " "))
children.add(Text(item.title))
})
}
}
}

Esto se puede usar para representar la interfaz de usuario en respuesta a nuestro modelo de datos específico de la aplicación. Una ejemplo de uso de esta función podría ser algo como:

fun main() {
todoItemRepository.observe { items ->
renderNodeToScreen(TodoApp(items))
}
}

Este ejemplo ha sido muy sencillo, pero a medida que la lógica de la función se vuelve más grande, la solución no escala bien y esto puede volverse difícil de manejar.

Una cosa que podemos hacer es crear un objeto “holder” que se aferre al nodo “parent” actual. Luego, podemos tener una función de "emisión" que agregará nodos al padre, pero también le permitirá proporcionar una lambda de "contenido".

Utilizamos la palabra clave "emitir" para describir que estaremos almacenando un nodo en una posición en el árbol, sin tener que saber exactamente a qué nodo lo estamos agregando.

Vamos a definirlo con la siguiente interfaz:

interface Composer {
// add node as a child to the current Node, execute
// `content` with `node` as the current Node
fun emit(node: Node, content: () -> Unit = {})
}

Obviamente existirá una clase que implemente esta interfaz, pero no vamos a centrarnos en la implementación concreta, sino que vamos a abstraernos de ella de momento (si tienes curiosidad puedes ver una posible implementación de esta interfaz en el post original sobre el que se base esta publicación: http://intelligiblebabble.com/compose-from-first-principles/)

Usando esta nueva abstracción, podemos reescribir nuestra función TodoApp como una función de extensión en Composer:

fun Composer.TodoApp(items: List<TodoItem>) {
emit(Stack(Orientation.Vertical)) {
for (item in items) {
emit(Stack(Orientation.Horizontal)) {
emit(Text(if (item.completed) "x" else " "))
emit(Text(item.title))
}
}
}
}

Si además creásemos una función de nivel superior llamada compose que crea un Composer, ejecuta un lambda con él como receptor y luego devuelve el nodo raíz:

fun compose(content: Composer.() -> Unit): Node {
return Stack(Orientation.Vertical).also {
ComposerImpl(it).apply(content)
}
}

Y cada vez que queramos representar nuestra interfaz de usuario en función de los elementos que tenemos, podemos ejecutar lo siguiente:

// render UI
renderNodeToScreen(compose { TodoApp(items) })

Poco a poco va cogiendo forma, ¿verdad?

Por último, y para rizar el rizo, con esta nueva abstracción, también es fácil extraer partes de nuestra interfaz de usuario en funciones más pequeñas:

fun Composer.TodoItem(item: TodoItem) {
emit(Stack(Orientation.Horizontal)) {
emit(Text(if (item.completed) "x" else " "))
emit(Text(item.title))
}
}

fun Composer.TodoApp(items: List<TodoItem>) {
emit(Stack(Orientation.Vertical)) {
for (item in items) {
TodoItem(item)
}
}
}

Este tipo de descomposición sencilla es una característica de importancia crítica. Podemos llamar a cada una de estas funciones “Componentes”.

Poco a poco vamos acercándonos a algo parecido a Compose.

En la parte 3 veremos el concepto de Memorización posicional, que es un concepto clave en el que todo empezará a cobrar sentido.

Si te has perdido la parte 1, te dejo el link por aquí.

Y si quieres seguir aprendiendo sobre compose nos vemos en la parte 3!

Por último,y como siempre, quiero recordar que este post es casi una traducción literal de:

http://intelligiblebabble.com/compose-from-first-principles/

Seguimos!

Si quieres escribirme puedes contactarme vía Linkedin a través de este link.

--

--