Diseños básicos en Compose

1. Introducción

Como es un kit de herramientas de IU, Compose facilita la implementación de los diseños de tu app. Tú describes el modo en que quieres que se vea la IU y Compose se encargará de dibujarla en la pantalla. En este codelab, aprenderás a escribir IU de Compose. Se supone que comprendes los conceptos que se enseñan en el codelab de aspectos básicos, así que primero asegúrate de completarlo. En ese codelab, aprendiste a implementar diseños simples con Surfaces, Rows y Columns. También aumentaste estos diseños con modificadores como padding, fillMaxWidth y size.

En este codelab, implementarás un diseño más realista y complejo, y durante el proceso aprenderás sobre varios elementos componibles listos para usar y modificadores. Después de finalizar este codelab, deberías poder transformar el diseño básico de una app en un código que funcione.

Este codelab no agrega ningún comportamiento real a la app. Si deseas conocer sobre el estado y la interacción, completa el codelab de Estado en Compose.

Para obtener más asistencia mientras realizas este codelab, consulta el siguiente código:

Qué aprenderás

En este codelab, aprenderás lo siguiente:

  • Cómo te ayudan los modificadores a aumentar tus elementos componibles
  • Cómo los componentes de diseño estándar, como Column y LazyRow, posicionan elementos secundarios componibles
  • Cómo los alineamientos y los arreglos cambian la posición de los elementos secundarios componibles en su elemento superior
  • Cómo los elementos de Material componibles, como Scaffold y Bottom Navigation, te ayudan a crear diseños comprensivos
  • Cómo compilar elementos componibles flexibles con las APIs de ranuras
  • Cómo compilar diseños para diferentes configuraciones de pantalla

Qué necesitarás

Qué compilarás

En este codelab, implementarás un diseño realista de apps basado en simulaciones proporcionadas por un diseñador. MySoothe es una app de bienestar que enumera varias formas de mejorar tu cuerpo y mente. Contiene una sección que enumera tus colecciones favoritas y una sección con ejercicios físicos. Así se ve la app:

Versión vertical de la app

Versión horizontal de la app

2. Cómo prepararte

En este paso, descargarás código que contiene temas y algunas configuraciones básicas.

Obtén el código

El código de este codelab se puede encontrar en el repositorio de GitHub de codelab-android-compose. Para clonarlo, ejecuta lo siguiente:

$ git clone https://github.com/android/codelab-android-compose

Como alternativa, puedes descargar dos archivos ZIP:

Consulta el código

El código descargado contiene el código de todos los codelabs de Compose disponibles. Para completar este codelab, abre el proyecto BasicLayoutsCodelab en Android Studio.

Te recomendamos que comiences con el código de la rama main y sigas el codelab paso a paso a tu propio ritmo.

3. Comienza con un plan

Para comenzar, implementaremos el diseño vertical de la app. Veamos esto con más detalle:

diseño vertical

Cuando se te solicita que implementes un diseño, una buena manera de comenzar es entender claramente su estructura. No comiences a programar de inmediato, en cambio, analiza el diseño en sí mismo. ¿Cómo puedes dividir la IU en varias partes reutilizables?

Pongamos esto a prueba con nuestro diseño. En el nivel de abstracción más alto, podemos dividir este diseño en las siguientes dos partes:

  • El contenido de la pantalla
  • La barra de navegación inferior

desglose del diseño de la app

Cuando se desglosa, el contenido de la pantalla contiene las siguientes tres subpartes:

  • La Barra de búsqueda
  • Una sección que se llama "Align your body"
  • Una sección que se llama "Favorite collections"

desglose del diseño de la app

Dentro de cada sección, también puedes ver algunos componentes de nivel inferior que se volvieron a usar, como los siguientes:

  • El elemento "Align your body" que se muestra en una fila desplazable horizontalmente

elemento "Align your body"

  • La tarjeta "Favorite collection" que se muestra en una cuadrícula desplazable horizontalmente

tarjeta de "Favorite collection"

Ahora que analizaste el diseño, puedes comenzar a implementar elementos componibles para cada parte identificada de la IU. Comienza con los elementos componibles de nivel más bajo y combínalos en otros más complejos. Al final del codelab, tu nueva app se verá como el diseño proporcionado.

4. Barra de búsqueda: Modificadores

El primer elemento que se transforma en uno componible es la barra de búsqueda. Volvamos a observar el diseño:

barra de búsqueda

En función de esta única captura de pantalla, sería bastante difícil implementar este diseño de manera perfecta en términos de píxeles. En general, un diseñador transmite más información sobre su trabajo. Puede darte acceso a su herramienta de diseño o compartir los llamados diseños de revisión. En este caso, nuestro diseñador transfirió los diseños de revisión, que puedes usar para identificar cualquier valor de tamaño. El diseño se muestra con una superposición de cuadrícula de 8 dp de modo que puedas ver con facilidad el espacio entre los elementos y a su alrededor. Además, se agregan algunos espaciados de forma explícita para aclarar determinados tamaños.

revisión de la barra de búsqueda

Puedes ver que la barra de búsqueda debe tener una altura de 56 píxeles independientes de la densidad. También debe llenar todo el ancho de su elemento superior.

Para implementar la barra de búsqueda, usa un componente de Material llamado Campo de texto. La biblioteca de Compose Material contiene un elemento componible llamado TextField, que es la implementación de este componente de Material.

Comienza con una implementación básica de TextField. En tu base de código, abre MainActivity.kt y busca el elemento SearchBar componible.

Dentro del elemento llamado SearchBar, escribe la implementación básica de TextField:

import androidx.compose.material3.TextField

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       modifier = modifier
   )
}

Estos son algunos puntos que deberías tener en cuenta:

  • Codificaste el valor del campo de texto, y la devolución de llamada onValueChange no hace nada. Como este es un codelab que se centra en el diseño, ignora cualquier tarea relacionada con el estado.
  • La función de componibilidad SearchBar acepta un parámetro modifier y lo pasa al TextField. Esta es una práctica recomendada de acuerdo con los lineamientos de Compose. Esto permite que el llamador del método modifique el aspecto del elemento que admite composición, lo que lo hace más flexible y reutilizable. Seguirás con esta práctica recomendada para todos los elementos componibles en este codelab.

Veamos la vista previa de este elemento componible. Recuerda que puedes usar la funcionalidad de vista previa en Android Studio para iterar rápidamente en elementos componibles individuales. MainActivity.kt contiene vistas previas de todos los elementos de este tipo que compilarás en este codelab. En este caso, el método SearchBarPreview procesa nuestro elemento SearchBar que admite composición, con cierto fondo y padding para darle un poco más de contexto. Luego de la implementación que acabas de agregar, debería verse de la siguiente manera:

vista previa de la barra de búsqueda

Faltan algunas funciones. Primero, corrijamos el tamaño del elemento componible con los modificadores.

Cuando escribes elementos componibles, usas modificadores para lo siguiente:

  • Cambiar el tamaño, el diseño, 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)

Cada elemento componible que llamas tiene un parámetro modifier que puedes configurar para adaptar su apariencia y comportamiento. Cuando estableces el modificador, puedes encadenar varios métodos de modificación para crear una adaptación más compleja.

En este caso, la barra de búsqueda debe medir al menos 56 dp y llenar el ancho de su elemento superior. A fin de encontrar los modificadores adecuados, consulta la lista de modificadores y la sección de Size (tamaño). Para la altura, puedes usar el modificador heightIn. De esta manera, se garantiza que el elemento componible tenga una altura mínima específica. Sin embargo, podrá aumentar de tamaño cuando, por ejemplo, el usuario aumente el tamaño de la fuente del sistema. Para el ancho, puedes usar el modificador fillMaxWidth. Este modificador hace que la barra de búsqueda ocupe todo el espacio horizontal de su elemento superior.

Actualiza el modificador de modo que coincida con el siguiente código:

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.material3.TextField

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       modifier = modifier
           .fillMaxWidth()
           .heightIn(min = 56.dp)
   )
}

En este caso, debido a que un modificador influye en el ancho y el otro en la altura, no importa el orden de estos modificadores.

También debes configurar algunos parámetros de TextField. Intenta hacer que el elemento componible se vea como el diseño configurando los valores de los parámetros. Una vez más, este es el diseño de referencia:

barra de búsqueda

Estos son los pasos que debes seguir para actualizar tu implementación:

  • Agrega el ícono de búsqueda. TextField contiene un parámetro leadingIcon que acepta otro elemento componible. Dentro, puedes establecer un Icon, que en nuestro caso debería ser el ícono Search. Asegúrate de usar la importación de Icon correcta de Compose.
  • Puedes usar TextFieldDefaults.textFieldColors para anular colores específicos. Establece los elementos focusedContainerColor y unfocusedContainerColor del campo de texto en el color surface de MaterialTheme.
  • Agrega un texto de marcador de posición "Search" (puedes encontrarlo como el recurso de cadenas R.string.placeholder_search).

Cuando termines, el elemento componible debería tener un aspecto similar al siguiente:

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.ui.res.stringResource
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       leadingIcon = {
           Icon(
               imageVector = Icons.Default.Search,
               contentDescription = null
           )
       },
       colors = TextFieldDefaults.colors(
           unfocusedContainerColor = MaterialTheme.colorScheme.surface,
           focusedContainerColor = MaterialTheme.colorScheme.surface
       ),
       placeholder = {
           Text(stringResource(R.string.placeholder_search))
       },
       modifier = modifier
           .fillMaxWidth()
           .heightIn(min = 56.dp)
   )
}

barra de búsqueda

Observa lo siguiente:

  • Agregaste un leadingIcon que muestra el ícono de búsqueda. Este ícono no necesita una descripción de contenido porque el marcador de posición del campo de texto ya describe su significado. Recuerda que una descripción de contenido en general se usa para fines de accesibilidad y proporciona al usuario de tu app una representación textual de una imagen o un ícono.
  • A fin de adaptar el color de fondo del campo de texto, configura la propiedad colors. En lugar de un parámetro separado para cada color, el elemento componible contiene un parámetro combinado. Aquí, pasas una copia de la clase de datos TextFieldDefaults, en la que solo actualizas los colores que son diferentes. En este caso, se trata solo de los colores unfocusedContainerColor y focusedContainerColor.

