Principios básicos de Compose (Parte 5)

Yago Rey
3 min readMay 27, 2023

--

Anotación @Composable

Hemos avanzado bastante en la creación de la interfaz de usuario de nuestra aplicación utilizando la funciones de extensión Composer. Dicho esto, hemos logrado complicar realmente una interfaz de usuario básica para que este enfoque sea eficiente y sólido.

Todo el texto repetitivo que hemos agregado podría haberse agregado sistemáticamente. Podríamos seguir una fórmula simple o un conjunto de reglas y agregar este modelo correctamente sin saber nada sobre la aplicación específica.

Como resultado de esto, es razonable que el compilador genere este código por nosotros. Compose introduce una anotación @Composable que hace exactamente eso. En particular, esta anotación tiene los siguientes efectos:

  1. Todas las llamadas al constructor de una subclase Node dentro de la función se transformarán en una llamada emit con mutación de cualquiera de sus propiedades rodeadas de una llamada memo.
  2. Cualquier otra función marcada con @Composable que se llama en el cuerpo de la función está rodeada por un grupo. La clave de cada grupo se compilará como un número entero exclusivo de la ubicación de origen del sitio de la llamada.
  3. Todas las llamadas emit en el cuerpo de la función también están rodeadas por un grupo. Asimismo, la clave de cada grupo se compilará como un número entero que es único para la ubicación de origen del sitio de la llamada.
  4. La función recibe un Composer implícito como parámetro, en lugar de requerir que sea una función de extensión de Composer. Esto es posible porque el único código que usaba Composer ahora está implícito debido a (1) y (2).
  5. Esto significa que solo se puede invocar desde dentro de otra función @Composable. Esto es necesario para que (3) funcione, ya que tenemos que pasar el objeto Composer implícitamente en el punto de invocación.

Dados estos efectos, podemos ver que la función App anterior se convertiría en:

class State<T>(var value: T)

@Composable fun App(recompose: () -> Unit) {
val count = memo { State(0) }

Text("${count.value}")
Button(text = "Increment", onClick = { count.value++; recompose(); })
Button(text = "Reset", onClick = { count.value = 0; recompose(); })
}

Del mismo modo, la función TodoApp podría convertirse en:

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

@Composable fun TodoApp(items: List<TodoItem>) {
Stack(Orientation.Vertical) {
for (item in items) {
TodoItem(item)
}
}
Text("Total: ${items.size} items")
}

Esto simplifica considerablemente las cosas. El objetivo aquí es que, si bien la anotación @Composable implica cierta cantidad de abstracción en torno nuestras sus invocaciones, no debería alterar drásticamente el modelo mental que tenemos sobre lo que sucede al invocar una función.

Esto es análogo a lo que se requiere para implementar funciones suspend y corrutinas en Kotlin. Podríamos escribir el mismo código usando Futures, pero si podemos crear un modelo mental consistente en torno a lo que significa suspend, entonces podemos adaptarlo al lenguaje y reducir una cantidad significativa de repeticiones.

Con este mapeo de invocaciones @Composable en tiempo de ejecución que acabamos de crear, debemos tener una comprensión sólida de lo que realmente hace @Composable y algunas de las decisiones de diseño que Compose ha tomado para terminar donde está hoy.

Espero que estas publicaciones te hayan ayudado a comprender mejor el funcionamiento de Compose.

Te dejo por aquí los links a la parte 1 y a la parte 2, parte 3 y parte 4

Y como siempre, recordar que este post no deja de ser una traducción casi literal de: http://intelligiblebabble.com/compose-from-first-principles/

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

--

--