Temas de Material en Compose

Jetpack Compose ofrece una implementación de Material Design, un sistema de diseño integral para crear interfaces digitales. Los componentes de Material Design (botones, tarjetas, interruptores, etc.) se compilan sobre la aplicación de temas de Material, que es una forma sistemática de personalizar Material Design para que refleje mejor la marca de tu producto. Un tema de Material comprende atributos de color, tipografía y forma. Cuando personalizas esos atributos, tus cambios se reflejan automáticamente en los componentes que usas para compilar tu app.

Jetpack Compose implementa esos conceptos con el elemento MaterialTheme que admite composición:

MaterialTheme(
    colors = …,
    typography = …,
    shapes = …
) {
    // app content
}

Configura los parámetros que pasas a MaterialTheme para aplicar un tema a tu aplicación.

Dos capturas de pantalla que contrastan. La primera usa el estilo predeterminado de MaterialTheme, mientras que la segunda usa un estilo modificado.

Figura 1: La primera captura de pantalla muestra una app que no configura MaterialTheme y, por lo tanto, usa el estilo predeterminado. La segunda, muestra una app que pasa parámetros a MaterialTheme para personalizar el estilo.

Color

Los colores se modelan en Compose con la clase Color, una clase simple que retiene datos.

val Red = Color(0xffff0000)
val Blue = Color(red = 0f, green = 0f, blue = 1f)

Si bien puedes organizarlos como te guste (como constantes de nivel superior, dentro de un singleton o intercalado definido), te recomendamos que especifiques los colores en tu tema y los recuperes desde allí. Este enfoque permite admitir, con facilidad, el tema oscuro y los temas anidados.

Ejemplo de paleta de colores de un tema

Figura 2: Sistema de colores de Material.

Compose proporciona la clase Colors para modelar el sistema de colores de Material. Colors ofrece funciones de compilador para crear conjuntos de colores claros u oscuros:

private val Yellow200 = Color(0xffffeb46)
private val Blue200 = Color(0xff91a4fc)
// ...

private val DarkColors = darkColors(
    primary = Yellow200,
    secondary = Blue200,
    // ...
)
private val LightColors = lightColors(
    primary = Yellow500,
    primaryVariant = Yellow400,
    secondary = Blue700,
    // ...
)

Una vez que hayas definido tus Colors, puedes pasarlos a un MaterialTheme:

MaterialTheme(
    colors = if (darkTheme) DarkColors else LightColors
) {
    // app content
}

Cómo usar colores de tema

Puedes recuperar los Colors proporcionados al elemento que admite compilación de MaterialTheme con MaterialTheme.colors.

Text(
    text = "Hello theming",
    color = MaterialTheme.colors.primary
)

Color de contenido y superficie

Muchos componentes aceptan un par de color y color del 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 su contenido, es decir, los elementos que admiten composición que este contiene. Muchos elementos que admiten composición usan este color de contenido de forma predeterminada. Por ejemplo, Text basa su color en el color de contenido del elemento superior, y Icon usa ese color para establecer su tono.

Dos ejemplos del mismo banner, con colores distintos

Figura 3: Cuando se establecen diferentes colores de fondo, se producen distintos colores de texto y de ícono.

El método contentColorFor() recupera el color adecuado para mostrar "arriba" de cualquier color del tema. Por ejemplo, si configuras un color de fondo primaryen Surface, se usará esta función para establecer onPrimary como color de contenido. Si configuras un color de fondo que no pertenece a ningún tema, también debes especificar un color de contenido apropiado. Usa LocalContentColor a fin de recuperar el color de contenido preferido para el fondo actual, en una posición determinada en la jerarquía.

Versión alfa de contenido

Muchas veces quieres variar la forma en que destacas el contenido para comunicar la importancia y ofrecer una jerarquía visual. En las recomendaciones de legibilidad de texto de Material Design, se recomienda usar diferentes niveles de opacidad para transmitir diferentes niveles de importancia.

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 anidados que admiten composición pueden usar este valor para aplicar el tratamiento alfa a su contenido. Por ejemplo, Text y Icon utilizan la combinación de LocalContentColor configurada como LocalContentAlpha de forma predeterminada. Material especifica algunos valores alfa estándar (high, medium y disabled), que están modelados por el objeto ContentAlpha.

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

Para obtener más información sobre CompositionLocal, consulta la Guía de datos de permisos local con CompositionLocal.

Captura de pantalla del título de un artículo en el que se muestran diferentes niveles de énfasis

Figura 4: Aplica diferentes niveles de énfasis en el texto para comunicar visualmente la jerarquía de la información. La primera línea de texto es el título, incluye la información más importante y, por lo tanto, usa ContentAlpha.high. La segunda línea incluye metadatos menos importantes y, por lo tanto, usa ContentAlpha.medium.

Tema oscuro

En Compose, debes proporcionar diferentes conjuntos de Colors al elemento MaterialTheme que admite composición para implementar temas claros y oscuros:

@Composable
fun MyTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    MaterialTheme(
        colors = if (darkTheme) DarkColors else LightColors,
        /*...*/
        content = content
    )
}

En este ejemplo, MaterialTheme se une a su propia función que admite composición y que acepta un parámetro que especifica si se debe usar un tema oscuro o no. En este caso, la función consulta a la configuración de tema del dispositivo para obtener el valor predeterminado de darkTheme.

Puedes usar código como el siguiente para verificar si los objetos Colors actuales son claros u oscuros:

val isLightTheme = MaterialTheme.colors.isLight
Icon(
    painterResource(
        id = if (isLightTheme) {
          R.drawable.ic_sun_24dp
        } else {
          R.drawable.ic_moon_24dp
        }
    ),
    contentDescription = "Theme"
)

Superposiciones de elevación

En Material, las superficies en temas oscuros con mayor elevación reciben superposiciones de elevación, lo que aclara su fondo. Cuanto más alta sea la elevación de una superficie (se acerque más a una fuente de luz implícita), más se aclarará la superficie.

A esas superposiciones las aplica, automáticamente, el elemento Surface que admite composición cuando se usan colores oscuros y para cualquier otro elemento de Material que admite composición y que use una superficie:

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

Captura de pantalla de una app que muestra la diferencia sutil de los colores que se usan para los elementos que se encuentran a diferentes niveles de elevación

Figura 5: Las tarjetas y la navegación inferior usan el color surface en el fondo. Como las tarjetas y la navegación inferior se encuentran en diferentes niveles de elevación sobre el fondo, tienen colores un poco diferentes: las tarjetas son más claras que el fondo, y la navegación inferior es más clara que las tarjetas.

Para situaciones personalizadas que no impliquen un elemento Surface, usa LocalElevationOverlay, un objeto CompositionLocal que contiene el elemento ElevationOverlay que usan los componentes Surface:

// Elevation overlays
// Implemented in Surface (and any components that use it)
val color = MaterialTheme.colors.surface
val elevation = 4.dp
val overlaidColor = LocalElevationOverlay.current?.apply(
    color, elevation
)

Para inhabilitar las superposiciones de elevación, brinda null en el punto deseado de una jerarquía que admite composición:

MyTheme {
    CompositionLocalProvider(LocalElevationOverlay provides null) {
        // Content without elevation overlays
    }
}

Acentuación de color limitada

En la mayoría de los casos, para Material, te recomendamos que apliques acentuación de color limitada para temas oscuros y le des preferencia al color surface por sobre el color primary. Los elementos de Material que admiten composición, como TopAppBar y BottomNavigation, implementan este comportamiento de forma predeterminada.

Figura 6: Tema oscuro de Material con acentuación de color limitada. La barra superior de la app usa el color principal en el tema claro y el color de superficie en el tema oscuro.

Para situaciones personalizadas, usa la propiedad de extensión primarySurface:

Surface(
    // Switches between primary in light theme and surface in dark theme
    color = MaterialTheme.colors.primarySurface,
    /*...*/
) { /*...*/ }

Tipografía

Material define un sistema de tipos y te recomienda que uses una pequeña cantidad de estilos con nombres semánticos.

Ejemplo de varios tipos de letra en distintos estilos

Figura 7: Sistema de tipos de Material.

Compose implementa el sistema de tipos con Typography, TextStyle y clases relacionadas con la fuente. El constructor Typography ofrece valores predeterminados para cada estilo, de modo que puedes omitir cualquiera que no quieras personalizar:

val Rubik = FontFamily(
    Font(R.font.rubik_regular),
    Font(R.font.rubik_medium, FontWeight.W500),
    Font(R.font.rubik_bold, FontWeight.Bold)
)

val MyTypography = Typography(
    h1 = TextStyle(
        fontFamily = Rubik,
        fontWeight = FontWeight.W300,
        fontSize = 96.sp
    ),
    body1 = TextStyle(
        fontFamily = Rubik,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    )
    /*...*/
)
MaterialTheme(typography = MyTypography, /*...*/)

Si deseas usar siempre la misma fuente, especifica defaultFontFamily parameter y omite el objeto fontFamily de cualquier elemento TextStyle:

val typography = Typography(defaultFontFamily = Rubik)
MaterialTheme(typography = typography, /*...*/)

Cómo usar estilos de texto

Se puede acceder a los objetos TextStyle a través de MaterialTheme.typography. Recupera los TextStyle de la siguiente manera:

Text(
    text = "Subtitle2 styled",
    style = MaterialTheme.typography.subtitle2
)

Captura de pantalla que muestra distintos tipos de letra combinados para diferentes fines

Figura 8: Usa una selección de tipos de letra y estilos para expresar tu marca.

Forma

Material define un sistema de formas, lo que te permite definir formas para componentes grandes, medianos y pequeños.

Muestra una variedad de formas de Material Design

Figura 9: Sistema de formas de Material.

Compose implementa el sistema de formas con la clase Shapes, que te permite especificar una CornerBasedShape para cada categoría de tamaño:

val Shapes = Shapes(
    small = RoundedCornerShape(percent = 50),
    medium = RoundedCornerShape(0f),
    large = CutCornerShape(
        topStart = 16.dp,
        topEnd = 0.dp,
        bottomEnd = 0.dp,
        bottomStart = 16.dp
    )
)

MaterialTheme(shapes = Shapes, /*...*/)

Muchos componentes usan estas formas de manera predeterminada. Por ejemplo, Button, TextField y FloatingActionButton, de manera predeterminada, adoptan el valor pequeño, AlertDialog adopta el valor mediano y ModalDrawer el grande. Consulta la referencia del esquema de formas para ver la asignación completa.

Cómo usar formas

Se puede acceder a los objetos Shape a través de MaterialTheme.shapes. Recupera los Shape con un código como este:

Surface(
    shape = MaterialTheme.shapes.medium, /*...*/
) {
    /*...*/
}

Captura de pantalla de una app que usa formas de Material para transmitir en qué estado se encuentra un elemento

Figura 10: Usa formas para expresar la marca o el estado.

Estilos predeterminados

En Compose, no existe un concepto equivalente a estilos predeterminados de objetos View de Android. Para brindar una funcionalidad similar, puedes crear tus propias funciones de "sobrecarga" que admitan composición y que unan componentes de Material. Por ejemplo, para crear un estilo de botón, une un botón a tu propia función que admite composición, configura directamente los parámetros que deseas modificar y expón otros como parámetros al elemento que admite composición que esta contiene.

@Composable
fun MyButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

Superposiciones de temas

Para alcanzar el equivalente a superposiciones de temas de objetos View de Android en Compose, puedes anidar elementos MaterialTheme que admitan composición. Como MaterialTheme establece, de forma predeterminada, los colores, la tipografía y las formas en el valor de tema actual, si un tema solo establece uno de esos parámetros, los otros mantienen sus valores predeterminados.

