Google se compromete a impulsar la igualdad racial para las comunidades afrodescendientes. Obtén información al respecto.

Diseños en Compose

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:

Dos elementos de texto dibujados uno encima del otro, lo que hace que el texto sea ilegible

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")
  }
}

Dos elementos de texto organizados en un diseño de columna; por lo tanto, el texto es legible

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(verticalGravity = Alignment.CenterVertically) {
        Image(...)
        Column {
            Text(artist.name)
            Text(artist.lastSeenOnline)
        }
    }
}

Muestra un diseño más complejo, con un pequeño gráfico junto a una columna de elementos de texto

Usa Stack para colocar un elemento sobre otro.

Compara tres simples elementos de diseño que admiten composición: columna, fila y pila.

Estos 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(verticalGravity = Alignment.CenterVertically) { … }
        Spacer(Modifier.preferredSize(padding))
        Card(elevation = 4.dp) { … }
    }
}

Un diseño aún más complejo, que usa modificadores para cambiar la disposición de los gráficos y las áreas que responden a la entrada del usuario

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
    }
}

Toda el área, incluso el padding alrededor de los bordes, responde a los clics

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
    }
}

El padding alrededor del borde del diseño ya no responde a los clics

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(it))
    }
  }
}

Varios diseños similares en una columna desplazable

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(it, onSelected(it))
    }
  }
}

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:

Muestra las ranuras en una barra de la app, donde puedes agregar elementos de la IU

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.

Muestra un diseño que usa el objeto Scaffold para organizar los elementos de forma coherente con Material Design.

@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 Stack. ConstraintLayout es útil cuando se implementan diseños más grandes con requisitos de alineación más complejos.

En Compose, ConstraintLayout funciona con un DSL:

  • Las referencias se crean con createRefs() o createRefFor(), y cada elemento que admite composición en ConstraintLayout 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 elemento ConstraintLayout.

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.

Muestra un botón y un elemento de texto organizados en un ConstraintLayout

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:

  1. Pasa un ConstraintSet como parámetro a ConstraintLayout.
  2. Asigna referencias creadas en el ConstraintSet a los elementos que admiten composición con el modificador tag.
@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.tag("button")
            ) {
                Text("Button")
            }

            Text("Text", Modifier.tag("text"))
        }
    }
}

private fun decoupledConstraints(margin: Dp): ConstraintSet {
    return ConstraintSet2 {
        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:

Muestra la diferencia entre el padding normal de la IU, que establece el espacio entre los elementos, y el padding del texto, que establece el espacio desde una línea de base a la siguiente.

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:

  1. En el parámetro lambda measurable, llamas a measurable.measure(constraints) para medir el Text.
  2. 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.
  3. 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ón y 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))
  }
}

Varias vistas previas de elementos de texto; una muestra un padding común entre los elementos, y la otra el padding desde una línea de base a la siguiente

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,
    children: @Composable() () -> Unit
) {
    Layout(
        modifier = modifier,
        children = children
    ) { 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!")
    }
}

Texto alternativo

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 en Jetpack Compose.