En este paso, observaste el modo en que puedes usar los modificadores y los parámetros componibles para cambiar su apariencia y estilo. Esto se aplica a los elementos componibles que proporcionan las bibliotecas de Compose y Material, y a los que escribes por tu cuenta. Siempre debes pensar en brindar parámetros para personalizar el elemento que escribes. También debes agregar una propiedad modifier de modo que la apariencia del elemento se pueda adaptar desde el exterior.

5. Align your body: Alineación

El siguiente elemento componible que implementarás es el elemento "Align your body". Veamos su diseño, incluido el diseño de revisión que se muestra a su lado:

componente "Align your body"

revisión de "Align your body"

El diseño de revisión ahora también contiene espaciados orientados a la línea base. Esta es la información que obtenemos de él:

  • La imagen debe tener 88 dp de altura.
  • El espaciado entre la línea base del texto y la imagen debe ser de 24 dp.
  • El espaciado entre la línea base y la parte inferior del elemento debe ser de 8 dp.
  • El texto debe tener un estilo tipográfico bodyMedium.

Para implementar este elemento componible, necesitas los elementos Image y Text del mismo tipo. Deben incluirse en un Column, por lo que se posicionan debajo de los demás.

Busca el elemento AlignYourBodyElement componible en tu código y actualiza su contenido con esta implementación básica:

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.ui.res.painterResource

@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null
       )
       Text(text = stringResource(R.string.ab1_inversions))
   }
}

Observa lo siguiente:

  • Debes establecer el contentDescription de la imagen como nulo, ya que esta es meramente decorativa. El texto debajo de la imagen es suficiente para describir el significado, por lo que no necesita una descripción adicional.
  • Estás usando imagen y texto hard-coded. En el siguiente paso, los moverás a fin de usar los parámetros proporcionados en el elemento AlignYourBodyElement componible y hacerlos dinámicos.

Observa la vista previa de este elemento componible:

vista previa de "Align your body"

Se deben realizar algunas mejoras. Lo más notable es que la imagen es demasiado grande y no tiene forma de círculo. Puedes adaptar el elemento Image componible con los modificadores size y clip, y el parámetro contentScale.

El modificador size adapta el elemento que admite composición de modo que se ajuste a un tamaño determinado, similar a los modificadores fillMaxWidth y heightIn que viste en el paso anterior. El modificador clip funciona de manera diferente y adapta la apariencia del elemento componible. Puedes establecerla en cualquier Shape y recortará el contenido del elemento a esa forma.

import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text(text = stringResource(R.string.ab1_inversions))
   }
}

Actualmente, tu diseño en la vista previa se ve de la siguiente manera:

vista previa de "Align your body"

La imagen también debe ajustarse correctamente. Para hacerlo, podemos usar el parámetro contentScale de Image. Existen varias opciones, en particular las siguientes:

vista previa del contenido de "Align your body"

En este caso, el tipo de recorte es el correcto para usar. Después de aplicar los modificadores y el parámetro, tu código debería verse de la siguiente manera:

import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null,
           contentScale = ContentScale.Crop,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text( text = stringResource(R.string.ab1_inversions) )
   }
}

Tu diseño debería verse de la siguiente manera:

vista previa de "Align your body"

Como siguiente paso, alinea el texto horizontalmente estableciendo la alineación de la Column.

En general, para alinear los elementos componibles dentro de un contenedor superior, debes establecer la alineación de ese contenedor superior. Por lo tanto, en lugar de indicarle al elemento secundario que se posicione en su elemento superior, debes indicarle al elemento superior el modo en que debe alinear sus elementos secundarios.

En una Column, tú decides el modo en que se deben alinear sus elementos secundarios de forma horizontal. Las opciones son las siguientes:

  • Start
  • CenterHorizontally
  • End

En una Row, establecerás la alineación vertical. Las opciones son similares a las de Column:

  • Top
  • CenterVertically
  • Bottom

En el caso de un elemento Box, combinarás la alineación horizontal y la vertical. Las opciones son las siguientes:

  • TopStart
  • TopCenter
  • TopEnd
  • CenterStart
  • Center
  • CenterEnd
  • BottomStart
  • BottomCenter
  • BottomEnd

Todos los elementos secundarios del contenedor seguirán este mismo patrón de alineación. Puedes anular el comportamiento de un solo elemento secundario si le agregas un modificador align.

A los efectos de este diseño, el texto debe estar centrado horizontalmente. Para ello, configura el elemento horizontalAlignment de Column para que se centre de forma horizontal:

import androidx.compose.ui.Alignment
@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       horizontalAlignment = Alignment.CenterHorizontally,
       modifier = modifier
   ) {
       Image(
           //..
       )
       Text(
           //..
       )
   }
}

