Conceptos básicos sobre el diseño de Compose

Jetpack Compose facilita mucho el diseño y la compilación de la IU de tu app. Compose transforma el estado en elementos de la IU mediante lo siguiente:

  1. Composición de elementos
  2. Diseño de elementos
  3. Dibujo de elementos

Compose transforma el estado en IU a través de la composición, el diseño y el dibujo

Este documento se centra en el diseño de los elementos. Explica algunos de los componentes fundamentales que proporciona Compose a fin de ayudarte a diseñar los elementos de tu IU.

Objetivos de los diseños en Compose

La implementación de Jetpack Compose del sistema de diseño tiene dos objetivos principales:

Conceptos básicos de las funciones de componibilidad

Las funciones de componibilidad son los componentes fundamentales de Compose. Una función de este tipo emite una Unit que describe alguna parte de tu IU. La función toma alguna entrada y genera lo que se muestra en la pantalla. Para obtener más información sobre elementos componibles, consulta la documentación sobre el modelo mental de Compose.

Una función de componibilidad 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 ArtistCardColumn() {
    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 ArtistCardRow(artist: Artist) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(bitmap = artist.image, contentDescription = "Artist 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 Box para colocar un elemento sobre otro. Además, Box admite la configuración de la alineación específica de los elementos que contiene.

@Composable
fun ArtistAvatar(artist: Artist) {
    Box {
        Image(bitmap = artist.image, contentDescription = "Artist image")
        Icon(Icons.Filled.Check, contentDescription = "Check mark")
    }
}

Muestra dos elementos apilados uno sobre otro

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.

Compara tres elementos de diseño simples que admiten composición: columnas, filas y cuadros.

Para establecer la posición de los elementos secundarios dentro de un Row, configura los argumentos horizontalArrangement y verticalAlignment. Para un objeto Column, configura los argumentos verticalArrangement y horizontalAlignment:

@Composable
fun ArtistCardArrangement(artist: Artist) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.End
    ) {
        Image(bitmap = artist.image, contentDescription = "Artist image")
        Column { /*...*/ }
    }
}

Los elementos están alineados a la derecha

Modelo de diseño

En el modelo de diseño, el árbol de IU se presenta en un solo pase. Primero, se solicita que cada nodo se mida, luego, que mida los elementos secundarios de manera recurrente y que pase las restricciones de tamaño por el árbol hasta el elemento secundario. Luego, se establece el tamaño y la posición de los nodos de hoja, y las instrucciones sobre el tamaño y la posición resueltas se vuelven a pasar al árbol.

En pocas palabras, los elementos superiores realizan la medición antes que sus elementos secundarios, pero su tamaño y posición se establecen después de sus elementos secundarios.

Considera la siguiente función SearchResult.

@Composable
fun SearchResult() {
    Row {
        Image(
            // ...
        )
        Column {
            Text(
                // ...
            )
            Text(
                // ...
            )
        }
    }
}

Esta función muestra el siguiente árbol de IU.

SearchResult
  Row
    Image
    Column
      Text
      Text

En el ejemplo de SearchResult, el diseño del árbol de IU sigue este orden:

  1. Se solicita que el nodo raíz Row realice la medición.
  2. El nodo raíz Row le solicita a su primer elemento secundario, Image, que realice la medición.
  3. Image es un nodo de hoja (es decir, no tiene elementos secundarios), por lo que informa un tamaño y devuelve instrucciones sobre la posición.
  4. El nodo raíz Row le solicita al segundo elemento secundario, Column, que realice la medición.
  5. El nodo Column le solicita al primer elemento secundario Text que realice la medición.
  6. El primer nodo Text es un nodo de hoja, por lo que informa un tamaño y devuelve instrucciones sobre la posición.
  7. El nodo Column le solicita a su segundo elemento secundario Text que realice la medición.
  8. El segundo nodo Text es un nodo de hoja, por lo que informa un tamaño y devuelve instrucciones sobre la posición.
  9. Ahora que el nodo Column realizó la medición de sus elementos secundarios, estableció sus tamaños y sus posiciones, puede determinar su propio tamaño y posición.
  10. Ahora que el nodo raíz Row realizó la medición de sus elementos secundarios, estableció sus tamaños y sus posiciones, puede determinar su propio tamaño y posición.

