Jetpack Compose facilita mucho el diseño y la compilación de la IU de tu app. En este documento se explican algunos de los componentes fundamentales que proporciona Compose para ayudarte a diseñar los elementos de tu IU, y se muestra cómo compilar diseños más especializados cuando los necesites.
Las funciones que admiten composición son los componentes fundamentales de Compose. Una función de este tipo describe alguna parte de tu IU, toma alguna entrada y genera lo que se muestra en la pantalla. Para obtener más información sobre elementos que admiten composición, consulta la documentación sobre el modelo mental de Compose.
Una función que admite composición podría emitir varios elementos de la IU. Sin embargo, si no indicas cómo deben organizarse, es posible que Compose lo haga de una forma que no te agrade. Por ejemplo, este código genera dos elementos de texto:
@Composable
fun ArtistCard() {
Text("Alfred Sisley")
Text("3 minutes ago")
}
Si no tiene indicaciones sobre cómo quieres organizarlos, Compose los apila uno encima del otro y resultan ilegibles:
Compose proporciona una colección de diseños listos para usar que te ayudan a organizar los elementos de la IU y facilitan la definición de tus propios diseños más especializados.
Componentes de diseño estándar
En muchos casos, puedes usar los elementos de diseño estándar de Compose.
Usa Column
para colocar elementos en sentido vertical en la pantalla.
@Composable
fun ArtistCard() {
Column {
Text("Alfred Sisley")
Text("3 minutes ago")
}
}
Del mismo modo, usa Row
para colocar los elementos en sentido horizontal en la pantalla. Tanto Column
como Row
admiten la configuración de gravedad de los elementos que contienen.
@Composable
fun ArtistCard(artist: Artist) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image( /*...*/ )
Column {
Text(artist.name)
Text(artist.lastSeenOnline)
}
}
}
Usa Box
para colocar un elemento sobre otro.
Esos componentes fundamentales suelen ser todo lo que necesitas. Puedes escribir tu propia función que admita composición para combinar esos diseños en uno más elaborado que se adapte a tu app.
Cada uno de esos diseños básicos define su propia configuración de gravedad y especifica cómo se deben organizar los elementos. Para configurar esos elementos, usa modificadores.
Modificadores
Los modificadores te permiten ajustar la presentación de un elemento que admite composición. Por ejemplo, puedes hacer todo esto:
- Cambiar el comportamiento y el aspecto del elemento que admite composición
- Agregar información (p. ej., etiquetas de accesibilidad)
- Procesar entradas del usuario
- Agregar interacciones de nivel superior, (p. ej., hacer que un elemento sea apto para hacer clic, desplazable, arrastrable o ampliable)
Los modificadores son objetos de Kotlin estándar. Para crear uno, llama a una de las funciones de clase Modifier
. Puedes encadenar estas funciones para crear una composición:
@Composable
fun ArtistCard(
artist: Artist,
onClick: () -> Unit
) {
val padding = 16.dp
Column(
Modifier
.clickable(onClick = onClick)
.padding(padding)
.fillMaxWidth()
) {
Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
Spacer(Modifier.preferredSize(padding))
Card(elevation = 4.dp) { /*...*/ }
}
}
En el código anterior, observa distintas funciones de modificadores que se usan juntas.
clickable()
hace que un elemento que admite composición reaccione a la entrada del usuario.padding()
coloca espacio alrededor de un elemento.fillMaxWidth()
hace que el elemento que admite composición ocupe el ancho máximo que le otorga su elemento principal.preferredSize()
especifica el ancho y la altura preferidos de un elemento.
El orden de las funciones de los modificadores es importante. Como cada función realiza cambios en la Modifier
que muestra la función anterior, la secuencia afecta al resultado final. Veamos un ejemplo:
@Composable
fun ArtistCard(/*...*/) {
val padding = 16.dp
Column(
Modifier
.clickable(onClick = onClick)
.padding(padding)
.fillMaxWidth()
) {
// rest of the implementation
}
}
En el código anterior, se puede hacer clic en toda el área, incluso en el padding que la rodea, porque se aplicó el modificador padding
después del modificador clickable
. Si se aplicaran en orden inverso, el espacio que agrega padding
no reaccionaría a la entrada del usuario:
@Composable
fun ArtistCard(/*...*/) {
val padding = 16.dp
Column(
Modifier
.padding(padding)
.clickable(onClick = onClick)
.fillMaxWidth()
) {
// rest of the implementation
}
}
Diseños desplazables
Usa ScrollableRow
o ScrollableColumn
para que los elementos dentro de una Row
o Column
se desplacen.
@Composable
fun Feed(
feedItems: List<Artist>,
onSelected: (Artist) -> Unit
) {
ScrollableColumn(Modifier.fillMaxSize()) {
feedItems.forEach {
ArtistCard(it, onSelected)
}
}
}
Este enfoque funciona bien si los elementos que se deben mostrar son pocos, pero puede convertirse rápidamente en un problema en términos de rendimiento para grandes conjuntos de datos. Para mostrar solo una parte de los elementos visibles en la pantalla, usa LazyColumnFor
o LazyRowFor
.
@Composable
fun Feed(
feedItems: List<Artist>,
onSelected: (Artist) -> Unit
) {
Surface(Modifier.fillMaxSize()) {
LazyColumnFor(feedItems) { item ->
ArtistCard(item, onSelected)
}
}
}
Componentes de Material incorporados
El nivel más alto de abstracción de IU de Compose es Material Design. Compose proporciona una amplia variedad de elementos que admiten composición listos para usar que facilitan la compilación de la IU. Estos elementos incluyen Drawer
, FloatingActionButton
y TopAppBar
.
Los componentes de Material usan mucho las API de ranuras, un patrón que introduce Compose para agregar una capa de personalización sobre elementos que admiten composición. Las ranuras dejan un espacio vacío en la IU para que el desarrollador lo complete como quiera. Por ejemplo, estas son las ranuras que puedes personalizar en una TopAppBar
:
Los elementos que admiten composición suelen adoptar una expresión lambda que admite composición content
( content: @Composable
() -> Unit
). Las API con ranuras exponen varios parámetros de content
para usos específicos.
Por ejemplo, TopAppBar
te permite proporcionar el contenido para title
, navigationIcon
y actions
.
El elemento que admite composición de nivel superior de Material es Scaffold
.
Scaffold
te permite implementar una IU con la estructura básica de diseño de Material Design. Scaffold
proporciona ranuras para los componentes de nivel superior más comunes de Material, como TopAppBar
, BottomAppBar
, FloatingActionButton
y Drawer
. Si usas Scaffold
, es fácil asegurarte de que esos componentes estén bien posicionados y funcionen de forma correcta.
@Composable
fun HomeScreen( /*...*/ ) {
Scaffold (
drawerContent = { /*...*/ },
topBar = { /*...*/ },
bodyContent = { /*...*/ }
)
}
ConstraintLayout
ConstraintLayout
puede ayudar a posicionar elementos que admiten composición en relación con otros en la pantalla, y es una alternativa al uso de varios elementos Row
, Column
y Box
.
ConstraintLayout
es útil cuando se implementan diseños más grandes con requisitos de alineación más complejos.
En Compose, ConstraintLayout
funciona con una DSL:
- Las referencias se crean con
createRefs()
ocreateRefFor()
, y cada elemento que admite composición enConstraintLayout
debe tener una referencia asociada. - Las restricciones se proporcionan mediante el modificador
constrainAs()
, que toma la referencia como parámetro y te permite especificar sus restricciones en la expresión lambda del cuerpo. - Las restricciones se especifican mediante
linkTo()
o algún otro método útil. parent
es una referencia existente que se puede usar para especificar restricciones hacia el mismo elementoConstraintLayout
.
En este ejemplo vemos uno de esos elementos que usa un ConstraintLayout
:
@Composable
fun ConstraintLayoutContent() {
ConstraintLayout {
// Create references for the composables to constrain
val (button, text) = createRefs()
Button(
onClick = { /* Do something */ },
// Assign reference "button" to the Button composable
// and constrain it to the top of the ConstraintLayout
modifier = Modifier.constrainAs(button) {
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text("Button")
}
// Assign reference "text" to the Text composable
// and constrain it to the bottom of the Button composable
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 16.dp)
})
}
}
Este código restringe la parte superior del Button
al elemento principal, con un margen de 16.dp
, y un Text
a la parte inferior del Button
, también con un margen de 16.dp
.
A fin de obtener más ejemplos para trabajar con ConstraintLayout
, prueba el codelab de diseños.
API desacoplada
En el ejemplo de ConstraintLayout
, las restricciones se especifican de forma intercalada, con un modificador en el elemento que admite composición al que se aplican. Sin embargo, hay situaciones en las que es preferible desacoplar las restricciones de los diseños a los que se aplican. Por ejemplo, quizás querrías cambiar las restricciones en función de la configuración de la pantalla o agregar una animación entre dos conjuntos de restricciones.
En casos como esos, puedes usar ConstraintLayout
de otro modo:
- Pasa un
ConstraintSet
como parámetro aConstraintLayout
. - Asigna referencias creadas en el
ConstraintSet
a los elementos que admiten composición con el modificadortag
.
@Composable
fun DecoupledConstraintLayout() {
WithConstraints {
val constraints = if (minWidth < 600.dp) {
decoupledConstraints(margin = 16.dp) // Portrait constraints
} else {
decoupledConstraints(margin = 32.dp) // Landscape constraints
}
ConstraintLayout(constraints) {
Button(
onClick = { /* Do something */ },
modifier = Modifier.layoutId("button")
) {
Text("Button")
}
Text("Text", Modifier.layoutId("text"))
}
}
}
private fun decoupledConstraints(margin: Dp): ConstraintSet {
return ConstraintSet {
val button = createRefFor("button")
val text = createRefFor("text")
constrain(button) {
top.linkTo(parent.top, margin = margin)
}
constrain(text) {
top.linkTo(button.bottom, margin)
}
}
}
Luego, cuando necesites cambiar las restricciones, simplemente puedes pasar un ConstraintSet
diferente.
Diseños personalizados
Algunas funciones que admiten composición emiten una parte de la IU cuando se las invoca, que posteriormente se agrega a un árbol de la IU que se renderiza en la pantalla. Cada elemento de la IU tiene un elemento principal y, posiblemente, varios secundarios. Además, cada uno tiene una ubicación dentro de su elemento principal, que se indica como una posición (x, y), y un tamaño, que se especifica como width
y height
.
Se solicita a los elementos que definan sus propias restricciones que deben cumplirse.
Las restricciones restringen los valores mínimos y máximos de width
y height
de un elemento.
Si un elemento tiene elementos secundarios, el principal puede medir cada uno de los secundarios para ayudar a determinar su propio tamaño. Una vez que un elemento informa su propio tamaño, tiene la oportunidad de colocar sus elementos secundarios en relación consigo mismo, como se describe en detalle en Cómo crear diseños personalizados.
La medición de un solo paso es ideal en términos de rendimiento, y permite que Compose procese de manera eficiente los árboles detallados de la IU. Supongamos que un elemento de diseño midió dos veces a su elemento secundario, y el elemento secundario, a su vez, midió dos veces a su elemento secundario, y así sucesivamente. Un solo intento para implementar toda la IU requeriría muchísimo trabajo, lo que dificultaría lograr que tu app funcione bien. Sin embargo, hay momentos en los que realmente necesitas información adicional, más allá de lo que te pueda indicar una sola medición del elemento secundario. Existen enfoques que permiten procesar de manera eficiente una situación como esta, y se analizan en Cómo usar el modificador de diseño.
Cómo usar el modificador de diseño
Puedes usar el modificador layout
para cambiar la manera en la que se mide e implementa un elemento secundario. Layout
es una expresión lambda; sus parámetros incluyen el elemento que admite composición que puedes medir, que se pasó como measurable
, y las restricciones correspondientes a ese elemento, que se pasaron como constraints
. La mayoría de los modificadores layout
personalizados siguen este patrón:
fun Modifier.customLayoutModifier(...) =
Modifier.layout { measurable, constraints ->
...
})
Mostremos un Text
en la pantalla y controlemos la distancia desde la parte superior hasta la línea de base de la primera línea de texto. Para ello, usa el modificador layout
, que permite colocar el elemento que admite composición de forma manual en la pantalla. Este es el comportamiento deseado en el que el padding superior de Text
está configurado en 24.dp
:
Este es el código que genera ese espaciado:
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = Modifier.layout { measurable, constraints ->
// Measure the composable
val placeable = measurable.measure(constraints)
// Check the composable has a first baseline
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
val firstBaseline = placeable[FirstBaseline]
// Height of the composable with padding - first baseline
val placeableY = firstBaselineToTop.toIntPx() - firstBaseline
val height = placeable.height + placeableY
layout(placeable.width, height) {
// Where the composable gets placed
placeable.placeRelative(0, placeableY)
}
}
Esto es lo que sucede en este código:
- En el parámetro lambda
measurable
, llamas ameasurable.measure(constraints)
para medir elText
. - A fin de especificar el tamaño del elemento que admite composición, llama al método
layout(width, height)
, que también proporciona una expresión lambda que se usa para posicionar los elementos secundarios. En este caso, es la altura entre la última línea de base y el padding superior agregado. - Para posicionar los elementos secundarios en la pantalla, llama a
placeable.placeRelative(x, y)
. Si no se posicionan los elementos secundarios, no podrán verse. La posicióny
corresponde al padding superior, es decir, la posición de la primera línea de base del texto.placeRelative
duplica automáticamente la posición en contextos de derecha a izquierda.
Para verificar que funcione como se espera, usa este modificador sobre un Text
:
@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
MyApplicationTheme {
Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
}
}
@Preview
@Composable
fun TextWithNormalPaddingPreview() {
MyApplicationTheme {
Text("Hi there!", Modifier.padding(top = 32.dp))
}
}
Cómo crear diseños personalizados
El modificador layout
solo cambia un elemento que admite composición. Para controlar de forma manual varios elementos de este tipo, usa Layout
. Ese elemento te permite medir e implementar elementos secundarios de forma manual. Todos los diseños de nivel superior, como Column
y Row
, se compilan con el elemento Layout
.
Compilemos una implementación simple de Column
. La mayoría de los diseños personalizados siguen este patrón:
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable() () -> Unit
) {
Layout(
modifier = modifier,
children = content
) { measurables, constraints ->
// measure and position children given constraints logic here
}
}
Al igual que el modificador layout
, measurables
es la lista de elementos secundarios que deben medirse, y constraints
son las restricciones que se pasaron a Layout
. Siguiendo la misma lógica de antes, se puede implementar MyOwnColumn
de la siguiente manera:
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable() () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each children
measurable.measure(constraints)
}
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Track the y co-ord we have placed children up to
var yPosition = 0
// Place children in the parent layout
placeables.forEach { placeable ->
// Position item on the screen
placeable.placeRelative(x = 0, y = yPosition)
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
Los elementos secundarios que admiten composición obedecen a las restricciones de Layout
y se posicionan en función del objeto yPosition
del elemento anterior.
Así es cómo se usaría ese elemento personalizado:
@Composable
fun CallingComposable(modifier: Modifier = Modifier) {
MyOwnColumn(modifier.padding(8.dp)) {
Text("MyOwnColumn")
Text("places items")
Text("vertically.")
Text("We've done it by hand!")
}
}
Dirección del diseño
Cambia la dirección del diseño de un elemento que admite composición usando el ambiente LayoutDirection
.
Si quieres posicionar elementos que admiten composición de manera manual en la pantalla, la LayoutDirection
forma parte del LayoutScope
del modificador layout
o del elemento Layout
.
Cuando uses layoutDirection
, posiciona los elementos que admiten composición con place
. A diferencia del método placeRelative
, place
no cambia en función de la dirección de lectura (de izquierda a derecha o de derecha a izquierda).
Más información
Para obtener más información, prueba el codelab de diseños de Jetpack Compose.