Con estas partes implementadas, solo hay algunos cambios menores que debes hacer para que el elemento componible sea idéntico al diseño. Intenta implementarlos por tu cuenta o consulta el código final si tienes algún problema. Piensa en los siguientes pasos:

  • Haz que la imagen y el texto sean dinámicos. Pásalos como argumentos a la función de componibilidad. No olvides actualizar la vista previa correspondiente y pasar algunos datos hard-coded.
  • Actualiza el texto de modo que use el estilo tipográfico bodyMedium.
  • Actualiza el espaciado de la línea base del elemento de texto según el diagrama.

revisión de "Align your body"

Cuando termines de implementar estos pasos, tu código debería ser similar al siguiente:

import androidx.compose.foundation.layout.paddingFromBaseline
import androidx.compose.ui.Alignment
import androidx.compose.ui.layout.ContentScale

@Composable
fun AlignYourBodyElement(
   @DrawableRes drawable: Int,
   @StringRes text: Int,
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier,
       horizontalAlignment = Alignment.CenterHorizontally
   ) {
       Image(
           painter = painterResource(drawable),
           contentDescription = null,
           contentScale = ContentScale.Crop,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text(
           text = stringResource(text),
           modifier = Modifier.paddingFromBaseline(top = 24.dp, bottom = 8.dp),
           style = MaterialTheme.typography.bodyMedium
       )
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun AlignYourBodyElementPreview() {
   MySootheTheme {
       AlignYourBodyElement(
           text = R.string.ab1_inversions,
           drawable = R.drawable.ab1_inversions,
           modifier = Modifier.padding(8.dp)
       )
   }
}

Consulta AlignYourBodyElement en la pestaña Design.

vista previa de "Align your body"

6. Tarjeta de colecciones favoritas: Elemento Surface de Material

El siguiente elemento componible que implementaremos es similar al elemento "Align the body". Este es el diseño, incluida la revisión:

tarjeta de "Favorite collection"

revisión de la tarjeta de "Favorite collection"

En este caso, se proporciona el tamaño original del elemento componible. Puedes ver que el texto debe ser titleMedium.

Este contenedor usa surfaceVariant como su color de fondo, que es diferente del fondo de toda la pantalla. También tiene esquinas redondeadas. Especificamos estos elementos para la tarjeta de "Favorite collection" con el elemento Surface componible de Material.

Adapta el componente Surface a tus necesidades configurando sus parámetros y modificadores. En este caso, la superficie debe tener esquinas redondeadas. Puedes usar el parámetro shape para esto. En lugar de establecer la forma en un elemento Shape como en el caso de la imagen del paso anterior, usarás un valor que proviene de nuestro tema Material.

Veamos cómo se vería esto:

import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Surface

@Composable
fun FavoriteCollectionCard(
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.medium,
       modifier = modifier
   ) {
       Row {
           Image(
               painter = painterResource(R.drawable.fc2_nature_meditations),
               contentDescription = null
           )
           Text(text = stringResource(R.string.fc2_nature_meditations))
       }
   }
}

Veamos la vista previa de esta implementación:

vista previa de "Favorite collection"

A continuación, aplica las lecciones aprendidas en el paso anterior.

  • Configura el ancho de Row y alinea sus elementos secundarios de forma vertical.
  • Establece el tamaño de la imagen según el diagrama y recórtala en su contenedor.

revisión de "Favorite collection"

Intenta implementar estos cambios antes de ver el código de la solución.

El código debería verse de la siguiente manera:

import androidx.compose.foundation.layout.width

@Composable
fun FavoriteCollectionCard(
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.medium,
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
           modifier = Modifier.width(255.dp)
       ) {
           Image(
               painter = painterResource(R.drawable.fc2_nature_meditations),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(80.dp)
           )
           Text(
               text = stringResource(R.string.fc2_nature_meditations)
           )
       }
   }
}

La vista previa debería tener el siguiente aspecto:

vista previa de "Favorite collection"

Para finalizar este elemento componible, implementa los siguientes pasos:

  • Haz que la imagen y el texto sean dinámicos. Pásalos como argumentos a la función de componibilidad.
  • Actualiza el color a surfaceVariant.
  • Actualiza el texto de modo que use el estilo tipográfico titleMedium.
  • Actualiza el espaciado entre la imagen y el texto.

El resultado final debería ser similar al siguiente:

@Composable
fun FavoriteCollectionCard(
   @DrawableRes drawable: Int,
   @StringRes text: Int,
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.medium,
       color = MaterialTheme.colorScheme.surfaceVariant,
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
           modifier = Modifier.width(255.dp)
       ) {
           Image(
               painter = painterResource(drawable),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(80.dp)
           )
           Text(
               text = stringResource(text),
               style = MaterialTheme.typography.titleMedium,
               modifier = Modifier.padding(horizontal = 16.dp)
           )
       }
   }
}

//..

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun FavoriteCollectionCardPreview() {
   MySootheTheme {
       FavoriteCollectionCard(
           text = R.string.fc2_nature_meditations,
           drawable = R.drawable.fc2_nature_meditations,
           modifier = Modifier.padding(8.dp)
       )
   }
}

Consulta la vista previa de FavoriteCollectionCardPreview.

vista previa de "Favorite collection"

7. Fila de Align your body: Disposiciones

