Diseños básicos en Compose

Organiza tus páginas con colecciones Guarda y categoriza el contenido según tus preferencias.

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 flexibles que admiten composición con las APIs de ranuras

Requisitos

  • Tener instalado Android Studio Chipmunk o una versión posterior
  • Tener experiencia con la sintaxis de Kotlin, incluidas las funciones de lambdas
  • Tener experiencia básica con Compose (si aún no lo hiciste, completa el codelab de los principios básicos de Jetpack Compose antes de comenzar este codelab)
  • Conocimientos básicos sobre lo que es un elemento componible y qué son los modificadores

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:

24ff9efa75f22198.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 para este codelab se puede encontrar en el repositorio de GitHub de android-compose-codelabs. Para clonarlo, ejecuta lo siguiente:

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

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

Analicemos con más detalle el diseño:

c31e78e48cc1f336.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

9a0f4be94a5a206c.png

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"

d9bf2ca5a0939959.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

29bed1f813622dc.png

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

cf1fe8b2d682bfca.png

Ahora que analizaste el diseño, puedes comenzar a implementar elementos componibles para cada parte identificada de la IU. Comienza con los elementos de nivel más bajo que admiten composición 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 que admite composición es la Barra de búsqueda. Volvamos a observar el diseño:

6b7c2f913d189b9a.png

Con base en 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.

6c6854661a89e995.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.

A fin de implementar la barra de búsqueda, usa un componente de Material llamado Campo de texto. La biblioteca de Compose Material contiene un elemento que admite composición 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.material.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 que admiten composición en este codelab.

Veamos la vista previa de este elemento que admite composición. Recuerda que puedes usar la funcionalidad de vista previa en Android Studio a fin de iterar rápidamente en elementos individuales que admiten composición. 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:

c2e1eec30f36bc72.png

Faltan algunas funciones. Primero, corrijamos el tamaño del elemento que admite composición 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 a fin de 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 que admite composición 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.material.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 que admite composición se vea como el diseño configurando los valores de los parámetros. Una vez más, este es el diseño de referencia:

6b7c2f913d189b9a.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.
  • Establece el color de fondo del campo de texto en el color surface de MaterialTheme. Puedes usar TextFieldDefaults.textFieldColors para anular colores específicos.
  • Agrega un texto de marcador de posición "Search" (puedes encontrarlo como el recurso de strings R.string.placeholder_search).

Cuando termines, el elemento que admite composición 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.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.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.textFieldColors(
           backgroundColor = MaterialTheme.colors.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 que admite composición 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, ese es solo el color de fondo.
  • Estableciste una altura mínima, no una altura fija. Este es el enfoque recomendado, de modo que el campo de texto pueda seguir creciendo en tamaño cuando, por ejemplo, el usuario aumente los tamaños de fuente en la configuración del sistema.

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:

29bed1f813622dc.png 9d11e16a8817686f.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 H3.

Para implementar este elemento que admite composición, 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 que admite composición 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 AlightYourBodyElement componible y hacerlos dinámicos.

Observa la vista previa de este elemento que admite composición:

b9686f83eb73c542.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.

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:

6576ed1e8b1cde30.png

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

En general, a fin de alinear los elementos que admiten composición 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 de modo que el elemento que admite composición 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 de tipografía correcto.
  • Actualiza el espaciado de la línea base del elemento de texto.

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

import androidx.compose.foundation.layout.paddingFromBaseline

@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),
           style = MaterialTheme.typography.h3,
           modifier = Modifier.paddingFromBaseline(
               top = 24.dp, bottom = 8.dp
           )
       )
   }
}

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

6. Tarjeta de colecciones favoritas: Elemento Surface de Material

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

71fcfc487ef8c02a.png

f2f4fb696389ba4f.png

En este caso, se proporciona el tamaño original del elemento que admite composición. Como puedes ver, el texto debe volver a tener un estilo H3.

Este contenedor tiene algún tipo de color de fondo, diferente del fondo de toda la pantalla. También tiene esquinas redondeadas. Dado que el diseñador no especificó un color, podemos suponer que el color se definirá según el tema. Para este tipo de contenedor, usamos el elemento Surface de Material, que admite composición.

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.material.Surface

@Composable
fun FavoriteCollectionCard(
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.small,
       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:

5584e459f9838f8b.png

A continuación, aplica las lecciones aprendidas en el paso anterior. Establece el tamaño de la imagen y recórtala en su contenedor. Configura el ancho de Row y alinea sus elementos secundarios de forma vertical. 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.small,
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
           modifier = Modifier.width(192.dp)
       ) {
           Image(
               painter = painterResource(R.drawable.fc2_nature_meditations),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(56.dp)
           )
           Text(
               text = stringResource(R.string.fc2_nature_meditations)
           )
       }
   }
}

La vista previa debería tener el siguiente aspecto:

e0afeb1658a6d82a.png

Para finalizar este elemento que admite composición, 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 texto de modo que use el estilo de tipografía correcto.
  • 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.small,
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
           modifier = Modifier.width(192.dp)
       ) {
           Image(
               painter = painterResource(drawable),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(56.dp)
           )
           Text(
               text = stringResource(text),
               style = MaterialTheme.typography.h3,
               modifier = Modifier.padding(horizontal = 16.dp)
           )
       }
   }
}

//..

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

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".

25089e1f3e5eab4e.gif

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

9d943fabcb5ae632.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 que admiten composición. 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, emitirás un elemento AlignYourBodyElement que admite composición que implementaste antes.

Observa cómo se muestra:

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

c29a8ee73f218868.png

También debes agregar padding a los costados del elemento LazyRow. En este caso, no es útil agrega run modificador de padding simple. Intenta agregar padding a LazyRow y observa su comportamiento:

6b3f390040e2b7fd.gif

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

A los efectos de mantener el mismo padding, pero también desplazar tu contenido dentro de los límites de tu lista superior sin recortarlo, todas las listas proporcionan un parámetro llamado contentPadding.

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

4378867d758590ae.gif

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:

e51beb5c00457902.png

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 que admite composición de modo que las celdas de cuadrícula tengan el tamaño y el espaciado correctos.

El resultado debería tener un aspecto similar al siguiente:

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

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:

2c0bc456d50bb078.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 HomeSection que admite composición de modo que reciba el título y el contenido de la ranura. También debes adaptar la vista previa asociada a fin de 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 = 0xFFF0EAE2)
@Composable
fun HomeSectionPreview() {
   MySootheTheme {
       HomeSection(R.string.align_your_body) {
           AlignYourBodyRow()
       }
   }
}

Puedes usar el parámetro content para la ranura del elemento que admite composición. 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:

d824b60e650deeb.png

El elemento de texto que admite composición necesita más información a fin de alinearse con el diseño. Actualízalo de modo que el texto tenga las siguientes características:

  • Se muestra en mayúsculas (sugerencia: puedes usar el método uppercase() de String).
  • Usa la tipografía H2.
  • Tiene paddings que se adaptan al diseño de revisión.

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

import java.util.*

@Composable
fun HomeSection(
   @StringRes title: Int,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   Column(modifier) {
       Text(
           text = stringResource(title).uppercase(Locale.getDefault()),
           style = MaterialTheme.typography.h2,
           modifier = Modifier
               .paddingFromBaseline(top = 40.dp, bottom = 8.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:

a535e10437e9df31.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 que admite composición 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.rememberScrollState
import androidx.compose.foundation.verticalScroll

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

Si deseas verificar el comportamiento de desplazamiento del elemento que admite composición, limita la altura de la vista previa y ejecútala en la vista previa interactiva:

@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2, 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 navegación inferior que permite al usuario alternar entre diferentes pantallas.

Primero, implementa este elemento de navegación inferior que admite composición y, luego, inclúyelo en tu app.

Echemos un vistazo al diseño:

2f42ad2b882885f8.png

Por fortuna, no tienes que implementar este elemento que admite composición desde cero. Puedes usar el elemento BottomNavigation que forma parte de la biblioteca de Compose Material. Dentro del elemento BottomNavigation que admite composición, puedes agregar un elemento BottomNavigationItem 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.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Spa

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

Así se ve esta implementación básica:

5bdb7729d75e1a72.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 backgroundColor. Para ello, puedes usar el color de fondo del tema de Material. Cuando estableces el color de fondo, el color de los íconos y los textos se adaptará automáticamente al color onBackground del tema. La solución final debería ser similar a la siguiente:

@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
   BottomNavigation(
       backgroundColor = MaterialTheme.colors.background,
       modifier = modifier
   ) {
       BottomNavigationItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_home))
           },
           selected = true,
           onClick = {}
       )
       BottomNavigationItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_profile))
           },
           selected = false,
           onClick = {}
       )
   }
}

12. App de MySoothe: Scaffold

Para este paso final, crea la implementación de pantalla completa, incluida la navegación inferior. Usa el elemento Scaffold de Material que admite composición. Scaffold te proporciona un elemento configurable de nivel superior que admite composición 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 que admite composición que creaste en el paso anterior.

Implementa el elemento MySootheApp que admite composición. 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 que admite composición.
  • Configura el contenido de modo que sea el elemento de la HomeScreen que admite composición.

El resultado final debería ser el siguiente:

import androidx.compose.material.Scaffold

@Composable
fun MySootheApp() {
   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 descargar la siguiente imagen y compararla con tu propia implementación de vista previa.

24ff9efa75f22198.png

13. 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 y los componentes de Material.

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: