Compose está construido alrededor de funciones componibles.
Introducción
Compose utiliza un modelo de interfaz de usuario (UI) declarativo que utiliza funciones que se componen de otras funciones que toman datos y emiten elementos de UI.
Estas funciones te permiten definir la UI de tu aplicación mediante código, describiendo qué aspecto debería tener y proporcionando las dependencias respecto a los datos, en lugar de tener que centrarte en la construcción de la UI (inicializando un elemento, añadiendo éste a un elemento superior, etc.).
Proyecto de soporte: https://gitlab.com/xtec/kotlin/compose/composition/
Proyecto
Instala Fleet
Crea un proyecto con Kotlin Multiplatform wizard:
- En el tab "New project", cambia el nombre del proyecto a
compose-composition
y el ID del proyecto adev.xtec
. - Selecciona las opciones Android y Desktop.
- Haz clic en el botón Download
- Descomprime el zip.
- Abre el proyecto con Fleet
Funciones componibles
Para crear una función componible sólo tienes que añadir la anotación @Composable
al nombre de la función:
@Composable
fun Greeting(name: String) {
Text("Hello $name")
}
Algunas cosas destacables sobre esta función:
-
La función está anotada con la anotación
@Composable
. Todas las funciones componibles deben tener esta anotación para que el compilador Compose sepa que esta función está destinada a convertir datos en UI. -
La función toma datos. Las funciones Composable pueden aceptar parámetros para que la función describa los componentes de la UI en función de dichos parámetros.
-
La función muestra texto en la UI. Lo hace llamando a la función componible
Text()
creando de esta manera una jerarquía de UI llamando a otras funciones componibles. -
La función no devuelve nada. Las funciones de composición que emiten UI no necesitan devolver nada porque sólo describen una parte de la UI, en ningún caso construyen widgets de UI.
-
Esta función es rápida, idempotente y libre de efectos secundarios. La función se comporta de la misma manera cuando se la llama varias veces con el mismo argumento y no utiliza otros valores como variables globales o llamadas a
random()
. La función describe la UI sin ningún efecto secundario, como modificar propiedades o variables globales.
En general, todas las funciones componibles deben escribirse con estas propiedades, por las razones que se explicarán en Recomposición.
Añade un elemento Text
Modifica el fichero composeApp/src/commonMain/kotlin/dev/xtec/App.kt
:
package dev.xtec
// ...
@Composable
fun App() {
Text("Hello World!")
}
App
és una función componbile que ahora utilitza la función componbile Text
que muestra el texto "Hello World!":
flowchart TD app([App]) --> text(["Text(Hello World!)"])
Ejecuta tu aplicación de escritorio des de la IDE:
O abre un terminal y ejectua gradlew
:
> .\gradlew composeApp:run
Define una función componible
A continuación crearás una función componible con la anotación @Composable
.
Define una función MessageCard
a la que se le pasa un nombre que utilizas para configurar un elemento de texto.
// ...
import androidx.compose.runtime.Composable
@Composable
fun MessageCard(name: String) {
Text(text = "Hello $name")
}
Obtén una vista previa de la función
La anotación @Preview
te permite obtener una vista previa de una función componibles con los parámetros que tu quieras sin tener que modificar el código principal de la apliación.
La anotación debe usarse en una función componible que no acepte parámetros. Por este motivo, no puedes obtener una vista previa de la función MessageCard
directamente.
En su lugar, crea una segunda función llamada PreviewMessageCard
, que llame MessageCard
con un parámetro apropiado.
Layouts
Los elementos de la interfaz de usuario son jerárquicos y están contenidos en otros elementos. En Compose, se crea una jerarquía de la interfaz de usuario llamando a funciones componibles desde otras funciones componibles.
Agregar varios textos
¡Hasta ahora has creado tu primera función componible y una vista previa! Para descubrir más funciones de Compose, crearás una pantalla de mensajería simple que contenga una lista de mensajes que se puede ampliar con algunas animaciones.
Comienza por enriquecer el mensaje componible mostrando el nombre de su autor y el contenido del mensaje. Primero debes cambiar el parámetro componible para que acepte un objeto Message en lugar de una String y agregar otro Text componible dentro del componible MessageCard. Asegúrese de actualizar también la vista previa.
// ...
@Composable
fun App() {
MaterialTheme {
MessageCard(Message("Eva", "En el pot petit hi ha la bona confitura"))
}
}
data class Message(val author: String, val body: String)
@Composable
fun MessageCard(msg: Message) {
Text(text = msg.author)
Text(text = msg.body)
}
Este código crea dos elementos de texto dentro de la vista de contenido. Sin embargo, dado que no ha proporcionado ninguna información sobre cómo organizarlos, los elementos de texto se dibujan uno sobre el otro, lo que hace que el texto sea ilegible.
Usando una columna
La función Column
permite organizar elementos verticalmente.
Añade una Column
a la función MessageCard
. Puedes utilizar Row
para organizar elementos horizontalmente y Box
para apilar elementos.
// ...
@Composable
fun MessageCard(msg: Message) {
Column {
Text(text = msg.author)
Text(text = msg.body)
}
}
Añade una imagen
A continuación añade una foto de perfil del remitente en MessageCard
.
Tienes que añadir las imagenes en el directorio composeApp/src/commonMain/composeResources/drawable
.
Tienes que ejecutar el proyecto para generar la clase Res
.
Añade un componible Row
para tener un diseño bien estructurado y un componible Image
dentro de él.
// ...
import org.jetbrains.compose.resources.painterResource
import compose.composeapp.generated.resources.Res
import compose.composeapp.generated.resources.eva
@Composable
fun MessageCard(msg: Message) {
Row {
Image(
painter = painterResource(Res.drawable.eva),
contentDescription = "Contact profile picture"
)
Column {
Text(text = msg.author)
Text(text = msg.body)
}
}
}
Configura el layout
El diseño de tu mensaje tiene la estructura correcta, pero sus elementos no están bien espaciados y la imagen es demasiado grande.
Para decorar o configurar un elemento componible, Compose utiliza modifiers. Estos permiten cambiar el tamaño, el diseño y la apariencia del elemento componible o agregar interacciones de alto nivel, como hacer que se pueda hacer clic en un elemento. Puedes encadenarlos para crear elementos componibles más completos. Usarás algunos de ellos para mejorar el diseño.
// ...
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
@Composable
fun MessageCard(msg: Message) {
// Add padding around our message
Row(modifier = Modifier.padding(all = 16.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = "Contact profile picture",
modifier = Modifier
// Set image size to 80 dp
.size(80.dp)
// Clip image to be shaped as a circle
.clip(CircleShape)
)
// Add a horizontal space between the image and the column
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(text = msg.author)
// Add a vertical space between the author and message texts
Spacer(modifier = Modifier.height(8.dp))
Text(text = msg.body)
}
}
}
Material Design
Compose está diseñado para admitir los principios de Material Design. Muchos de sus elementos de interfaz de usuario implementan Material Design de manera predeterminada. En esta lección, diseñarás tu aplicación con widgets de Material Design.
Modifica el fichero composeApp/build.gradle.kts
commonMain.dependencies {
// ...
implementation(compose.material3)
}
Compose ofrece una implementación de Material Design 3 y sus elementos de interfaz de usuario listos para usar. Mejorarás la apariencia de componible MessageCarde
mediante el estilo de Material Design.
Para comenzar, envuelva la función MessageCard
con el tema Material creado en tu proyecto, ComposeTheme
, así como un Surface
. H
// ...
@Composable
fun App() {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
MessageCard(Message("Eva", "En el pot petit hi ha la bona confitura"))
}
}
}
Material Design se basa en tres pilares: Color
, Typography
, y Shape
. Los irás añadiendo uno por uno.
Color
Use MaterialTheme.colors
to style with colors from the wrapped theme. You can use these values from the theme anywhere a color is needed. This example uses dynamic theming colors (defined by device preferences).
You can set dynamicColor
to false
in the MaterialTheme.kt
file to change this. TODO on??
Style the title and add a border to the image.
// ...
import androidx.compose.foundation.border
import androidx.compose.material3.MaterialTheme
@Composable
fun MessageCard(msg: Message) {
// Add padding around our message
Row(modifier = Modifier.padding(all = 16.dp)) {
Image(
painter = painterResource(Res.drawable.eva),
contentDescription = "Contact profile picture",
modifier = Modifier
// Set image size to 80 dp
.size(80.dp)
// Clip image to be shaped as a circle
.clip(CircleShape).border(3.dp, color = MaterialTheme.colors.primary, CircleShape)
)
// Add a horizontal space between the image and the column
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = msg.author,
color = MaterialTheme.colors.secondary
)
// Add a vertical space between the author and message texts
Spacer(modifier = Modifier.height(8.dp))
Text(text = msg.body)
}
}
}
Typography
Material Typography styles are available in the MaterialTheme
, just add them to the Text
composables.
// ...
@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 16.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = null,
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.border(3.dp, MaterialTheme.colors.primary, CircleShape)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = msg.author,
color = MaterialTheme.colors.secondary,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = msg.body,
style = MaterialTheme.typography.bodyLarge
)
}
}
}
Shape
With Shape
you can add the final touches. First, wrap the message body text around a Surface
composable. Doing so allows customizing the message body's shape and elevation. Padding is also added to the message for a better layout.
// ...
import androidx.compose.material3.Surface
@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 16.dp)) {
Image(
painter = painterResource(R.drawable.coyote),
contentDescription = "Contact profile picture",
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.border(
3.dp, MaterialTheme.colors.primary,
CircleShape
)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = msg.author, color = MaterialTheme.colors.secondary,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
Surface(shape = MaterialTheme.shapes.large, shadowElevation = 2.dp) {
Text(
text = msg.body,
modifier = Modifier.padding(all = 4.dp),
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
Lists and animations
Lists and animations are everywhere in apps. In this lesson, you will learn how Compose makes it easy to create lists and fun to add animations.
Create a list of messages
A chat with one message feels a bit lonely, so we are going to change the conversation to have more than one message.
val messages = listOf(
Message(
"Josep Pla",
"Antigament, el viatjar era un privilegi de gran senyor. Generalment, era la coronació normal dels estudis d'un home. Ara el viatjar s'ha generalitzat i democratitzat considerablement. Viatja molta gent"
),
Message(
"Josep Pla",
"Però, potser, les persones que viatgen per arrodonir i afermar la seva visió del món i dels homes són més rares avui que fa cent anys. "
),
Message(
"Josep Pla",
"En el nostre país hi ha tres pretextos essencials per a passar la frontera: la peregrinació a Lourdes, la lluna de mel i els negocis. Hom no pot tenir idea de la quantitat de gent del nostre país que ha estat a Lourdes. És incomptable. "
),
Message(
"Josep Pla",
"Fa trenta anys, les persones riques de Catalunya feien el viatge de noces a Madrid. Avui van a París o a Niça i de vegades a Itàlia. La lluna de mel, però, és un mal temps per veure res i per formar-se. No es poden pas fer dues coses importants a la vegada. El pitjor temps, potser, per a viatjar, de la vida, és la temporada de la lluna de mel."
),
)
You'll need to create a Conversation
function that will show multiple messages. For this use case, use Compose's LazyColumn
and LazyRow
. These composables render only the elements that are visible on screen, so they are designed to be very efficient for long lists.
In this code snippet, you can see that LazyColumn
has an items
child. It takes a List
as a parameter and its lambda receives a parameter we've named message
(we could have named it whatever we want) which is an instance of Message
. In short, this lambda is called for each item of the provided List
.
// ...
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@Composable
fun App() {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
Conversation(messages)
}
}
}
@Composable
fun Conversation(messages: List<Message>) {
LazyColumn {
items(messages) { message -> MessageCard(message) }
}
}
Animate messages while expanding
The conversation is getting more interesting. It's time to play with animations! You will add the ability to expand a message to show a longer one, animating both the content size and the background color. To store this local UI state, you need to keep track of whether a message has been expanded or not. To keep track of this state change, you have to use the functions remember
and mutableStateOf
.
Composable functions can store local state in memory by using remember
, and track changes to the value passed to mutableStateOf
. Composables (and their children) using this state will get redrawn automatically when the value is updated. This is called recomposition.
By using Compose's state APIs like remember
and mutableStateOf
, any changes to state automatically update the UI.
// ...
import androidx.compose.foundation.clickable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun MessageCard(msg: Message) {
// ...
// We keep track if the message is expanded or not in this
// variable
var isExpanded by remember { mutableStateOf(false) }
Column(modifier = Modifier.clickable{isExpanded = !isExpanded}) {
Text(
text = msg.author,
color = MaterialTheme.colors.secondary,
style = MaterialTheme.typography.subtitle2
)
// Add a vertical space between the author and message texts
Spacer(modifier = Modifier.height(8.dp))
Surface(shape = MaterialTheme.shapes.large, elevation = 2.dp) {
Text(
text = msg.body,
modifier = Modifier.padding(all = 4.dp),
// If the message is expanded, we display all its content
// otherwise we only display the first line
maxLines = if (isExpanded) Int.MAX_VALUE else 1,
style = MaterialTheme.typography.body1
)
}
}
}
}
Now you can change the background of the message content based on isExpanded
when we click on a message. You will use the clickable
modifier to handle click events on the composable.
Instead of just toggling the background color of the Surface, you will animate the background color by gradually modifying its value from MaterialTheme.colors.surface
to MaterialTheme.colors.primary
and vice versa.
To do so, you will use the animateColorAsState
function.
Lastly, you will use the animateContentSize
modifier to animate the message container size smoothly:
// ...
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize
@Composable
fun MessageCard(msg: Message) {
// Add padding around our message
Row(modifier = Modifier.padding(all = 16.dp)) {
Image(
painter = painterResource(Res.drawable.josep),
contentDescription = "Contact profile picture",
modifier = Modifier
// Set image size to 80 dp
.size(80.dp)
// Clip image to be shaped as a circle
.clip(CircleShape).border(3.dp, color = MaterialTheme.colors.primary, CircleShape)
)
// Add a horizontal space between the image and the column
Spacer(modifier = Modifier.width(16.dp))
// We keep track if the message is expanded or not in this
// variable
var isExpanded by remember { mutableStateOf(false) }
// surfaceColor will be updated gradually from one color to the other
val surfaceColor by animateColorAsState(
if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface
)
Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
Text(
text = msg.author,
color = MaterialTheme.colors.secondary,
style = MaterialTheme.typography.subtitle2
)
// Add a vertical space between the author and message texts
Spacer(modifier = Modifier.height(8.dp))
Surface(
shape = MaterialTheme.shapes.large,
elevation = 2.dp,
// surfaceColor color will be changing gradually from primary to surface
color = surfaceColor,
// animateContentSize will change the Surface size gradually
modifier = Modifier.animateContentSize().padding(2.dp)
) {
Text(
text = msg.body,
modifier = Modifier.padding(all = 4.dp),
// If the message is expanded, we display all its content
// otherwise we only display the first line
maxLines = if (isExpanded) Int.MAX_VALUE else 1,
style = MaterialTheme.typography.body1
)
}
}
}
}