tutoriales.com

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

Intermedio20 min de lectura10 views
Reportar error

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'
}
💡 Consejo: Siempre verifica las últimas versiones de las dependencias de Compose y Material 3 en la documentación oficial para asegurar la compatibilidad y las últimas características.

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 ColorDescripciónEjemplo de Uso
---------
primaryColor principal, la mayoría de los componentes interactivos.Botones, íconos de navegación, texto de encabezado.
onPrimaryColor para el contenido sobre primary.Texto en un botón primario.
---------
primaryContainerUn color complementario a primary para contenedores.Fondo de chips, tarjetas resaltadas.
onPrimaryContainerColor para el contenido sobre primaryContainer.Texto en un chip.
---------
secondaryColor secundario para elementos menos prominentes.Botones flotantes (FAB), selección de texto.
tertiaryColor terciario, útil para acentos o estados.Acentos visuales, indicadores de progreso.
---------
backgroundFondo general de la pantalla.Fondo de toda la Activity.
surfaceSuperficie de componentes como tarjetas, hojas inferiores.Fondo de Card, BottomSheet.
---------
errorColor para estados de error.Texto de error, íconos de validación.
📌 Nota: Material 3 utiliza un sistema de `tonalidad` para generar variaciones de color que funcionan bien juntas, tanto en modo claro como oscuro.

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) y dynamicLightColorScheme(context): Estas funciones son la clave para los temas dinámicos. En dispositivos con Android 12 (API 31) o superior, estas funciones generan un ColorScheme basado en el fondo de pantalla del usuario. Necesitan un Context que obtenemos de LocalContext.current.
  • DarkColorScheme y LightColorScheme: Estas son las paletas de colores fallback que se usarán en dispositivos anteriores a Android 12, o si el dynamicColor está deshabilitado. Se definen en el archivo Color.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 el colorScheme, typography y shapes definidos.

🎨 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")
    }
}
🔥 Importante: Para que los colores dinámicos funcionen, tu `targetSdk` en `build.gradle` debe ser al menos 31. Además, debes ejecutar la aplicación en un dispositivo o emulador con Android 12 (API 31) o superior.

🔄 Probando el Tema Dinámico

Para ver los temas dinámicos en acción, sigue estos pasos:

  1. Ejecuta la aplicación en un dispositivo o emulador con Android 12 (API 31) o superior.
  2. 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).
  3. 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.
1. Usuario cambia fondo de pantalla 2. Sistema Android genera paleta de colores 3. App inicia: se llama a dynamicLightColorScheme() o dynamicDarkColorScheme() 4. MaterialTheme usa la nueva paleta 5. UI de la App se actualiza con colores dinámicos

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.primary
  • MaterialTheme.typography.bodyLarge
  • MaterialTheme.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.

⚠️ Advertencia: Si tu `minSdk` es menor que 31, asegúrate de que el bloque `when` en `DynamicThemingTheme` maneje correctamente los casos para versiones anteriores, usando tus `DarkColorScheme` y `LightColorScheme` predefinidos.

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

Inicio ¿SDK >= 31? ¿Dynamic Color? ¿Modo Oscuro? ¿Modo Oscuro? ¿Modo Oscuro? dynamicDark ColorScheme dynamicLight ColorScheme Dark ColorScheme Light ColorScheme Dark ColorScheme Light ColorScheme Aplicar ColorScheme a MaterialTheme No No No No No

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

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!