Ya está aquí una nueva entrega donde ampliaremos lo que ya sabemos sobre el uso de Material Theme en Compose. Si todavía no has tenido tiempo de familiarizarte con ello, te invitamos a pasarte por el post Themes en Compose: MaterialTheme.

Ahora que ya has empezado a hacer tus primeras pruebas con Material Theme, probablemente te hayas topado con un escollo común a la hora de implementar aplicaciones “del mundo real”: los diseñadores tienen su propio sistema de organización y por mucho que haya adaptado sus variables de color o tipografía al patrón de Material, hay valores extra que no tienen cabida.

A continuación, estudiaremos cómo podemos añadir nuevos valores a la definición y así ampliar sus funciones.

Añadir un valor

Lo primero que haremos será aprender cómo añadir una definición extra.

Para ampliar las colecciones de colores, tipografía o shapes que presenta Material Theme de forma rápida y consistente con las APIs de uso de la librería, podemos optar por crear extensiones para cada una de ellas. Por ejemplo, si queremos añadir un nuevo valor a los colores que ofrece Material Theme lo definiríamos como una extensión de la clase:

val ColorScheme.extraColor: Color
   @Composable
   get() = if (isSystemInDarkTheme()) Color.Red else Color.Blue

De esta forma podemos invocar de manera normal a nuestro nuevo color de la siguiente manera:

MaterialTheme.colorScheme.extraColor

También podemos añadir tipografías o formas (shapes) siguiendo esta misma línea:

val Typography.extraTextStyle: TextStyle
   @Composable
   get() = TextStyle(color = Color.Red)

// Uso
MaterialTheme.typography.extraTextStyle

val Shapes.extraShape: Shape
   @Composable
   get() = RoundedCornerShape(size = 20.dp)

// Uso
MaterialTheme.shapes.extraShape

Aunque es una forma completamente lícita de extender los valores de Material Theme, realmente es poco efectiva. En la siguiente sección veremos una mejor forma de enfocarlo.

Añadir varios valores

Hay una segunda aproximación que nos permite añadir más valores de una forma más organizada y elegante consistente en definir nuestros propios esquemas de colores o tipografías y añadirlos como extensión de Material Theme. Así, podremos acceder a dichos valores de forma consistente con el resto de la API de Material.

En primer lugar, tendremos que crear una clase análoga a la que queremos ampliar, en nuestro ejemplo será ColorScheme. Como la propia clase indica en sus comentarios, contiene todos los parámetros con nombres de colores utilizados en un Material Theme. Nuestra clase CustomColorScheme define todos los nuevos nombres de colores que queramos utilizar en nuestra app.

Por simplicidad, en el ejemplo se han incluido solo tres con nombres genéricos extraColorN:

@Immutable
class CustomColorScheme(
   extraColor1: Color,
   extraColor2: Color,
   extraColor3: Color,
) {
   var extraColor1 by mutableStateOf(extraColor1, structuralEqualityPolicy())
       internal set
   var extraColor2 by mutableStateOf(extraColor2, structuralEqualityPolicy())
       internal set
   var extraColor3 by mutableStateOf(extraColor3, structuralEqualityPolicy())
       internal set

   /** Returns a copy of this CustomColorScheme, optionally overriding some of the values. */
   fun copy(
       extraColor1: Color = this.extraColor1,
       extraColor2: Color = this.extraColor2,
       extraColor3: Color = this.extraColor3,
   ): CustomColorScheme = CustomColorScheme(extraColor1, extraColor2, extraColor3)

   override fun toString(): String {
       return "CustomColorScheme(" +
               "extraColor1=$extraColor1" +
               "extraColor2=$extraColor2" +
               "extraColor3=$extraColor3" +
               ")"
   }
}

En términos generales, hemos redefinido las mismas funciones que utiliza Material en su ColorScheme para asegurar la coherencia con el resto del sistema. Para ello, hemos creado una clase sellada que define los nombres adicionales de recursos que queramos declarar. A esto solo habría que añadir un par de detalles.

En primer lugar, definir las funciones en el mismo fichero CustomColorScheme que definen los valores en función de si estamos en light o dark mode y su ProvidableCompositionLocal para acceder a los valores por defecto:

/**
* Creates a light custom color scheme with default colors for the extended colors
* @return A CustomColorScheme instance representing the light color scheme.
*/
fun lightCustomColorScheme(
   extraColor1: Color = CustomColors.darkNavy,
   extraColor2: Color = CustomColors.yellow20,
   extraColor3: Color = CustomColors.black40,
): CustomColorScheme =
   CustomColorScheme(
       extraColor1 = extraColor1,
       extraColor2 = extraColor2,
       extraColor3 = extraColor3
   )

/**
* Creates a dark custom color scheme with default colors for the extended colors
* @return A CustomColorScheme instance representing the dark color scheme.
*/
fun darkCustomColorScheme(
   extraColor1: Color = CustomColors.darkNavy,
   extraColor2: Color = CustomColors.yellow20,
   extraColor3: Color = CustomColors.white,
): CustomColorScheme =
   CustomColorScheme(
       extraColor1 = extraColor1,
       extraColor2 = extraColor2,
       extraColor3 = extraColor3
   )

/**
* A composition local containing the current custom color scheme.
*/
val LocalCustomColorScheme = staticCompositionLocalOf { lightCustomColorScheme() }

En el caso de querer ampliar las tipografías, procederíamos de forma muy similar: Crearíamos nuestra clase CustomTypography y su ProvidableCompositionLocal siguiendo el patrón de la clase Typography.

// Custom fonts (if necessary)
private val montserratNormal = Font(R.font.montserrat_regular_ttf, FontWeight.Normal)
private val montserratBold = Font(R.font.montserrat_bold_ttf, FontWeight.Bold)

// Declare the FontFamily
val montserratFontFamily = FontFamily(
   montserratNormal,
   montserratBold
)
@Immutable
class CustomTypography(
   val customBase: TextStyle = TextStyle(
       fontFamily = montserratFontFamily,
       fontWeight = FontWeight.Normal,
       fontSize = 16.sp,
   ),
   val customBold: TextStyle = customBase.copy(
       fontWeight = FontWeight.Bold,
       letterSpacing = 1.5.sp,
       lineHeight = 112.sp,
       fontSize = 96.sp
   ),
   val customSmall: TextStyle = customBase.copy(
       fontSize = 12.sp
   ),
) {

   fun copy(
       customBase: TextStyle = this.customBase,
       customBold: TextStyle = this.customBold,
       customSmall: TextStyle = this.customSmall,
   ): CustomTypography =
       CustomTypography(
           customBase = customBase,
           customBold = customBold,
           customSmall = customSmall
       )
   override fun equals(other: Any?): Boolean {...}
   override fun hashCode(): Int {...}
   override fun toString(): String {...}
}
val LocalCustomTypography = staticCompositionLocalOf { CustomTypography() }

Y, como último paso, solo deberíamos hacer accesibles dichos valores desde los Themes. Para ello tenemos dos opciones (ambas en nuestro fichero Theme).

val MaterialTheme.customColorScheme: CustomColorScheme
   @Composable
   @ReadOnlyComposable
   get() = LocalCustomColorScheme.current

val MaterialTheme.customTypography: CustomTypography
   @Composable
   @ReadOnlyComposable
   get() = LocalCustomTypography.current

@Composable
fun CustomTheme(
   darkTheme: Boolean = isSystemInDarkTheme(),
   customTypography: CustomTypography = CustomTypography(),
   content: @Composable () -> Unit,
) {
   val colorScheme = when (darkTheme) {
       true -> DarkColorScheme
       false -> LightColorScheme 
   }
   val customColorScheme = when (darkTheme) {
       true -> lightCustomColorScheme()
       false -> darkCustomColorScheme()    }

   CompositionLocalProvider(
       LocalCustomColorScheme provides customColorScheme,
       LocalCustomTypography provides customTypography
   ) {
       MaterialTheme(
           colorScheme = colorScheme,
           content = content
       )
   }
}

De esta forma, podremos acceder a los nuevos colores de la siguiente manera:

MaterialTheme.customColorScheme.extraColor1
MaterialTheme.customTypography.extraColor1
object CustomTheme {
   val colors: CustomColorScheme
       @Composable
       get() = LocalCustomColorScheme.current
   val typography: CustomTypography
       @Composable
       get() = LocalCustomTypography.current
}

// Uso
CustomTheme.colors.extraColor1
CustomTheme.typography.customSmall

Conclusión

En este post, hemos visto cómo añadir más valores de forma individual o en grupo y acoplarnos a la API de Material Theme, haciendo nuestros temas mucho más flexibles y adaptables a las formas de trabajar de nuestros equipos de diseño.

Aun así, si esto se quedase corto o no quisiéramos utilizar en absoluto la API de Material, siempre podemos crear nuestro propio tema desde cero y utilizarlo con nuestros componentes personalizados. Este cambio resulta más radical y habría que adaptar todo el sistema de componentes a esta forma, por lo que lo dejaremos para más adelante.

Espero que os haya sido de ayuda y recuerda: ¡Keep Calm and Debug!

Referenicas

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.