Ahora que creaste los elementos básicos componibles que se muestran en la pantalla, puedes comenzar a crear sus diferentes secciones en ella.

Comienza con la fila desplazable "Align your body".

elemento desplazable "Align your body"

A continuación, se muestra el diseño de revisión para este componente:

revisión de "Align your body"

Recuerda que un bloque de la cuadrícula representa 8 dp. En este diseño, hay 16 dp de espacio antes del primer elemento de la fila y después del último. Hay 8 dp de espaciado entre cada elemento.

En Compose, puedes implementar una fila desplazable como esta con el elemento LazyRow componible. La documentación sobre listas contiene mucha más información sobre las listas diferidas, como LazyRow y LazyColumn. En este codelab, basta con saber que LazyRow solo renderiza los elementos que se muestran en pantalla en lugar de todos los elementos al mismo tiempo, lo que ayuda a mantener el rendimiento de la app.

Comienza con una implementación básica de este LazyRow:

import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

Como puedes ver, los elementos secundarios de un LazyRow no son elementos componibles. En cambio, debes usar el DSL de lista diferida que proporciona métodos como item y items que emiten elementos componibles como los de lista. Para cada elemento en el componente alignYourBodyData proporcionado, emites un elemento AlignYourBodyElement componible que implementaste antes.

Observa cómo se muestra:

vista previa de "Align your body"

Todavía falta el espaciado que vimos en el diseño de revisión. Para implementarlo, deberás aprender sobre disposiciones.

En el paso anterior, conociste las alineaciones, que se usan para alinear los elementos secundarios de un contenedor en el eje cruzado. En el caso de una Column, el eje cruzado es el horizontal, mientras que, en el caso de una Row, el eje cruzado es el vertical.

Sin embargo, también podemos tomar una decisión sobre la forma de ubicar los elementos secundarios que admiten composición en el eje principal de un contenedor (el horizontal para una Row, el vertical para una Column).

En el caso de una Row, puedes elegir las siguientes disposiciones:

disposiciones en fila

Y para una Column:

disposiciones en columna

Además de estas disposiciones, también puedes usar el método Arrangement.spacedBy() para agregar un espacio fijo entre cada elemento componible secundario.

En el ejemplo, el método spacedBy es el que debes usar, ya que quieres colocar 8 dp de espacio entre cada elemento en LazyRow.

import androidx.compose.foundation.layout.Arrangement

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

Ahora, el diseño se ve así:

vista previa de "Align your body"

También debes agregar padding a los costados del elemento LazyRow. En este caso, no es útil agregar un modificador de padding simple. Intenta agregar padding a LazyRow y observa su comportamiento con la vista previa interactiva:

revisión de "Align your body"

Como puedes ver, cuando te desplazas, el primer y el último elemento visible están cortados en ambos lados de la pantalla.

Para mantener el mismo padding, pero también desplazar tu contenido dentro de los límites de la lista superior sin recortarlo, todas las listas proporcionan un parámetro a LazyRow llamado contentPadding y lo establece en 16.dp.

import androidx.compose.foundation.layout.PaddingValues

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       contentPadding = PaddingValues(horizontal = 16.dp),
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

Prueba la vista previa interactiva para ver la diferencia que hace el padding.

elemento desplazable "Align your body"

8. Cuadrícula de Favorite collections: Cuadrículas diferidas

La siguiente sección para implementar es la sección "Favorite collections" de la pantalla. En lugar de una sola fila, este elemento necesita una cuadrícula:

desplazamiento de "Favorite collections"

Podrías implementar esta sección de manera similar a la anterior si creas un LazyRow y permites que cada elemento contenga un Column con dos instancias de FavoriteCollectionCard. Sin embargo, en este paso usarás la LazyHorizontalGrid, que proporciona una asignación más atractiva de elementos a elementos de cuadrícula.

Comienza con una implementación simple de la cuadrícula con dos filas fijas:

import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items

@Composable
fun FavoriteCollectionsGrid(
   modifier: Modifier = Modifier
) {
   LazyHorizontalGrid(
       rows = GridCells.Fixed(2),
       modifier = modifier
   ) {
       items(favoriteCollectionsData) { item ->
           FavoriteCollectionCard(item.drawable, item.text)
       }
   }
}

Como puedes ver, simplemente reemplazaste el LazyRow del paso anterior por una LazyHorizontalGrid. Sin embargo, esto todavía no te dará el resultado correcto:

vista previa de "Favorite collections"

La cuadrícula ocupa tanto espacio como su elemento superior, lo que significa que las tarjetas de colecciones favoritas están demasiado estiradas.

Adapta el elemento componible de modo que se cumpla lo siguiente:

  • La cuadrícula tiene contentPadding horizontal de 16 dp.
  • La disposición horizontal y vertical tiene espacios de 16 dp.
  • La altura de la cuadrícula es de 168 dp.
  • El modificador de FavoriteCollectionCard especifica una altura de 80 dp.

El código final debería verse así:

@Composable
fun FavoriteCollectionsGrid(
   modifier: Modifier = Modifier
) {
   LazyHorizontalGrid(
       rows = GridCells.Fixed(2),
       contentPadding = PaddingValues(horizontal = 16.dp),
       horizontalArrangement = Arrangement.spacedBy(16.dp),
       verticalArrangement = Arrangement.spacedBy(16.dp),
       modifier = modifier.height(168.dp)
   ) {
       items(favoriteCollectionsData) { item ->
           FavoriteCollectionCard(item.drawable, item.text, Modifier.height(80.dp))
       }
   }
}

La vista previa debería verse de la siguiente manera:

vista previa de "Favorite collections"

9. Sección principal: APIs de ranuras

En la pantalla principal de MySoothe, hay varias secciones que siguen el mismo patrón. Cada una tiene un título, y parte del contenido varía según la sección. Este es el diseño de revisión que queremos implementar:

revisión de la sección de la pantalla principal

Como puedes ver, cada sección tiene un título y una ranura. El título tiene asociada determinada información como el estilo y el espaciado. La ranura se puede rellenar de forma dinámica con contenido diferente, según la sección.

Para implementar este contenedor flexible de secciones, usa las llamadas APIs de ranuras. Antes de implementar esto, lee la sección sobre diseños basados en ranuras en la documentación. Esto te ayudará a comprender lo que es un diseño basado en ranuras y el modo en que puedes usar las APIs de ranuras a fin de compilar diseños de este tipo.

Adapta el elemento componible HomeSection de modo que reciba el título y el contenido de la ranura. También debes adaptar la vista previa asociada para llamar a esta HomeSection con el título y el contenido de "Align your body":

@Composable
fun HomeSection(
   @StringRes title: Int,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   Column(modifier) {
       Text(stringResource(title))
       content()
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun HomeSectionPreview() {
   MySootheTheme {
       HomeSection(R.string.align_your_body) {
           AlignYourBodyRow()
       }
   }
}

Puedes usar el parámetro content para la ranura del elemento componible. De esta manera, cuando usas el elemento HomeSection, puedes usar una expresión lambda final a los efectos de llenar la ranura de contenido. Cuando un elemento componible proporciona varias ranuras para completar, puedes asignarles nombres significativos que representen su función en el contenedor más grande. Por ejemplo, la TopAppBar de Material proporciona las ranuras para el title, el navigationIcon y las actions.

Veamos cómo se ve la sección con esta implementación:

Vista previa de la sección de la pantalla principal

El elemento de texto componible necesita más información para alinearse con el diseño.

revisión de la sección de la pantalla principal

Actualízalo de modo que el texto tenga las siguientes características:

  • Usa la tipografía titleMedium.
  • El espaciado entre la línea base del texto y la parte superior es de 40 dp.
  • El espaciado entre la línea base y la parte inferior del elemento es de 16 dp.
  • El padding horizontal es de 16 dp.

La solución final debería ser similar a la siguiente:

@Composable
fun HomeSection(
   @StringRes title: Int,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   Column(modifier) {
       Text(
           text = stringResource(title),
           style = MaterialTheme.typography.titleMedium,
           modifier = Modifier
               .paddingFromBaseline(top = 40.dp, bottom = 16.dp)
               .padding(horizontal = 16.dp)
       )
       content()
   }
}

10. Pantalla principal: Desplazamiento

Ahora que creaste todos los componentes fundamentales, puedes combinarlos en una implementación de pantalla completa.

Este es el diseño que intentas implementar:

revisión de la sección de la pantalla principal

Simplemente estamos ubicando la barra de búsqueda y las dos secciones, una debajo de la otra. Debes agregar un espaciado de modo que todo encaje en el diseño. Un elemento componible que no usamos antes es Spacer, que nos ayuda a colocar espacio adicional dentro de nuestra Column. Si quieres, en cambio, establecer el padding de la Column, obtendrás el mismo comportamiento de corte que vimos antes en la cuadrícula de Favorite collections.

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
   Column(modifier) {
       Spacer(Modifier.height(16.dp))
       SearchBar(Modifier.padding(horizontal = 16.dp))
       HomeSection(title = R.string.align_your_body) {
           AlignYourBodyRow()
       }
       HomeSection(title = R.string.favorite_collections) {
           FavoriteCollectionsGrid()
       }
       Spacer(Modifier.height(16.dp))
   }
}

Aunque el diseño se adapta bien a la mayoría de los tamaños de dispositivos, es necesario que se pueda desplazar de forma vertical en caso de que el dispositivo no sea lo suficientemente alto, por ejemplo, cuando está en modo horizontal. Esto requiere que agregues un comportamiento de desplazamiento.

Como vimos antes, los diseños diferidos como LazyRow y LazyHorizontalGrid agregan automáticamente el comportamiento de desplazamiento. Sin embargo, no siempre necesitas un diseño diferido. En general, se usa un diseño diferido cuando se tiene muchos elementos en una lista o grandes conjuntos de datos para cargar, por lo que emitir todos los elementos a la vez tendría un costo de rendimiento y ralentizaría tu app. Cuando una lista tiene solo una cantidad limitada de elementos, puedes optar por usar una simple Column o Row y agregar el comportamiento de desplazamiento de forma manual. A tal fin, usa los modificadores verticalScroll o horizontalScroll. Estos requieren un elemento ScrollState, que contiene el estado actual del desplazamiento y que se usa para modificar el estado del desplazamiento desde el exterior. En este caso, no quieres modificar el estado de desplazamiento, por lo que solo debes crear una instancia de ScrollState persistente usando rememberScrollState.

El resultado final debería verse de la siguiente manera:

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
   Column(
       modifier
           .verticalScroll(rememberScrollState())
   ) {
       Spacer(Modifier.height(16.dp))
       SearchBar(Modifier.padding(horizontal = 16.dp))
       HomeSection(title = R.string.align_your_body) {
           AlignYourBodyRow()
       }
       HomeSection(title = R.string.favorite_collections) {
           FavoriteCollectionsGrid()
       }
       Spacer(Modifier.height(16.dp))
   }
}

Si deseas verificar el comportamiento de desplazamiento del elemento componible, limita la altura de la vista previa y ejecútala en la vista previa interactiva:

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE, heightDp = 180)
@Composable
fun ScreenContentPreview() {
   MySootheTheme { HomeScreen() }
}

desplazamiento del contenido de la pantalla

11. Navegación inferior: Material

Ahora que implementaste el contenido de la pantalla, podrás agregar la decoración de la ventana. En el caso de MySoothe, existe una barra de navegación que permite al usuario alternar entre diferentes pantallas.

Primero, implementa la barra de navegación componible y, luego, inclúyela en tu app.

Echemos un vistazo al diseño:

diseño de navegación inferior

Por fortuna, no tienes que implementar este elemento componible desde cero. Puedes usar el elemento componible NavigationBar que forma parte de la biblioteca de Compose Material. Dentro del elemento NavigationBar componible, puedes agregar un elemento NavigationBarItem o más. Luego, la biblioteca de Material les dará estilo automáticamente.

Comienza con una implementación básica de esta navegación inferior:

import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Spa

@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
   NavigationBar(
       modifier = modifier
   ) {
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(
                   text = stringResource(R.string.bottom_navigation_home)
               )
           },
           selected = true,
           onClick = {}
       )
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(
                   text = stringResource(R.string.bottom_navigation_profile)
               )
           },
           selected = false,
           onClick = {}
       )
   }
}

Este es el aspecto de la implementación básica: no hay mucho contraste entre el color del contenido y el color de la barra de navegación.

vista previa de la navegación inferior

Hay algunas adaptaciones de estilo que deberías hacer. En primer lugar, puedes actualizar el color de fondo de la navegación inferior si configuras el parámetro containerColor. Para ello, puedes usar el color surfaceVariant del tema de Material. La solución final debería ser similar a la siguiente:

@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
   NavigationBar(
       containerColor = MaterialTheme.colorScheme.surfaceVariant,
       modifier = modifier
   ) {
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_home))
           },
           selected = true,
           onClick = {}
       )
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_profile))
           },
           selected = false,
           onClick = {}
       )
   }
}

Ahora la barra de navegación debería verse así: observa que proporciona más contraste.

diseño de navegación inferior

12. App de MySoothe: Scaffold

Para este paso, crea la implementación de pantalla completa, incluida la navegación inferior. Usa el elemento Scaffold componible de Material. Scaffold te proporciona un elemento configurable componible de nivel superior a los efectos de usarlo en apps que implementan Material Design. Contiene ranuras para varios conceptos de Material, uno de los cuales es la barra inferior. En esta barra, puedes colocar el elemento de navegación inferior componible que creaste en el paso anterior.

Implementa el elemento MySootheAppPortrait() componible. Este es el elemento de nivel superior que usarás en tu app, por lo que debes hacer lo siguiente:

  • Aplica el tema MySootheTheme de Material.
  • Agrega el componente Scaffold.
  • Establece la barra inferior de modo que sea el elemento de la SootheBottomNavigation componible.
  • Configura el contenido de modo que sea el elemento de la HomeScreen componible.

El resultado final debería ser el siguiente:

import androidx.compose.material3.Scaffold

@Composable
fun MySootheAppPortrait() {
   MySootheTheme {
       Scaffold(
           bottomBar = { SootheBottomNavigation() }
       ) { padding ->
           HomeScreen(Modifier.padding(padding))
       }
   }
}

La implementación ya está completa. Si quieres verificar si tu versión se implementa de manera perfecta para píxeles, puedes comparar esta imagen con tu propia implementación de vista previa.

implementación de MySoothe

13. Riel de navegación: Material

Cuando crees diseños para apps, también debes tener en cuenta cómo se verá en varias configuraciones, incluido el modo horizontal de tu teléfono. A continuación, se muestra el diseño de la app en este modo. Observa cómo la navegación inferior se convierte en un riel a la izquierda del contenido de la pantalla.

diseño horizontal

Para implementar esto, usarás el elemento NavigationRail componible, que forma parte de la biblioteca de Compose Material y tiene una implementación similar a la de NavigationBar que se usó para crear la barra de navegación inferior. Dentro del elemento componible NavigationRail, agregarás elementos NavigationRailItem para la pantalla principal y el perfil.

diseño de navegación inferior

Comencemos con la implementación básica de un riel de navegación.

import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem

@Composable
private fun SootheNavigationRail(modifier: Modifier = Modifier) {
   NavigationRail(
   ) {
       Column(
       ) {
           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.Spa,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_home))
               },
               selected = true,
               onClick = {}
           )

           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.AccountCircle,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_profile))
               },
               selected = false,
               onClick = {}
           )
       }
   }
}

vista previa del riel de navegación

Hay algunas adaptaciones de estilo que deberías hacer.

  • Agrega 8 dp de padding al principio y al final del riel.
  • Para actualizar el color de fondo del riel de navegación, configura su parámetro containerColor con el color de fondo del tema de Material. Cuando estableces el color de fondo, el color de los íconos y los textos se adapta automáticamente al color onBackground del tema.
  • La columna debe rellenar la altura máxima.
  • Establece la disposición vertical de la columna como centrada.
  • Establece la alineación horizontal de la columna como centrada.
  • Agrega 8 dp de padding entre los dos íconos.

La solución final debería ser similar a la siguiente:

import androidx.compose.foundation.layout.fillMaxHeight

@Composable
private fun SootheNavigationRail(modifier: Modifier = Modifier) {
   NavigationRail(
       modifier = modifier.padding(start = 8.dp, end = 8.dp),
       containerColor = MaterialTheme.colorScheme.background,
   ) {
       Column(
           modifier = modifier.fillMaxHeight(),
           verticalArrangement = Arrangement.Center,
           horizontalAlignment = Alignment.CenterHorizontally
       ) {
           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.Spa,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_home))
               },
               selected = true,
               onClick = {}
           )
           Spacer(modifier = Modifier.height(8.dp))
           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.AccountCircle,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_profile))
               },
               selected = false,
               onClick = {}
           )
       }
   }
}

diseño del riel de navegación

Ahora, agregaremos el riel de navegación al diseño horizontal.

diseño horizontal

Para la versión vertical de la app, usaste Scaffold. Sin embargo, en la vista horizontal, usarás una fila y colocarás el contenido de la pantalla y del riel de navegación uno al lado del otro.

@Composable
fun MySootheAppLandscape() {
   MySootheTheme {
       Row {
           SootheNavigationRail()
           HomeScreen()
       }
   }
}

Cuando usaste Scaffold en la versión vertical, este componente también se encargó de configurar el color del contenido de fondo. Para establecer el color del riel de navegación, une la fila en una superficie y configúrala con el color de fondo.

@Composable
fun MySootheAppLandscape() {
   MySootheTheme {
       Surface(color = MaterialTheme.colorScheme.background) {
           Row {
               SootheNavigationRail()
               HomeScreen()
           }
       }
   }
}

vista previa horizontal

14. App de MySoothe: Tamaño de la ventana

Tienes una vista previa del modo horizontal que se ve genial. Sin embargo, si ejecutas la app en un dispositivo o emulador, y lo giras, este no te mostrará la versión horizontal. Esto se debe a que debemos indicarle a la app cuándo mostrar la configuración apropiada de la app. Para ello, usa la función calculateWindowSizeClass() para ver la configuración actual del teléfono.

diagrama del tamaño de la ventana

Hay tres anchos de clase de tamaño de ventana: compacto, medio y expandido. Cuando la app está en modo vertical, el ancho es el compacto y, cuando está en modo horizontal, el ancho es el expandido. A los fines de este codelab, no trabajarás con el ancho medio.

En el elemento componible MySootheApp, actualízalo para que tome el elemento WindowSizeClass del dispositivo. Si es compacto, pasa la versión vertical de la app. Si es expandido, pasa la versión horizontal de la app.

import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@Composable
fun MySootheApp(windowSize: WindowSizeClass) {
   when (windowSize.widthSizeClass) {
       WindowWidthSizeClass.Compact -> {
           MySootheAppPortrait()
       }
       WindowWidthSizeClass.Expanded -> {
           MySootheAppLandscape()
       }
   }
}

En setContent(), crea un valor llamado windowSizeClass establecido en calculateWindowSize() y pásalo a MySootheApp().

Dado que calculateWindowSize() aún es experimental, deberás habilitar la clase ExperimentalMaterial3WindowSizeClassApi.

import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass

class MainActivity : ComponentActivity() {
   @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           val windowSizeClass = calculateWindowSizeClass(this)
           MySootheApp(windowSizeClass)
       }
   }
}

Ahora, ejecuta la app en tu emulador o dispositivo y observa cómo cambia la pantalla tras una rotación.

Versión vertical de la app

Versión horizontal de la app

15. Felicitaciones

¡Felicitaciones! Completaste correctamente este codelab y aprendiste más sobre los diseños en Compose. A través de la implementación de un diseño real, aprendiste sobre los modificadores, las alineaciones, las disposiciones, los diseños diferidos, las APIs de ranuras, el desplazamiento, los componentes de Material y los diseños específicos.

Consulta los otros codelabs sobre la ruta de aprendizaje de Compose. No olvides consultar las muestras de código también.

Documentación

Para obtener más información y orientación sobre estos temas, consulta la siguiente documentación: