Temas de Jetpack Compose

1. Introducción

En este codelab, aprenderás a usar las API de temas de Jetpack Compose para definir el estilo de tu app. Veremos la personalización de colores, formas y tipografías para que se usen de manera coherente en toda tu app y ofrezcan compatibilidad con varios temas, como el claro y el oscuro.

Qué aprenderás

En este codelab, aprenderás lo siguiente:

  • Una introducción a Material Design y su personalización para tu marca
  • Cómo Compose implementa el sistema de Material Design
  • Cómo definir y usar colores, tipografía y formas en toda tu app
  • Cómo aplicar estilos a los componentes
  • Cómo admitir temas claros y oscuros

Qué compilarás

En este codelab, diseñaremos una app para leer noticias. Comenzaremos con una app sin estilo y pondremos en práctica lo que aprendamos a fin de aplicar un tema en ella y admitir temas oscuros.

Imagen en la que se muestra Jetnews, una app para leer noticias, antes de aplicar estilos.

Imagen en la que se muestra Jetnews, una app para leer noticias, después de aplicar estilos.

Imagen en la que se muestra Jetnews, una app para leer noticias, con estilo de tema oscuro.

Antes: app sin estilo

Después: app con estilo

Después: tema oscuro

Requisitos previos

2. Cómo prepararte

En este paso, descargarás el código de una app para leer noticias simple a la que le aplicaremos un estilo.

Lo que necesitarás

Descarga el código

Si tienes Git instalado, simplemente puedes ejecutar el comando que se indica abajo. Para comprobarlo, escribe git --version en la terminal o la línea de comandos, y verifica que se ejecute correctamente.

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

Si no tienes Git, puedes hacer clic en el siguiente botón a fin de descargar todo el código de este codelab:

Descargar código fuente

Abre el proyecto en Android Studio, selecciona 'File > Import Project' y navega hasta el directorio ThemingCodelab.

El proyecto contiene tres paquetes principales:

  • com.codelab.theming.data: contiene clases de modelos y datos de muestra. No será necesario que edites este paquete durante este codelab.
  • com.codelab.theming.ui.start: es el punto de partida de este codelab. Debes realizar todos los cambios solicitados en este codelab en este paquete.
  • com.codelab.theming.ui.finish: es el estado final del codelab, para tu referencia.

Compila y ejecuta la app

La app tiene 2 configuraciones de ejecución que reflejan los estados de inicio y finalización del codelab. Si seleccionas la configuración y presionas el botón de ejecución, se implementará el código en tu dispositivo o emulador.

a43ae3c4fa75836e.png

La app también contiene Vistas previas del diseño de Compose. Cuando navegas a Home.kt en el paquete start o finish, y abres la vista de diseño, se muestran varias vistas previas que permiten iteraciones rápidas en tu código de IU:

758a285ad8a6cd51.png

3. Temas de Material

Jetpack Compose ofrece una implementación de Material Design, un sistema de diseño integral para crear interfaces digitales. Los componentes (botones, tarjetas, interruptores, etc.) de Material Design se basan en los temas de Material, que son una forma sistemática de personalizar Material Design a fin de reflejar mejor la marca de tu producto. Un tema de Material comprende atributos de color, tipografía y forma. La personalización se reflejará automáticamente en los componentes que uses para compilar tu app.

Es útil comprender los temas de Material para saber cómo aplicar temas en tus apps de Jetpack Compose. A continuación encontrarás una descripción breve de los conceptos. Si ya tienes conocimientos sobre los temas de Material, puedes omitir esta parte.

Color

Material Design define una cantidad de colores con nombre semántico que puedes usar en toda tu app.

62ccfe5761fd9eda.png

El primario es el color principal de la marca y el secundario se usa para dar algunos toques. Puedes proporcionar variantes más oscuras o más claras para lograr áreas contrastantes. Los colores de fondo y de superficie se usan para contenedores que tienen componentes que se alojan de forma nominal en una "superficie" de tu app. Material también define los colores "activados", es decir, aquellos que se usarán para el contenido además de uno de los colores con nombre. Por ejemplo, el texto de un contenedor de color "superficie" debe tener el color "en la superficie". Los componentes de Material están configurados para usar estos colores de tema; por ejemplo, de forma predeterminada, un botón de acción flotante tiene un color secondary, las tarjetas tienen un color surface, etcétera.

Cuando se definen colores con nombre, se pueden proporcionar paletas de colores alternativas, como un tema claro y uno oscuro:

1a9b78141ddfa87b.png

También te incentiva a definir una pequeña paleta de colores y a usarlos de manera coherente en tu app. La herramienta de color de Material puede ayudarte a elegir colores y crear una paleta de colores, incluso garantizando la accesibilidad de las combinaciones.

Tipografía

De manera similar, Material define varios estilos de tipo con nombres semánticos:

1d44de3ff2f7fd1c.png

Si bien no puedes variar los estilos de tipo por tema, el uso de una escala de tipo promueve la coherencia dentro de tu app. El hecho de proporcionar tus propias fuentes y otras personalizaciones de tipo se reflejará en los componentes de Material que uses en tu app; p. ej., las barras de la app usan el estilo h6 de forma predeterminada, los botones usan button, etc. La herramienta de generación de escalas de tipo de Material puede ayudarte a compilar tu escala de tipo.

Forma

Material admite el uso de formas de manera sistemática a fin de transmitir tu marca. Define 3 categorías: componentes pequeños, medianos y grandes. Cada uno de estos elementos puede definir una forma para usar y personalizar el estilo de la esquina (cortada o redondeada) y el tamaño.

886b811cc9cad18e.png

La personalización del tema de la forma se verá reflejada en varios componentes; p. ej., los botones y los campos de texto usan el tema pequeño, las tarjetas y los diálogos usan el tema mediano, y las hojas usan el tema grande de forma predeterminada. Aquí se ofrece una representación visual completa de las asignaciones de componentes a temas de formas. La herramienta de personalización de formas de Material puede ayudarte a generar un tema de forma.

Modelo de referencia

De forma predeterminada, Material usa un tema de "modelo de referencia", es decir, el esquema de colores púrpura, la escala de tipo Roboto y las formas ligeramente redondeadas que se ven en las imágenes anteriores. Si no especificas ni personalizas tu tema, los componentes usarán el tema del modelo de referencia.

4. Define tu tema

MaterialTheme

El elemento principal para implementar temas en Jetpack Compose es el elemento que admite composición MaterialTheme. Si colocas este elemento que admite composición en tu jerarquía de Compose, podrás especificar las personalizaciones de color, tipo y forma de todos los componentes que contiene. En la biblioteca, se define este elemento que admite composición de la siguiente manera:

@Composable
fun MaterialTheme(
    colors: Colors,
    typography: Typography,
    shapes: Shapes,
    content: @Composable () -> Unit
) { ...

Más adelante, puedes recuperar los parámetros que se pasaron a este elemento que admite composición mediante el object de MaterialTheme, que expone las propiedades de colors, typography y shapes. Hablaremos en detalle sobre cada uno de ellos más adelante.

Abre Home.kt y busca la función que admite composición Home; este es el punto de entrada principal a la app. Ten en cuenta que, si bien declaramos un MaterialTheme, no especificamos ningún parámetro, de manera que recibiremos el estilo predeterminado de "modelo de referencia":

@Composable
fun Home() {
  ...
  MaterialTheme {
    Scaffold(...

Creemos parámetros de color, tipo y forma a fin de implementar un tema para nuestra app.

Crea un tema

Para centralizar el estilo, te recomendamos que crees tu propio elemento que admite composición que una y configure un MaterialTheme. De esta manera, puedes especificar las personalizaciones de tu tema en un solo lugar y volver a usarlas fácilmente en muchos lugares, como en varias pantallas o @Preview. De ser necesario, puedes crear varios elementos que admiten composición para los temas. Por ejemplo, si quieres admitir estilos diferentes para distintas secciones de tu app.

En el paquete com.codelab.theming.ui.start.theme, crea un nuevo archivo llamado Theme.kt. Agrega una nueva función de componibilidad llamada JetnewsTheme, que acepta otros elementos componibles como contenido y une un MaterialTheme:

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(content = content)
}

Ahora, vuelve a Home.kt y reemplaza MaterialTheme por JetnewsTheme (e impórtalo):

-  MaterialTheme {
+  JetnewsTheme {
    ...

Aún no notarás ningún cambio en la @Preview de esta pantalla. Actualiza PostItemPreview y FeaturedPostPreview para unir su contenido con nuestro nuevo elemento que admite composición JetnewsTheme a fin de que las vistas previas usen nuestro nuevo tema:

@Preview("Featured Post")
@Composable
private fun FeaturedPostPreview() {
  val post = remember { PostRepo.getFeaturedPost() }
+ JetnewsTheme {
    FeaturedPost(post = post)
+ }
}

Colores

Esta es la paleta de colores que queremos implementar en nuestra app (por ahora, solo serán opciones claras, pero pronto volveremos a trabajar en ella para admitir el tema oscuro):

b2635ed3ec4bfc8f.png

Los colores en Compose se definen usando la clase Color. Hay varios constructores que te permiten especificar el color como ULong o mediante un canal de color por separado.

Crea un archivo nuevo Color.kt en tu paquete theme. Agrega los siguientes colores como propiedades públicas de nivel superior en este archivo:

val Red700 = Color(0xffdd0d3c)
val Red800 = Color(0xffd00036)
val Red900 = Color(0xffc20029)

Ahora que definimos los colores de nuestras apps, los uniremos en un objeto Colors que requiere el MaterialTheme asignando colores específicos a los colores con nombre de Material. Vuelve a Theme.kt y agrega lo siguiente:

private val LightColors = lightColors(
    primary = Red700,
    primaryVariant = Red900,
    onPrimary = Color.White,
    secondary = Red700,
    secondaryVariant = Red900,
    onSecondary = Color.White,
    error = Red800
)

Aquí usamos la función lightColors para compilar nuestros Colors. De esta manera, se brindan valores predeterminados confidenciales para que no tengamos que especificar todos los colores que conforman una paleta de colores de Material. Por ejemplo, puedes darte cuenta que no especificamos un color de background ni muchos de los colores "activados", sino que usaremos los valores predeterminados.

Usemos estos colores en nuestra app. Actualiza el elemento JetnewsTheme que admite composición para usar nuestro nuevo Colors:

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
+   colors = LightColors,
    content = content
  )
}

Abre Home.kt y actualiza la vista previa. Observa el nuevo esquema de colores reflejado en componentes como TopAppBar.

Tipografía

Esta es la escala de tipo que nos gustaría implementar en nuestra app:

54c420f78529b77d.png

En Compose, podemos definir objetos TextStyle para establecer la información necesaria y darle estilo a un texto. Una muestra de sus atributos:

data class TextStyle(
    val color: Color = Color.Unset,
    val fontSize: TextUnit = TextUnit.Inherit,
    val fontWeight: FontWeight? = null,
    val fontStyle: FontStyle? = null,
    val fontFamily: FontFamily? = null,
    val letterSpacing: TextUnit = TextUnit.Inherit,
    val background: Color = Color.Unset,
    val textAlign: TextAlign? = null,
    val textDirection: TextDirection? = null,
    val lineHeight: TextUnit = TextUnit.Inherit,
    ...
)

La escala de tipo deseada usa Montserrat para títulos y Domine para el texto del cuerpo. Los archivos de fuentes necesarios ya se agregaron a la carpeta res/fonts de tu proyecto.

Crea un archivo nuevo llamado Typography.kt en el paquete theme: Primero, definamos las FontFamily (que combinan las diferentes ponderaciones de cada Font):

private val Montserrat = FontFamily(
    Font(R.font.montserrat_regular),
    Font(R.font.montserrat_medium, FontWeight.W500),
    Font(R.font.montserrat_semibold, FontWeight.W600)
)

private val Domine = FontFamily(
    Font(R.font.domine_regular),
    Font(R.font.domine_bold, FontWeight.Bold)
)

Ahora crea un objeto Typography que acepte un elemento MaterialTheme y especifica TextStyle para cada estilo semántico en la escala:

val JetnewsTypography = Typography(
    h4 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 30.sp
    ),
    h5 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 24.sp
    ),
    h6 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 20.sp
    ),
    subtitle1 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    ),
    subtitle2 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    body1 = TextStyle(
        fontFamily = Domine,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    ),
    body2 = TextStyle(
        fontFamily = Montserrat,
        fontSize = 14.sp
    ),
    button = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    caption = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Normal,
        fontSize = 12.sp
    ),
    overline = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 12.sp
    )
)

Abre Theme.kt y actualiza el elemento JetnewsTheme que admite composición para usar nuestra nueva Typography:

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
+   typography = JetnewsTypography,
    content = content
  )
}

Abre Home.kt y actualiza la vista previa para ver la nueva tipografía vigente.

Formas

Queremos usar formas para expresar nuestra marca en la app. Usaremos una forma de esquina recortada en varios elementos:

9b60c78a78c61570.png

Compose ofrece las clases RoundedCornerShape y CutCornerShape, que puedes usar para definir el tema de tu forma.

Crea un nuevo archivo Shape.kt en el paquete theme y agrega lo siguiente:

val JetnewsShapes = Shapes(
    small = CutCornerShape(topStart = 8.dp),
    medium = CutCornerShape(topStart = 24.dp),
    large = RoundedCornerShape(8.dp)
)

Abre Theme.kt y actualiza el elemento JetnewsTheme que admite composición para usar estas Shapes:

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
    typography = JetnewsTypography,
+   shapes = JetnewsShapes,
    content = content
  )
}

Abre Home.kt y actualiza la vista previa para ver el modo en que el elemento Card que muestra la publicación destacada refleja el tema de forma que se acaba de aplicar.

Tema oscuro

Si admites el tema oscuro en tu app, no solo hace que se integre mejor en los dispositivos de los usuarios (que tienen un botón de activación del tema oscuro global a partir de Android 10), sino que también puede reducir el uso de energía y brindar compatibilidad con las necesidades de accesibilidad. Material ofrece ayuda de diseño para crear un tema oscuro. A continuación, se muestra una paleta de colores alternativa que nos gustaría implementar para el tema oscuro:

21768b33f0ccda5f.png

Abre Color.kt y agrega los siguientes colores:

val Red200 = Color(0xfff297a2)
val Red300 = Color(0xffea6d7e)

Ahora abre Theme.kt y agrega lo siguiente:

private val DarkColors = darkColors(
    primary = Red300,
    primaryVariant = Red700,
    onPrimary = Color.Black,
    secondary = Red300,
    onSecondary = Color.Black,
    error = Red200
)

Ahora actualiza JetnewsTheme:

@Composable
fun JetnewsTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
  content: @Composable () -> Unit
) {
  MaterialTheme(
+   colors = if (darkTheme) DarkColors else LightColors,
    typography = JetnewsTypography,
    shapes = JetnewsShapes,
    content = content
  )
}

Aquí, agregamos un nuevo parámetro para determinar si se debe usar un tema oscuro y cambiamos su configuración predeterminada para que consulte el parámetro de configuración global del dispositivo. Esto nos brinda un buen valor predeterminado, pero es fácil de anular si queremos que una pantalla en particular sea o no oscura en todo momento o que una @Preview tenga un tema oscuro.

Abre Home.kt y crea una nueva vista previa para el elemento FeaturedPost que admite composición, que lo muestra en tema oscuro:

@Preview("Featured Post • Dark")
@Composable
private fun FeaturedPostDarkPreview() {
    val post = remember { PostRepo.getFeaturedPost() }
    JetnewsTheme(darkTheme = true) {
        FeaturedPost(post = post)
    }
}

Actualiza el panel de vista previa para obtener la vista previa del tema oscuro.

84f93b209ce4fd46.png

5. Cómo trabajar con color

En el último paso, vimos cómo crear tu propio tema para configurar los colores, los estilos de tipo y las formas de tu app. Todos los componentes de Material emplean estas personalizaciones listas para usar. Por ejemplo, el elemento FloatingActionButton que admite composición usa de forma predeterminada el color secondary del tema, pero puedes establecer un color alternativo si especificas un valor diferente para este parámetro:

@Composable
fun FloatingActionButton(
  backgroundColor: Color = MaterialTheme.colors.secondary,
  ...
) {

Es posible que no siempre desees usar la configuración predeterminada. En esta sección, se muestra cómo trabajar con colores en tu app.

Colores sin procesar

Como vimos con anterioridad, Compose ofrece una clase Color. Puedes crearlos de manera local, conservarlos en un object, etcétera.

Surface(color = Color.LightGray) {
  Text(
    text = "Hard coded colors don't respond to theme changes :(",
    textColor = Color(0xffff00ff)
  )
}

Color tiene varios métodos útiles, como copy, lo que te permite crear un nuevo color con diferentes valores alfa, rojo, verde y azul.

Colores del tema

Un enfoque más flexible consiste en recuperar los colores de tu tema:

Surface(color = MaterialTheme.colors.primary)

Aquí, usamos el object de MaterialTheme cuya propiedad colors muestra los Colors establecidos en el elemento MaterialTheme que admite composición. Esto significa que podemos brindar compatibilidad con diferentes aspectos del diseño si ofrecemos distintos conjuntos de colores a nuestro tema, no es necesario tocar el código de la app. Por ejemplo, nuestra AppBar usa el color primary y el fondo de la pantalla es surface. El cambio de colores del tema se refleja en estos elementos que admiten composición:

b0b0ca02b52453a7.png

253ab041d7ea904e.png

Como cada color de nuestro tema son instancias de Color, también podemos derivar fácilmente los colores con el método copy:

val derivedColor = MaterialTheme.colors.onSurface.copy(alpha = 0.1f)

Aquí haremos una copia del color onSurface, pero con una opacidad del 10%. Este enfoque garantiza que los colores funcionen con diferentes temas, en lugar de codificar colores estáticos.

Colores de superficie y contenido

Muchos componentes aceptan un par de color y "colores de contenido":

Surface(
  color: Color = MaterialTheme.colors.surface,
  contentColor: Color = contentColorFor(color),
  ...

TopAppBar(
  backgroundColor: Color = MaterialTheme.colors.primarySurface,
  contentColor: Color = contentColorFor(backgroundColor),
  ...

Esto te permite no solo definir el color de un elemento que admite composición, sino también proporcionar un color predeterminado para ese "contenido" (es decir, los elementos que admiten composición). Muchos elementos componibles usan este color del contenido de forma predeterminada, p. ej., el color de Text o el tono de Icon. El método contentColorFor muestra el color "activado" adecuado de cualquier color de tema, p. ej., si estableces un fondo primary, mostrará onPrimary como color de contenido. Si configuras un color de fondo que no pertenece a ningún tema, debes proporcionar un color de contenido razonable.

Surface(color = MaterialTheme.colors.primary) {
  Text(...) // default text color is 'onPrimary'
}
Surface(color = MaterialTheme.colors.error) {
  Icon(...) // default tint is 'onError'
}

Puedes usar el CompositionLocal de LocalContentColor para recuperar el color que contrasta con el fondo actual:

BottomNavigationItem(
  unselectedContentColor = LocalContentColor.current ...

Cuando configures el color de cualquier elemento, opta por usar una Surface para hacerlo, ya que establece un valor CompositionLocal de color de contenido apropiado. Ten cuidado con las llamadas directas a Modifier.background que no establecen un color de contenido adecuado.

-Row(Modifier.background(MaterialTheme.colors.primary)) {
+Surface(color = MaterialTheme.colors.primary) {
+  Row(
...

Por el momento, nuestros componentes de Header siempre tienen un fondo Color.LightGray. Se ve bien con el tema claro, pero habrá un alto contraste con el fondo en el tema oscuro. Tampoco se especifica el color particular del texto, por lo que hereda el color del contenido actual que podría no contrastar con el fondo:

7329ac6ead5097eb.png

Tiene una solución. En el elemento Header que admite composición, en Home.kt, quita el modificador background que especifica el color hard-coded. En cambio, une el Text en una Surface con un color derivado del tema y especifica que el contenido debe tener el color primary:

+ Surface(
+   color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
+   contentColor = MaterialTheme.colors.primary,
+   modifier = modifier
+ ) {
  Text(
    text = text,
    modifier = Modifier
      .fillMaxWidth()
-     .background(Color.LightGray)
      .padding(horizontal = 16.dp, vertical = 8.dp)
  )
+ }

Versión alfa del contenido

Con frecuencia, deseamos enfatizar o minimizar la atención en el contenido para comunicar la importancia y ofrecer una jerarquía visual. Para ello, Material Design recomienda usar diferentes niveles de opacidad.

En Jetpack Compose, esto se implementa mediante LocalContentAlpha. Puedes especificar una versión alfa de contenido para establecer una jerarquía proporcionando un valor para este objeto CompositionLocal. Los elementos secundarios que admiten composición pueden usar este valor; por ejemplo, Text y Icon de forma predeterminada utilizan la combinación de LocalContentColor ajustada para utilizar LocalContentAlpha. Material especifica algunos valores alfa estándar (high, medium y disabled), que están modelados por el objeto ContentAlpha. Ten en cuenta que MaterialTheme cambia LocalContentAlpha a ContentAlpha.high de manera predeterminada.

// By default, both Icon & Text use the combination of LocalContentColor &
// LocalContentAlpha. De-emphasize content by setting a different content alpha
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
    Text(...)
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
    Icon(...)
    Text(...)
}

De esta manera, es fácil y coherente transmitir la importancia de los componentes.

Usaremos la versión alfa de contenido para aclarar la jerarquía de la información de la publicación destacada. En Home.kt, en el elemento PostMetadata que admite composición, haz el énfasis en los metadatos medium:

+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
  Text(
    text = text,
    modifier = modifier
  )
+ }

103ff62c71744935.png

Tema oscuro

Como vimos, para implementar temas oscuros en Compose, simplemente debes proporcionar diferentes conjuntos de colores y consultar colores a través del tema. Estas son algunas excepciones:

Puedes verificar si estás ejecutando en un tema claro de la siguiente manera:

val isLightTheme = MaterialTheme.colors.isLight

Este valor lo establecen las funciones del compilador lightColors o darkColors.

En Material, en los temas oscuros, las superficies con mayor elevación reciben superposiciones de elevación (se aclara su fondo). Esto se implementa automáticamente cuando se usa una paleta de colores oscuros:

Surface(
  elevation = 2.dp,
  color = MaterialTheme.colors.surface, // color will be adjusted for elevation
  ...

Podemos ver este comportamiento automático en nuestra app tanto en los componentes TopAppBar como en los Card que usamos. Tienen una elevación de 4 dp y 1 dp de forma predeterminada, por lo que sus fondos se aclaran automáticamente en el tema oscuro para comunicar mejor esta elevación:

cb8c617b8c151820.png

Material Design sugiere evitar grandes áreas de colores brillantes en el tema oscuro. Un patrón común consiste en colorear el color primary de un contenedor con el tema claro y el color surface con los temas oscuros. Muchos componentes usan esta estrategia de forma predeterminada, como las barras de la app y navegación inferior. Para facilitar la implementación, Colors ofrece un color primarySurface que brinda exactamente este comportamiento y que estos componentes usan de forma predeterminada.

En este momento, nuestra app establece la barra de la app con el color primary. Para seguir esta guía, puedes cambiarla a primarySurface o simplemente quitar este parámetro como predeterminado. En el elemento AppBar que admite composición, cambia el parámetro backgroundColor de TopAppBar:

@Composable
private fun AppBar() {
  TopAppBar(
    ...
-   backgroundColor = MaterialTheme.colors.primary
+   backgroundColor = MaterialTheme.colors.primarySurface
  )
}

6. Cómo trabajar con texto

Cuando trabajas con texto, usamos el elemento Text que admite composición a fin de mostrarlo, TextField y OutlinedTextField para la entrada, y TextStyle para aplicar un solo estilo. Podemos usar AnnotatedString y aplicar varios estilos al texto.

Como vimos con los colores, los componentes de Material que muestran texto tomarán nuestras personalizaciones de tipografía de temas:

Button(...) {
  Text("This text will use MaterialTheme.typography.button style by default")
}

Lograr este resultado es un poco más complejo que usar parámetros predeterminados como vimos con los colores. Esto se debe a que los componentes no suelen mostrar texto por sí mismos, sino que ofrecen "API de ranuras", lo que te permite pasar un elemento Text que admite composición. ¿Cómo establecen los componentes un estilo de tipografía para temas? Internamente, utilizan el elemento ProvideTextStyle que admite composición (que a su vez utiliza un elemento CompositionLocal) para establecer un objeto TextStyle "actual". De forma predeterminada, el elemento Text busca este estilo "actual" si no proporcionas un parámetro textStyle concreto.

Por ejemplo, de las clases Button y Text de Compose:

@Composable
fun Button(
    // many other parameters
    content: @Composable RowScope.() -> Unit
) {
  ...
  ProvideTextStyle(MaterialTheme.typography.button) { //set the "current" text style
    ...
    content()
  }
}

@Composable
fun Text(
    // many, many parameters
    style: TextStyle = LocalTextStyle.current // get the value set by ProvideTextStyle
) { ...

Estilos de texto de temas

Al igual que con los colores, es mejor recuperar los TextStyle del tema actual, lo que te alentará a usar un conjunto pequeño y coherente de estilos para que sean más fáciles de mantener. MaterialTheme.typography recupera la instancia de Typography establecida en tu elemento MaterialTheme que admite composición, lo que te permite usar los estilos que definiste:

Text(
  style = MaterialTheme.typography.subtitle2
)

Si necesitas personalizar un TextStyle, puedes usar copy y anular las propiedades (es solo una data class), o bien el elemento Text que admite composición acepta una serie de parámetros de estilo que se superpondrán en la parte superior de cualquier TextStyle:

Text(
  text = "Hello World",
  style = MaterialTheme.typography.body1.copy(
    background = MaterialTheme.colors.secondary
  )
)
Text(
  text = "Hello World",
  style = MaterialTheme.typography.subtitle2,
  fontSize = 22.sp // explicit size overrides the size in the style
)

En muchos lugares de nuestra app, se aplican automáticamente TextStyle de temas. Por ejemplo, la TopAppBar usa h6 como estilo para title, mientras que ListItem define el estilo del texto principal y secundario como subtitle1 y body2, respectivamente.

Aplicaremos los estilos tipográficos del tema al resto de nuestra app. Configura Header para que use subtitle2, y el texto en la FeaturedPost de modo que use h6 en el título y body2 en el autor y los metadatos:

@Composable
fun Header(...) {
  ...
  Text(
    text = text,
+   style = MaterialTheme.typography.subtitle2

45dbf11d6c1013a0.png

Varios estilos

Si necesitas aplicar varios estilos a un texto, puedes usar la clase AnnotatedString para aplicar lenguaje de marcado y agregar SpanStyle a un rango de texto. Puedes hacerlo de forma dinámica o usar la sintaxis de DSL para crear contenido:

val text = buildAnnotatedString {
  append("This is some unstyled text\n")
  withStyle(SpanStyle(color = Color.Red)) {
    append("Red text\n")
  }
  withStyle(SpanStyle(fontSize = 24.sp)) {
    append("Large text")
  }
}

Aplicaremos estilo a las etiquetas que describen cada publicación en nuestra app. Actualmente, usan el mismo estilo de texto que el resto de los metadatos. Usaremos el estilo de texto overline y un color de fondo para diferenciarlos. En el elemento PostMetadata que admite composición:

+ val tagStyle = MaterialTheme.typography.overline.toSpanStyle().copy(
+   background = MaterialTheme.colors.primary.copy(alpha = 0.1f)
+ )
post.tags.forEachIndexed { index, tag ->
  ...
+ withStyle(tagStyle) {
    append(" ${tag.toUpperCase()} ")
+ }
}

3f504aaa0a94599a.png

7. Cómo trabajar con formas

Al igual que el color y la tipografía, la configuración del tema de la forma se reflejará en los componentes de Material, por ejemplo, los objetos Button tomarán el conjunto de formas de los componentes pequeños:

@Composable
fun Button( ...
  shape: Shape = MaterialTheme.shapes.small
) {

Tal y como los colores, los componentes de Material usan parámetros predeterminados para que sea más simple comprobar qué categoría de forma usará un componente, o bien para proporcionar una alternativa. A fin de asignar todos los componentes a la categoría de la forma, consulta la documentación.

Ten en cuenta que algunos componentes usan formas de temas modificadas para adaptarse a su contexto. Por ejemplo, de forma predeterminada, TextField usa el tema de forma pequeña, pero aplica un tamaño de esquina cero a las esquinas inferiores:

@Composable
fun FilledTextField(
  // other parameters
  shape: Shape = MaterialTheme.shapes.small.copy(
    bottomStart = ZeroCornerSize, // overrides small theme style
    bottomEnd = ZeroCornerSize // overrides small theme style
  )
) {

1f5fa6cf1355e7a6.png

Formas del tema

Por supuesto, puedes usar formas por tu cuenta cuando crees tus propios componentes mediante elementos que admiten composición o Modifier que acepten formas, p. ej., Surface, Modifier.clip o Modifier.background, Modifier.border, etc.

@Composable
fun UserProfile(
  ...
  shape: Shape = MaterialTheme.shapes.medium
) {
  Surface(shape = shape) {
    ...
  }
}

Agregaremos temas de formas a la imagen que se muestra en PostItem. Aplicaremos la forma small del tema a la imagen con un elemento clip Modifier para cortar la esquina superior izquierda:

@Composable
fun PostItem(...) {
  ...
  Image(
    painter = painterResource(post.imageThumbId),
+   modifier = Modifier.clip(shape = MaterialTheme.shapes.small)
  )

2f989c7c1b8d9e63.png

8. Estilos de componente

Compose no ofrece una forma explícita de extraer el estilo de un componente, como los estilos de Android View o CSS. Dado que todos los componentes de Compose se crean en Kotlin, existen otras formas de lograr el mismo objetivo. En cambio, crea tu propia biblioteca de componentes personalizados y úsalos en tu app.

Ya lo hicimos en nuestra app:

@Composable
fun Header(
  text: String,
  modifier: Modifier = Modifier
) {
  Surface(
    color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
    contentColor = MaterialTheme.colors.primary,
    modifier = modifier.semantics { heading() }
  ) {
    Text(
      text = text,
      style = MaterialTheme.typography.subtitle2,
      modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 16.dp, vertical = 8.dp)
    )
  }
}

Básicamente, el elemento Header que admite composición es un Text con estilo que podemos usar en toda la app.

Vimos que todos los componentes están hechos de elementos básicos de nivel más bajo. Puedes usar estos mismos componentes básicos para personalizar los componentes de Material. Por ejemplo, vimos que Button usa el elemento ProvideTextStyle que admite composición con el fin de establecer un estilo de texto predeterminado para el contenido que se le pasa. Puedes usar el mismo mecanismo para configurar tu propio estilo de texto:

@Composable
fun LoginButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonConstants.defaultButtonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier
    ) {
        ProvideTextStyle(...) { // set our own text style
            content()
        }
    }
}

En este ejemplo, creamos nuestro propio "estilo" de LoginButton uniendo la clase Button estándar y especificando ciertas propiedades, como un backgroundColor y un estilo de texto diferentes.

Tampoco hay un concepto de diseño predeterminado, es decir, una manera de personalizar el aspecto predeterminado de un tipo de componente. Nuevamente, para lograrlo, puedes crear tu propio componente que une y personaliza un componente de la biblioteca. Supongamos que, por ejemplo, deseas personalizar la forma de todos los elementos Button de tu app, pero no quieres cambiar el tema de forma pequeña, lo que afectará a otros componentes (que no sean Button). Para lograrlo, crea tu propio elemento que admite composición y úsalo a lo largo del código:

@Composable
fun AcmeButton(
  // expose Button params consumers should be able to change
) {
  val acmeButtonShape: Shape = ...
  Button(
    shape = acmeButtonShape,
    // other params
  )
}

9. Felicitaciones

¡Felicitaciones! Completaste correctamente este codelab y diseñaste una app de Jetpack Compose.

Implementaste un tema de Material, personalizaste el color, la tipografía y las formas que se usan en toda la app para expresar tu marca y mejorar la coherencia. Agregaste compatibilidad con los temas claro y oscuro.

¿Qué sigue?

Consulta los otros codelabs sobre la ruta de aprendizaje de Compose:

Lecturas adicionales

Apps de ejemplo

  • Un búho muestra varios temas
  • Jetcaster demuestra temas dinámicos
  • Jetsnack demuestra la implementación de un sistema de diseño personalizado

Documentos de referencia