Orden de medición, tamaño y posición en el árbol de IU de los resultados de la Búsqueda

Rendimiento

Compose logra un alto rendimiento midiendo los elementos secundarios solo una vez. 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 midió dos veces a su elemento secundario, y el elemento secundario, a su vez, midió dos veces cada uno de sus elementos secundarios, 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.

Si tu diseño necesita varias mediciones por algún motivo, Compose ofrece un sistema especial: mediciones intrínsecas. Puedes obtener más información sobre esta función en Medición intrínseca en los diseños de Compose.

Dado que la medición y la posición son subfases distintas del pase de diseño, cualquier cambio que solo afecte a la ubicación de los elementos, y a no la medición, se puede ejecutar por separado.

Cómo puedes usar modificadores en tus diseños

Como se indicó en Modificadores de Compose, puedes usar modificadores para decorar o aumentar tus elementos componibles. Los modificadores son esenciales para personalizar tu diseño. Por ejemplo, aquí encadenamos varios modificadores para personalizar ArtistCard:

@Composable
fun ArtistCardModifiers(
    artist: Artist,
    onClick: () -> Unit
) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
        Spacer(Modifier.size(padding))
        Card(
            elevation = CardDefaults.cardElevation(defaultElevation = 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 componible reaccione a la entrada del usuario y muestre una onda.
  • padding coloca espacio alrededor de un elemento.
  • fillMaxWidth hace que el elemento componible ocupe el ancho máximo que le otorga su elemento superior.
  • size() especifica el ancho y la altura preferidos de un elemento.

Diseños desplazables

Obtén más información sobre los diseños desplazables en la documentación sobre gestos de Compose.

Para obtener información sobre listas y listas Lazy, consulta la documentación sobre listas de Compose.

Diseños receptivos

Un diseño debe tener en cuenta diferentes orientaciones de pantalla y tamaños de factores de forma. Compose ofrece configuraciones integradas de forma inmediata para facilitar la adaptación de tus diseños componibles a diferentes configuraciones de pantalla.

Restricciones

Para conocer las restricciones que provienen del elemento superior y diseñar el diseño según corresponda, puedes usar BoxWithConstraints. Las restricciones de medición se pueden encontrar en el alcance de la lambda de contenido. Puedes usar estas restricciones de medición a fin de componer diferentes diseños para distintas configuraciones de pantalla:

@Composable
fun WithConstraintsComposable() {
    BoxWithConstraints {
        Text("My minHeight is $minHeight while my maxWidth is $maxWidth")
    }
}

Diseños basados en ranuras

Compose proporciona una gran variedad de elementos componibles basados en Material Design con la dependencia androidx.compose.material:material (que se incluye cuando se crea un proyecto de Compose en Android Studio) para facilitar la compilación de 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 componibles. Este enfoque hace que los componentes sean más flexibles, ya que aceptan un elemento secundario que puede configurarse automáticamente, en lugar de tener que exponer cada parámetro de configuración del elemento secundario. 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:

Diagrama que muestra los espacios disponibles en la barra de la app de componentes de Material

Los elementos componibles suelen adoptar una expresión lambda content componible (content: @Composable () -> Unit). Las APIs 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.

Por ejemplo, Scaffold te permite implementar una IU con la estructura básica de diseño de Material Design. Scaffold proporciona ranuras para los componentes de Material de nivel superior más comunes, 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.

La app de muestra de JetNews, que usa Scaffold para posicionar varios elementos

@Composable
fun HomeScreen(/*...*/) {
    ModalNavigationDrawer(drawerContent = { /* ... */ }) {
        Scaffold(
            topBar = { /*...*/ }
        ) { contentPadding ->
            // ...
        }
    }
}