Además, cuando migres pantallas basadas en View a Compose, presta atención a los usos del atributo android:theme. Es probable que necesites un nuevo MaterialTheme en esa parte del árbol de IU de Compose.

En el ejemplo de Owl, la pantalla de detalles usa PinkTheme para la mayor parte de la pantalla y, luego, BlueTheme para la sección relacionada. Consulta la captura de pantalla y el código que aparecen más adelante.

Figura 11: Temas anidados en el ejemplo de Owl.

@Composable
fun DetailsScreen(/* ... */) {
    PinkTheme {
        // other content
        RelatedSection()
    }
}

@Composable
fun RelatedSection(/* ... */) {
    BlueTheme {
        // content
    }
}

Estados de componentes

Los componentes de Material con los que se puede interactuar (con un clic o un botón de activación, etc.) pueden estar en diferentes estados visuales. Los estados incluyen habilitado, inhabilitado, presionado, etc.

Con frecuencia, los elementos que admiten composición tienen un parámetro enabled. Si se establece en false, se evita la interacción, y se cambian propiedades, como el color y la elevación, para transmitir, de forma visual, el estado del componente.

Figura 12: Botón con enabled = true (izquierda) y enabled = false (derecha)

En la mayoría de los casos, puedes confiar en los parámetros predeterminados para valores como el color y la elevación. Si deseas configurar los valores que se usan en diferentes estados, hay clases y funciones prácticas disponibles. Consulta el siguiente ejemplo de botón:

Button(
    onClick = { /* ... */ },
    enabled = true,
    // Custom colors for different states
    colors = ButtonDefaults.buttonColors(
        backgroundColor = MaterialTheme.colors.secondary,
        disabledBackgroundColor = MaterialTheme.colors.onBackground
            .copy(alpha = 0.2f)
            .compositeOver(MaterialTheme.colors.background)
        // Also contentColor and disabledContentColor
    ),
    // Custom elevation for different states
    elevation = ButtonDefaults.elevation(
        defaultElevation = 8.dp,
        disabledElevation = 2.dp,
        // Also pressedElevation
    )
) { /* ... */ }

Figura 13: Botón con enabled = true (izquierda) y enabled = false (derecha), con valores de color y elevación ajustados.

Ecos

Los componentes de Material usan ondas para indicar con qué se está interactuando. Si usas MaterialTheme en la jerarquía, se usará Ripple como un objeto Indication predeterminado dentro de los modificadores, por ejemplo, clickable y indication.

En la mayoría de los casos, puedes confiar en el objeto Ripple predeterminado. Si deseas configurar su apariencia, puedes usar RippleTheme para cambiar propiedades como color y alfa.

Puedes extender RippleTheme y aprovechar las funciones de utilidad defaultRippleColor y defaultRippleAlpha. Luego, puedes brindar el tema de ondas personalizado en la jerarquía mediante LocalRippleTheme:

@Composable
fun MyApp() {
  MaterialTheme {
    CompositionLocalProvider(
      LocalRippleTheme provides SecondaryRippleTheme
    ) {
      // App content
    }
  }
}

@Immutable
private object SecondaryRippleTheme : RippleTheme {
  @Composable
  override fun defaultColor() = RippleTheme.defaultRippleColor(
    contentColor = MaterialTheme.colors.secondary,
    lightTheme = MaterialTheme.colors.isLight
  )

  @Composable
  override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
    contentColor = MaterialTheme.colors.secondary,
    lightTheme = MaterialTheme.colors.isLight
  )
}

Texto alternativo

Figura 14: Botones con diferentes valores de ondas que se brindan a través de RippleTheme

Material Design 3 y Material You

Jetpack Compose ofrece una implementación de Material Design 3, la próxima evolución de Material Design. Material 3 incluye temas y componentes actualizados, y funciones de personalización de Material You, como el color dinámico. Además, se diseñó para ser coherente con el nuevo estilo visual y la IU del sistema de Android 12.

Para comenzar, agrega la nueva dependencia de Compose Material 3 a tus archivos build.gradle:

implementation "androidx.compose.material3:material3:material3_version"

Un tema M3 contiene valores de esquema de colores y tipografía, y próximamente se agregarán actualizaciones a las formas. Cuando personalizas esos valores, tus cambios se reflejan automáticamente en los componentes de M3 que usas para compilar tu app.

Jetpack Compose implementa esos conceptos con el nuevo elemento MaterialTheme de M3 que admite composición:

MaterialTheme(
    colorScheme = …,
    typography = …
    // Updates to shapes coming soon
) {
    // M3 app content
}

Configura los parámetros que pasas a MaterialTheme de M3 para aplicar un tema a tu aplicación.

Figura 15: Las primeras dos capturas de pantalla muestran una app que no configura MaterialTheme de M3 y, por lo tanto, usa el estilo predeterminado. Las segundas capturas de pantalla muestran una app que pasa parámetros a MaterialTheme para personalizar el estilo.

Esquema de colores

Material Design 3 separa los colores en "espacios" de color con nombre, como "principal", "en segundo plano" y "error", que usan los componentes de Material 3. En conjunto, estos espacios forman un esquema de colores. Los valores de color que usa cada espacio se extraen de un conjunto de paletas de tonos, que se seleccionan para cumplir con los requisitos de accesibilidad (por ejemplo, se garantiza el contraste entre el espacio "principal" y el espacio "en principal"). Los esquemas de colores presentan nuevos colores de referencia predeterminados para temas claros y oscuros.

Figura 16: Esquemas de colores y paletas de tonos de Material 3, con valores de color de referencia

Compose proporciona la clase ColorScheme para modelar el esquema de colores de Material 3. ColorScheme proporciona funciones de compilador para crear un esquema de colores claro u oscuro:

private val Blue40 = Color(0xff1e40ff​​)
private val DarkBlue40 = Color(0xff3e41f4)
private val Yellow40 = Color(0xff7d5700)
// Remaining colors from tonal palettes

private val LightColorScheme = lightColorScheme(
    primary = Blue40,
    secondary = DarkBlue40,
    tertiary = Yellow40,
    // error, primaryContainer, onSecondary, etc.
)
private val DarkColorScheme = darkColorScheme(
    primary = Blue80,
    secondary = DarkBlue80,
    tertiary = Yellow80,
    // error, primaryContainer, onSecondary, etc.
)

Una vez que hayas definido tu ColorScheme, puedes pasarlos a MaterialTheme de M3:

val darkTheme = isSystemInDarkTheme()
MaterialTheme(
    colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
) {
    // M3 app content
}

Cómo generar esquemas de colores

Si bien puedes crear un ColorScheme personalizado de forma manual, suele ser más fácil generar uno con los colores de origen de tu marca. La herramienta Material Theme Builder te permite hacer esto y, de manera opcional, exportar el código de temas de Compose.

Figura 17: Esquemas de colores y paletas tonales de Material 3, con valores de color personalizados, generados por la herramienta Material Theme Builder.

Esquemas de colores dinámicos

El color dinámico es la parte clave de Material You, en la que un algoritmo deriva colores personalizados del fondo de pantalla de un usuario para aplicarlos a las IU de sus apps y sistema. Esta paleta de colores se utiliza como punto de partida para generar un esquema completo de colores claros y oscuros.

El color dinámico está disponible en Android 12 y versiones posteriores. Si el color dinámico está disponible, puedes configurar un ColorScheme dinámico. De lo contrario, puedes recurrir a un ColorScheme claro u oscuro personalizado.

ColorScheme proporciona funciones de compilador para crear un esquema de colores claro u oscuro dinámico:

// Dynamic color is available on Android 12+
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when {
    dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
    dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
    darkTheme -> DarkColorScheme
    else -> LightColorScheme
}

