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:

af26dcf59c74e995.png

94083c1e68a00295.png

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:

9825de962ae22604.png

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

a49160245fc819c3.png

Cuando se desglosa, 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"

5a60849913489fed.png

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

9f8a4d4b0a940571.png

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

a5299e3b1219971.png

Ahora que analizaste el diseño, puedes comenzar a implementar elementos componibles de 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:

907293b875cba19e.pngEn 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.

73b1b3df76ae5f07.png

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:

f9a7c6602c84f652.png

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:

9d72db0576c2b916.png

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.colors 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)
   )
}

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:

52f31d2e422d69e2.png

ea3d96db9dd6c062.png

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 para usar los parámetros proporcionados en el elemento AlignYourBodyElement componible y hacerlos dinámicos.

Observa la vista previa de este elemento componible:

71b61d3ff56b479e.png

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:

61809abae2e61520.png

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

5f17f07fcd0f1dc.png

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:

32b7f181d6c486a1.png

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 referencia del elemento de texto según el diagrama.

9b0505a98255508b.png

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.

94a07b90fbd0bde.png

6. Tarjeta de Favorite collection: 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:

52e72a19e67f646d.png

b5a11ff3afd99c09.png

En este caso, se proporciona el tamaño completo 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:

50b88836019b377.png

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.

85c43a6c27bafb4f.png

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:

26545aa897135433.png

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.

70fe9b9a5531b55.png

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 de "Align your body".

378dc391bf6f10f.gif

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

190d80ae866ad58d.png

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:

7fc50fa534a91430.png

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:

c1e6c40e30136af2.gif

Y para una Column:

df69881d07b064d0.gif

Además de estas disposiciones, también puedes usar el método Arrangement.spacedBy() a fin de agregar un espacio fijo entre cada elemento secundario que admite composición.

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í:

432399130e1b79c8.png

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:

1210a4da54a9d1bd.gif

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

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:

ee7c454636bd5939.gif

Podrías implementar esta sección de manera similar a la anterior si creas una LazyRow y permites que cada elemento contenga una 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:

4da2ecb238171bed.png

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

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:

fbe51e89e1e74b8d.png

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 que queremos implementar:

8d70500bc8e296cb.png

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:

37f9e54a3d56ba46.png

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

87c1159591a61aa.png

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:

3c2a284aa77735ca.png

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 tienen 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 Column o Row simple 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() }
}

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:

7fe4985abb54445a.png

Por fortuna, no tienes que implementar este elemento componible desde cero. Puedes usar el elemento NavigationBar componible 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.

3a5988f4e135ba58.png

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):

c78fee1cb0263bf3.png

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.

ef4f392d3ad1ecf7.png

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.

14ea5bb18785e4a0.png

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 NavigationRail componible, agregarás elementos NavigationRailItem para la pantalla principal y el perfil.

8b6b1e17e374ae56.png

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 = {}
           )
       }
   }
}

afaa7588f4081ffb.png

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 = {}
           )
       }
   }
}

efbaa88c691c106e.png

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

93883b6cebbbe6a5.png

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

e91a0bc068797eec.png

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. Para ello, usa la función calculateWindowSizeClass() para ver la configuración actual del teléfono.

346355a616f580a5.png

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.

d7f79fd7013d499a.png

94083c1e68a00295.png

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: