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 consistente en toda tu aplicación 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.
Antes: app sin estilo | Después: app con estilo | Después: tema oscuro |
Requisitos previos
- Tener experiencia con la sintaxis de Kotlin (incluidas las funciones de lambdas)
- Conocimientos básicos sobre Compose
- Conocimientos básicos sobre diseños de Compose (p. ej.,
Row
,Column
yModifier
)
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.
Qué 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/ThemingCodelabM2
Si no tienes Git, puedes hacer clic en el siguiente botón a fin de descargar todo el código de este codelab:
Abre el proyecto en Android Studio, selecciona 'File > Import Project' y navega hasta el directorio ThemingCodelabM2
.
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.
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:
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.
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:
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:
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, personalizando el estilo de la esquina (cortada o redondeada) y el tamaño.
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 componible 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 componible 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 componible con 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 de componibilidad 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):
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:
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
componible 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:
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
componible 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:
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
componible, 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.
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
componible 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
componible. 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 componibles:
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 componible, sino también proporcionar un color predeterminado para ese "contenido" (es decir, los elementos componibles). 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:
Tiene una solución. En el elemento Header
componible, 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 objeto PostMetadata
componible, haz el énfasis en los metadatos medium
:
+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(
text = text,
modifier = modifier
)
+ }
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:
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
componible, 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
componible 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
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
componible:
+ 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()} ")
+ }
}
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
)
) {
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)
)
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
componible 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