Figura 18: En las primeras dos capturas de pantalla se muestra una app que usa un ColorScheme dinámico basado en un fondo de pantalla rojo. En las segundas dos capturas de pantalla se muestra una app que usa un ColorScheme dinámico basado en un fondo de pantalla azul.

Cómo usar colores de esquema de colores

Puedes usar MaterialTheme.colorScheme para recuperar el ColorScheme proporcionado al elemento que admite compilación MaterialTheme de M3.

Text(
    text = "Hello M3 theming",
    color = MaterialTheme.colorScheme.tertiary
)
.

Tipografía

Material Design 3 define una escala de tipo, incluidos los estilos de texto que se adaptaron de Material Design 2. Los nombres y las agrupaciones se simplificaron a los siguientes: gráficos, encabezados, títulos, cuerpos y etiquetas, con tamaños grandes, medianos y pequeños para cada uno.

Figura 19: Comparación entre la escala de tipo Material 3 y la escala de tipo Material 2

Compose proporciona la nueva clase Typography de M3 (junto con las clases de TextStyle y relacionadas con la fuente) para modelar la escala de tipo de Material 3:

val KarlaFontFamily = FontFamily(
    Font(R.font.karla_regular),
    Font(R.font.karla_bold, FontWeight.Bold)
)

val AppTypography = Typography(
    bodyLarge = TextStyle(
        fontFamily = KarlaFontFamily,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.15.sp
    ),
    // titleMedium, labelSmall, etc.
)

Una vez que hayas definido tu Typography, puedes pasarla a MaterialTheme de M3:

MaterialTheme(
    typography = AppTypography
) {
    // M3 app content
}

Cómo usar estilos de texto

Puedes usar MaterialTheme.typography para recuperar la Typography proporcionada al elemento que admite compilación MaterialTheme de M3.

Text(
    text = "Hello M3 theming",
    style = MaterialTheme.typography.bodyLarge
)

Elevación

Material 3 representa la elevación, principalmente, mediante superposiciones de colores tonales. Esta es una nueva forma de distinguir contenedores y superficies entre sí (aumentar la elevación tonal utiliza un tono más prominente) además de las sombras.

Las superposiciones de elevación en el tema oscuro también cambiaron por las superposiciones de color tonales en Material 3.

El color de superposición proviene del espacio de color primario.

Figura 20: Comparación entre la elevación de Material 3 y la elevación de Material 2, en tema claro y oscuro

La Surface de M3 (el elemento de copia de seguridad que admite composición detrás de la mayoría de los componentes de M3) incluye compatibilidad con la elevación de tonos y sombras:

Surface(
    tonalElevation = 16.dp,
    shadowElevation = 16.dp
) {
    // Surface content
}

IU del sistema

Algunos aspectos de Material You provienen de la nueva IU del sistema y el estilo visual de Android 12. Dos áreas clave en las que se producen cambios son las ondas y el desplazamiento. No se requiere trabajo adicional para implementar estos cambios.

Ripple

Ripple ahora usa un destello sutil para iluminar superficies cuando se presiona. Compose Material Ripple usa una plataforma RippleDrawable de forma interna en Android, por lo que la función de onda de destello está disponible en Android 12 y versiones posteriores para todos los componentes de Material.

Figura 21: Comparación entre ondas de Android 12 y ondas anteriores a Android 12

Sobredesplazamiento

El sobredesplazamiento ahora usa un efecto de estiramiento en el perímetro de los contenedores de desplazamiento. El sobredesplazamiento de estiramiento está activado de forma predeterminada al desplazarse en los elementos que se pueden componer del contenedor (por ejemplo.LazyColumn ,LazyRow y LazyVerticalGrid) en Compose Foundation 1.1.0 y posteriores, sin importar el nivel de API.

Figure 22. Sobredesplazamiento de estiramiento.

Más información

Para obtener más información sobre los temas de Material en Compose, consulta los siguientes recursos adicionales.

Codelabs

Videos