¡Ponte al Día con Compose! Construyendo Temas Dinámicos en Android con Material 3 y Jetpack Compose
Este tutorial te guiará paso a paso en la creación e implementación de temas dinámicos en tu aplicación Android, utilizando las últimas directrices de Material 3 y el framework de UI declarativa Jetpack Compose. Aprenderás a integrar la personalización de colores para ofrecer una experiencia de usuario única y moderna.
Introducción a los Temas Dinámicos y Material 3 en Compose ✨
En el ecosistema Android actual, la personalización es clave para ofrecer una experiencia de usuario excepcional. Los usuarios esperan que las aplicaciones se adapten a sus preferencias, y una de las características más solicitadas es la capacidad de cambiar el tema visual de una app. Con la llegada de Material 3 y Jetpack Compose, implementar temas dinámicos nunca ha sido tan sencillo y potente.
Material 3 es la última evolución del sistema de diseño de Google, que pone un énfasis significativo en la personalización y la adaptabilidad. Una de sus características estrella es la capacidad de generar paletas de colores dinámicas a partir del fondo de pantalla del usuario, creando una experiencia coherente y armoniosa en todo el dispositivo. Jetpack Compose, por su parte, es el toolkit moderno para construir UI nativas en Android, facilitando enormemente la integración de estos principios de diseño.
Este tutorial te sumergirá en el mundo de los temas dinámicos con Material 3 y Compose, desde la configuración inicial hasta la aplicación de colores personalizados y la gestión del modo oscuro.
¿Por qué Temas Dinámicos? 💡
Los temas dinámicos ofrecen múltiples beneficios:
- Personalización: Permiten que la aplicación se adapte al gusto del usuario, incluso extrayendo colores de su fondo de pantalla (una característica de Android 12+).
- Accesibilidad: Facilitan la implementación de modos oscuro/claro y esquemas de color de alto contraste.
- Coherencia de Marca: Aunque dinámicos, permiten definir un conjunto base de colores para mantener la identidad visual.
- Modernidad: Las aplicaciones con temas dinámicos se perciben como más actuales y mejor integradas en el sistema Android.
Requisitos Previos y Configuración del Proyecto 🛠️
Antes de sumergirnos en el código, asegúrate de tener el entorno de desarrollo listo.
📌 Herramientas Necesarias
- Android Studio Dolphin | 2021.3.1 o superior: Es crucial para tener el soporte completo de Compose y Material 3.
- SDK de Android 31 (Android 12) o superior: Para aprovechar al máximo las características de color dinámico del sistema.
- Kotlin 1.7.0 o superior.
⚙️ Configuración del build.gradle (Module: app)
Primero, asegúrate de que tus dependencias de Compose estén actualizadas y que incluyas las librerías de Material 3.
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'com.example.dynamictheming'
compileSdk 34 // O la versión más reciente
defaultConfig {
applicationId 'com.example.dynamictheming'
minSdk 21
targetSdk 34 // Debe ser >= 31 para color dinámico del sistema
versionCode 1
versionName '1.0'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.1' // Asegúrate de que coincida con tu versión de Kotlin
}
packagingOptions {
resources {
excludes '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
implementation 'androidx.activity:activity-compose:1.8.1'
implementation platform('androidx.compose:compose-bom:2023.08.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
androidTestImplementation platform('androidx.compose:compose-bom:2023.08.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
androidTestImplementation 'androidx.compose.ui:ui-test-manifest'
}
Entendiendo Material 3 Theming en Compose 🎨
Material 3 introduce un sistema de theming robusto y personalizable, que se basa en un sistema de tokens de diseño. En Compose, esto se traduce en el MaterialTheme composable, que es el punto de entrada para aplicar la theming a toda tu aplicación. MaterialTheme toma tres parámetros principales:
colorScheme: Define la paleta de colores de tu app (primario, secundario, terciario, etc.).typography: Define las fuentes y estilos de texto.shapes: Define la forma de los componentes (redondez de esquinas, etc.).
Nos centraremos principalmente en colorScheme para los temas dinámicos.
🌈 El ColorScheme de Material 3
ColorScheme es una clase que contiene todas las propiedades de color para un tema. Material 3 tiene una paleta de colores expandida en comparación con Material 2, incluyendo nuevos roles como primaryContainer, onPrimaryContainer, tertiary, tertiaryContainer, etc. Esto permite una mayor granularidad y flexibilidad en el diseño.
Aquí tienes un resumen de algunos roles de color clave:
| Rol de Color | Descripción | Ejemplo de Uso |
|---|---|---|
| --- | --- | --- |
primary | Color principal, la mayoría de los componentes interactivos. | Botones, íconos de navegación, texto de encabezado. |
onPrimary | Color para el contenido sobre primary. | Texto en un botón primario. |
| --- | --- | --- |
primaryContainer | Un color complementario a primary para contenedores. | Fondo de chips, tarjetas resaltadas. |
onPrimaryContainer | Color para el contenido sobre primaryContainer. | Texto en un chip. |
| --- | --- | --- |
secondary | Color secundario para elementos menos prominentes. | Botones flotantes (FAB), selección de texto. |
tertiary | Color terciario, útil para acentos o estados. | Acentos visuales, indicadores de progreso. |
| --- | --- | --- |
background | Fondo general de la pantalla. | Fondo de toda la Activity. |
surface | Superficie de componentes como tarjetas, hojas inferiores. | Fondo de Card, BottomSheet. |
| --- | --- | --- |
error | Color para estados de error. | Texto de error, íconos de validación. |
Implementación del Tema Base en Compose 🧑💻
Vamos a crear nuestro archivo Theme.kt para configurar el tema de nuestra aplicación. En un proyecto nuevo de Compose, este archivo ya existe en la carpeta ui.theme.
📂 Creando el Theme.kt (o modificándolo)
package com.example.dynamictheming.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun DynamicThemingTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+ (SDK 31+)
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
Aquí hay una explicación de los componentes clave:
isSystemInDarkTheme(): Este composable detecta si el sistema está en modo oscuro, lo que nos permite aplicar un tema oscuro o claro.dynamicDarkColorScheme(context)ydynamicLightColorScheme(context): Estas funciones son la clave para los temas dinámicos. En dispositivos con Android 12 (API 31) o superior, estas funciones generan unColorSchemebasado en el fondo de pantalla del usuario. Necesitan unContextque obtenemos deLocalContext.current.DarkColorSchemeyLightColorScheme: Estas son las paletas de colores fallback que se usarán en dispositivos anteriores a Android 12, o si eldynamicColorestá deshabilitado. Se definen en el archivoColor.kt.SideEffect: Se usa para cambiar el color de la barra de estado y la barra de navegación para que coincidan con el tema de la aplicación. Esto es importante para una experiencia de usuario inmersiva.MaterialTheme: El composable principal que envuelve el contenido de tu aplicación y aplica elcolorScheme,typographyyshapesdefinidos.
🎨 Definiendo los Colores (Color.kt)
Necesitarás definir los colores base para tus esquemas de color de respaldo (DarkColorScheme y LightColorScheme). Estos colores deben ir en el archivo ui.theme/Color.kt:
package com.example.dynamictheming.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
// Otros colores que necesites para tu tema, por ejemplo:
val AccentColor = Color(0xFF00C853)
val WarningColor = Color(0xFFFFC107)
val ErrorColor = Color(0xFFD32F2F)
📝 Aplicando el Tema en MainActivity.kt
Ahora, envuelve tu Surface principal con el DynamicThemingTheme que acabamos de crear. Por defecto, en un nuevo proyecto Compose, esto ya está configurado.
package com.example.dynamictheming
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.dynamictheming.ui.theme.DynamicThemingTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
DynamicThemingTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier,
color = MaterialTheme.colorScheme.primary // Usa un color del tema
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
DynamicThemingTheme {
Greeting("Android")
}
}
🔄 Probando el Tema Dinámico
Para ver los temas dinámicos en acción, sigue estos pasos:
- Ejecuta la aplicación en un dispositivo o emulador con Android 12 (API 31) o superior.
- Cambia el fondo de pantalla de tu dispositivo. Puedes usar imágenes con colores muy contrastantes (ej. una imagen mayormente azul y luego una mayormente verde).
- Abre y cierra tu aplicación o navega entre pantallas. Deberías notar cómo los colores de tu aplicación se adaptan automáticamente a la paleta derivada del fondo de pantalla.
Deshabilitando el Color Dinámico
Si necesitas ofrecer a los usuarios la opción de deshabilitar el color dinámico y usar tu tema predeterminado, puedes hacerlo fácilmente. Modifica el parámetro dynamicColor en tu DynamicThemingTheme composable y permite que el usuario lo cambie a través de una configuración en la app.
// En DynamicThemingTheme composable
@Composable
fun DynamicThemingTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true, // Controla esto desde un estado de tu app
content: @Composable () -> Unit
) {
// ... (el resto del código es el mismo)
}
Luego, puedes tener una preferencia de usuario guardada con DataStore o SharedPreferences que controle el valor de dynamicColor.
Ejemplo de cómo controlar `dynamicColor` desde un ViewModel
// En tu ViewModel
class ThemeViewModel : ViewModel() {
private val _useDynamicColor = MutableStateFlow(true)
val useDynamicColor: StateFlow<Boolean> = _useDynamicColor
fun setUseDynamicColor(value: Boolean) {
_useDynamicColor.value = value
}
}
// En tu Activity o composable principal
@Composable
fun MyApp() {
val themeViewModel: ThemeViewModel = viewModel()
val useDynamicColor by themeViewModel.useDynamicColor.collectAsState()
DynamicThemingTheme(dynamicColor = useDynamicColor) {
// ... tu app
}
}
Extendiendo tu Tema: Tipografía y Formas ✒️🔳
Aunque los colores dinámicos son la estrella, un tema completo también incluye typography y shapes. Material 3 tiene un sistema de tipografía y formas más refinado.
🔡 Tipografía (Typography.kt)
El archivo Typography.kt (en ui.theme) define los estilos de texto para tu aplicación. Material 3 tiene una escala tipográfica más extensa, con 15 categorías de tipo para cubrir todos los casos de uso.
package com.example.dynamictheming.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
Puedes personalizar las propiedades de TextStyle (familia de fuente, peso, tamaño, etc.) para cada categoría.
🔲 Formas (Shape.kt)
El archivo Shape.kt (en ui.theme) define las formas predeterminadas para los componentes de Material Design. Esto incluye la redondez de las esquinas.
package com.example.dynamictheming.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
extraSmall = RoundedCornerShape(4.dp),
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(16.dp),
extraLarge = RoundedCornerShape(24.dp)
)
Aquí, puedes definir diferentes grados de redondez para elementos de diferentes tamaños. Por ejemplo, un botón pequeño podría tener un small shape, mientras que una tarjeta grande podría usar un large shape.
Uso en Componibles
Una vez definidos en MaterialTheme, puedes acceder a estos valores en cualquier composable:
MaterialTheme.colorScheme.primaryMaterialTheme.typography.bodyLargeMaterialTheme.shapes.medium
Por ejemplo:
@Composable
fun MyThemedCard(title: String, description: String) {
Card(
modifier = Modifier.padding(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
shape = MaterialTheme.shapes.medium // Aplica la forma definida
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium, // Aplica el estilo de tipografía
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.height(8.dp))
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
Buenas Prácticas y Consideraciones Avanzadas 🚀
⚠️ Compatibilidad y minSdk
Recuerda que la funcionalidad de color dinámico del sistema está limitada a Android 12 (API 31) y versiones posteriores. Asegúrate de que tu minSdk no sea demasiado alto si necesitas soportar dispositivos antiguos, pero ten en cuenta que no obtendrán esta característica.
Persistencia del Tema
Para ofrecer una experiencia consistente, es posible que desees que los usuarios puedan seleccionar un tema (claro, oscuro, o seguir al sistema) y que esa preferencia persista entre sesiones. Puedes lograr esto usando DataStore o SharedPreferences para guardar la preferencia del usuario y luego pasarla como parámetro a DynamicThemingTheme.
// Ejemplo simplificado de cómo se podría implementar la persistencia
// (Requiere DataStore o SharedPreferences en tu proyecto)
enum class ThemeMode { SYSTEM, LIGHT, DARK, DYNAMIC }
// ... en tu ViewModel o un Manager de preferencias
class UserPreferences(private val context: Context) {
companion object {
private val THEME_MODE_KEY = stringPreferencesKey("theme_mode")
}
val themeMode: Flow<ThemeMode> = context.dataStore.data
.map { preferences ->
ThemeMode.valueOf(preferences[THEME_MODE_KEY] ?: ThemeMode.SYSTEM.name)
}
suspend fun saveThemeMode(mode: ThemeMode) {
context.dataStore.edit {
it[THEME_MODE_KEY] = mode.name
}
}
}
// ... en tu DynamicThemingTheme
@Composable
fun DynamicThemingTheme(
userSelectedTheme: ThemeMode = ThemeMode.SYSTEM, // Valor de las preferencias del usuario
content: @Composable () -> Unit
) {
val darkTheme = when (userSelectedTheme) {
ThemeMode.LIGHT -> false
ThemeMode.DARK -> true
ThemeMode.SYSTEM -> isSystemInDarkTheme()
ThemeMode.DYNAMIC -> isSystemInDarkTheme() // Dynamic se aplica sobre el modo oscuro/claro del sistema
}
val dynamicColor = userSelectedTheme == ThemeMode.DYNAMIC || userSelectedTheme == ThemeMode.SYSTEM // Podrías definir esto de forma más granular
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
// ... el resto es igual
}
Pro Componentes Personalizados con Temas
Si creas tus propios componentes de diseño en Compose, asegúrate de que utilicen los colores y la tipografía de MaterialTheme. Esto garantizará que tus componentes personalizados se integren perfectamente con el tema dinámico de la aplicación.
@Composable
fun CustomElevatedButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
androidx.compose.material3.ElevatedButton(
onClick = onClick,
modifier = modifier,
colors = ButtonDefaults.elevatedButtonColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer, // Usar color del tema
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
)
) {
Text(text = text, style = MaterialTheme.typography.labelLarge) // Usar tipografía del tema
}
}
Diagrama de Flujo de Decisión de Tema
Conclusión 🎉
Has llegado al final de este tutorial sobre temas dinámicos en Android con Material 3 y Jetpack Compose. Ahora tienes el conocimiento y las herramientas para implementar una experiencia de usuario altamente personalizable y visualmente atractiva en tus aplicaciones. La capacidad de tu app para adaptarse al entorno y las preferencias del usuario no solo mejora la estética, sino que también contribuye a una mayor satisfacción y accesibilidad.
Experimenta con diferentes combinaciones de colores, tipografías y formas para encontrar la identidad visual perfecta para tu aplicación. ¡El poder de la personalización está en tus manos!
Tutoriales relacionados
- Almacenamiento Seguro de Datos en Android: SharedPreferences Cifradas y Jetpack DataStoreintermediate20 min
- ¡Despide la Fragmentación! Construyendo una Arquitectura Modular Robusta en Android con Dynamic Feature Modulesintermediate20 min
- ¡Poder y Flexibilidad! Construyendo Componentes Reutilizables en Android con Jetpack Compose y Modificadores Personalizadosintermediate25 min
- Navegación Avanzada en Android: Jetpack Navigation Component y Deep Linksintermediate25 min
- Carga de Datos Offline en Android: Implementando una Estrategia Robusta con Room y WorkManagerintermediate25 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!