androidmaterial-designandroid-jetpack-compose

Android - Create custom colors in Compose with Material 3


I have been exploring Compose + Material 3 for the first time and I am having a really hard time trying to implement a custom color.

What I mean by this is doing the following based on how things could be done before Compose:

I have my custom attribute in attrs.xml

<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <declare-styleable name="CustomStyle">
        <attr name="myCustomColor"
            format="reference|color"/>
    </declare-styleable>
</resources>

And that custom attribute can be used in my light and dark styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
    <style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
        <item name="myCustomColor">@color/white</item> <!-- @color/black in the dark style config -->
    </style>
</resources>

And then I can use it anywhere I want, both in code or in layouts:

<com.google.android.material.imageview.ShapeableImageView
    android:layout_width="24dp"
    android:layout_height="24dp"
    android:background="?myCustomColor"

This is very simple and practical because it resolves the light and dark color automatically and all I need is to use the custom color reference.

But in Compose with Material 3 I can't find anywhere explained how something like this can be done.

In Material 2 it was possible to something like this:

val Colors.myExtraColor: Color
    get() = if (isLight) Color.Red else Color.Green

But in Material 3 this is no longer possible:

Unlike the M2 Colors class, the M3 ColorScheme class doesn’t include an isLight parameter. In general you should try and model whatever needs this information at the theme level.

https://developer.android.com/jetpack/compose/designsystems/material2-material3#islight

I tried looking for a solution here in SO but so far found nothing that would work for this.

Is there a simple way of achieving this like it is possible with the non-Compose version as I exemplified above?


Solution

  • CompositionLocalProvider is the way for that.

    Colors.kt:

    import androidx.compose.material3.darkColorScheme
    import androidx.compose.material3.lightColorScheme
    import androidx.compose.ui.graphics.Color
    
    val LightPrimary = Color(color = 0xFF6750A4)
    val LightOnPrimary = Color(color = 0xFFFFFFFF)
    val LightPrimaryContainer = Color(color = 0xFFEADDFF)
    val LightOnPrimaryContainer = Color(color = 0xFF21005D)
    val LightInversePrimary = Color(color = 0xFFD0BCFF)
    val LightSecondary = Color(color = 0xFF625B71)
    val LightOnSecondary = Color(color = 0xFFFFFFFF)
    val LightSecondaryContainer = Color(color = 0xFFE8DEF8)
    val LightOnSecondaryContainer = Color(color = 0xFF1D192B)
    val LightTertiary = Color(color = 0xFF7D5260)
    val LightOnTertiary = Color(color = 0xFFFFFFFF)
    val LightTertiaryContainer = Color(color = 0xFFFFD8E4)
    val LightOnTertiaryContainer = Color(color = 0xFF31111D)
    val LightBackground = Color(color = 0xFFFFFBFE)
    val LightOnBackground = Color(color = 0xFF1C1B1F)
    val LightSurface = Color(color = 0xFFFFFBFE)
    val LightOnSurface = Color(color = 0xFF1C1B1F)
    val LightSurfaceVariant = Color(color = 0xFFE7E0EC)
    val LightOnSurfaceVariant = Color(color = 0xFF49454F)
    val LightInverseSurface = Color(color = 0xFF313033)
    val LightInverseOnSurface = Color(color = 0xFFF4EFF4)
    val LightSurfaceTint = Color(color = 0xFF6750A4)
    val LightError = Color(color = 0xFFB3261E)
    val LightOnError = Color(color = 0xFFFFFFFF)
    val LightErrorContainer = Color(color = 0xFFF9DEDC)
    val LightOnErrorContainer = Color(color = 0xFF410E0B)
    val LightOutline = Color(color = 0xFF79747E)
    val LightOutlineVariant = Color(color = 0xFFCAC4D0)
    val LightScrim = Color(color = 0xFF4B484E)
    
    val DarkPrimary = Color(color = 0xFFD0BCFF)
    val DarkOnPrimary = Color(color = 0xFF381E72)
    val DarkPrimaryContainer = Color(color = 0xFF4F378B)
    val DarkOnPrimaryContainer = Color(color = 0xFFEADDFF)
    val DarkInversePrimary = Color(color = 0xFF6750A4)
    val DarkSecondary = Color(color = 0xFFCCC2DC)
    val DarkOnSecondary = Color(color = 0xFF332D41)
    val DarkSecondaryContainer = Color(color = 0xFF4A4458)
    val DarkOnSecondaryContainer = Color(color = 0xFFE8DEF8)
    val DarkTertiary = Color(color = 0xFFEFB8C8)
    val DarkOnTertiary = Color(color = 0xFF492532)
    val DarkTertiaryContainer = Color(color = 0xFF633B48)
    val DarkOnTertiaryContainer = Color(color = 0xFFFFD8E4)
    val DarkBackground = Color(color = 0xFF1C1B1F)
    val DarkOnBackground = Color(color = 0xFFE6E1E5)
    val DarkSurface = Color(color = 0xFF1C1B1F)
    val DarkOnSurface = Color(color = 0xFFE6E1E5)
    val DarkSurfaceVariant = Color(color = 0xFF49454F)
    val DarkOnSurfaceVariant = Color(color = 0xFFCAC4D0)
    val DarkInverseSurface = Color(color = 0xFFE6E1E5)
    val DarkInverseOnSurface = Color(color = 0xFF313033)
    val DarkSurfaceTint = Color(color = 0xFFD0BCFF)
    val DarkError = Color(color = 0xFFF2B8B5)
    val DarkOnError = Color(color = 0xFF601410)
    val DarkErrorContainer = Color(color = 0xFF8C1D18)
    val DarkOnErrorContainer = Color(color = 0xFFF9DEDC)
    val DarkOutline = Color(color = 0xFF938F99)
    val DarkOutlineVariant = Color(color = 0xFF49454F)
    val DarkScrim = Color(color = 0xFFB4B0BB)
    
    val LightColorScheme = lightColorScheme(
        primary = LightPrimary,
        onPrimary = LightOnPrimary,
        primaryContainer = LightPrimaryContainer,
        onPrimaryContainer = LightOnPrimaryContainer,
        inversePrimary = LightInversePrimary,
        secondary = LightSecondary,
        onSecondary = LightOnSecondary,
        secondaryContainer = LightSecondaryContainer,
        onSecondaryContainer = LightOnSecondaryContainer,
        tertiary = LightTertiary,
        onTertiary = LightOnTertiary,
        tertiaryContainer = LightTertiaryContainer,
        onTertiaryContainer = LightOnTertiaryContainer,
        background = LightBackground,
        onBackground = LightOnBackground,
        surface = LightSurface,
        onSurface = LightOnSurface,
        surfaceVariant = LightSurfaceVariant,
        onSurfaceVariant = LightOnSurfaceVariant,
        surfaceTint = LightSurfaceTint,
        inverseSurface = LightInverseSurface,
        inverseOnSurface = LightInverseOnSurface,
        error = LightError,
        onError = LightOnError,
        errorContainer = LightErrorContainer,
        onErrorContainer = LightOnErrorContainer,
        outline = LightOutline,
        outlineVariant = LightOutlineVariant,
        scrim = LightScrim
    )
    
    val DarkColorScheme = darkColorScheme(
        primary = DarkPrimary,
        onPrimary = DarkOnPrimary,
        primaryContainer = DarkPrimaryContainer,
        onPrimaryContainer = DarkOnPrimaryContainer,
        inversePrimary = DarkInversePrimary,
        secondary = DarkSecondary,
        onSecondary = DarkOnSecondary,
        secondaryContainer = DarkSecondaryContainer,
        onSecondaryContainer = DarkOnSecondaryContainer,
        tertiary = DarkTertiary,
        onTertiary = DarkOnTertiary,
        tertiaryContainer = DarkTertiaryContainer,
        onTertiaryContainer = DarkOnTertiaryContainer,
        background = DarkBackground,
        onBackground = DarkOnBackground,
        surface = DarkSurface,
        onSurface = DarkOnSurface,
        surfaceVariant = DarkSurfaceVariant,
        onSurfaceVariant = DarkOnSurfaceVariant,
        surfaceTint = DarkSurfaceTint,
        inverseSurface = DarkInverseSurface,
        inverseOnSurface = DarkInverseOnSurface,
        error = DarkError,
        onError = DarkOnError,
        errorContainer = DarkErrorContainer,
        onErrorContainer = DarkOnErrorContainer,
        outline = DarkOutline,
        outlineVariant = DarkOutlineVariant,
        scrim = DarkScrim
    )
    

    Theme.kt:

    import android.os.Build
    import androidx.compose.foundation.isSystemInDarkTheme
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.dynamicDarkColorScheme
    import androidx.compose.material3.dynamicLightColorScheme
    import androidx.compose.runtime.Composable
    import androidx.compose.ui.platform.LocalContext
    
    @Composable
    fun AppTheme(
        useDarkTheme: Boolean = isSystemInDarkTheme(),
        useDynamicColors: Boolean = true,
        content: @Composable () -> Unit
    ) {
        val colorScheme = when {
            useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
                if (useDarkTheme) dynamicDarkColorScheme(context = LocalContext.current)
                else dynamicLightColorScheme(context = LocalContext.current)
            }
    
            useDarkTheme -> DarkColorScheme
            else -> LightColorScheme
        }
    
        MaterialTheme(
            colorScheme = colorScheme,
            content = content
        )
    }
    

    Above we have a basic code for defining theme colors in Material 3.

    To add custom colors or anything else using CompositionLocalProvider, we first need to create a data class that will contain the values/types. Let's assume we want 3 new color types, which can be different depending on the light or dark theme.

    To do this, let's add a new file to the project:

    CustomColorsPalette.kt

    import androidx.compose.runtime.Immutable
    import androidx.compose.runtime.staticCompositionLocalOf
    import androidx.compose.ui.graphics.Color
    
    @Immutable
    data class CustomColorsPalette(
        val extraColor1: Color = Color.Unspecified,
        val extraColor2: Color = Color.Unspecified,
        val extraColor3: Color = Color.Unspecified
    )
    
    val LightExtraColor1 = Color(color = 0xFF29B6F6)
    val LightExtraColor2 = Color(color = 0xFF26A69A)
    val LightExtraColor3 = Color(color = 0xFFEF5350)
    
    val DarkExtraColor1 = Color(color = 0xFF0277BD)
    val DarkExtraColor2 = Color(color = 0xFF00695C)
    val DarkExtraColor3 = Color(color = 0xFFC62828)
    
    val LightCustomColorsPalette = CustomColorsPalette(
        extraColor1 = LightExtraColor1,
        extraColor2 = LightExtraColor2,
        extraColor3 = LightExtraColor3
    )
    
    val DarkCustomColorsPalette = CustomColorsPalette(
        extraColor1 = DarkExtraColor1,
        extraColor2 = DarkExtraColor2,
        extraColor3 = DarkExtraColor3
    )
    
    val LocalCustomColorsPalette = staticCompositionLocalOf { CustomColorsPalette() }
    

    After that, we can go back to our Theme.kt file to add the logic and finish configuring the CompositionLocalProvider.

    Theme.kt:

    import android.os.Build
    import androidx.compose.foundation.isSystemInDarkTheme
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.dynamicDarkColorScheme
    import androidx.compose.material3.dynamicLightColorScheme
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.CompositionLocalProvider
    import androidx.compose.ui.platform.LocalContext
    
    @Composable
    fun AppTheme(
        useDarkTheme: Boolean = isSystemInDarkTheme(),
        useDynamicColors: Boolean = true,
        content: @Composable () -> Unit
    ) {
        // "normal" palette, nothing change here
        val colorScheme = when {
            useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
                if (useDarkTheme) dynamicDarkColorScheme(context = LocalContext.current)
                else dynamicLightColorScheme(context = LocalContext.current)
            }
    
            useDarkTheme -> DarkColorScheme
            else -> LightColorScheme
        }
    
        // logic for which custom palette to use
        val customColorsPalette =
            if (useDarkTheme) DarkCustomColorsPalette
            else LightCustomColorsPalette
    
        // here is the important point, where you will expose custom objects
        CompositionLocalProvider(
            LocalCustomColorsPalette provides customColorsPalette // our custom palette
        ) {
            MaterialTheme(
                colorScheme = colorScheme, // the MaterialTheme still uses the "normal" palette
                content = content
            )
        }
    }
    

    And finally we can use the colors as follows:

    MainActivity.kt

    AppTheme {
        Scaffold { innerPadding ->
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(paddingValues = innerPadding),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(text = "Default Material 3 color on background")
    
                Card {
                    Text(text = "Default Material 3 color for elevation")
                }
    
                Text(
                    text = "One of customs colors",
                    color = LocalCustomColorsPalette.current.extraColor1
                )
    
                Text(
                    text = "Other custom color",
                    color = LocalCustomColorsPalette.current.extraColor2
                )
            }
        }
    }
    

    light theme sample dark theme sample

    The colors we added can be used through the LocalCustomColorsPalette.current, as seen in the example above. It's exactly the same as other Compose objects, such as the LocalTextStyle.current, LocalDensity.current, etc.

    There is the possibility of applying a trick to modify the call to these custom objects to be similar to the patterns that are inside the MaterialTheme object, for that just add the following code on CustomColorsPalette.kt:

    // ...
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.ReadOnlyComposable
    // ...
    
    val MaterialTheme.customColorsPalette: CustomColorsPalette
        @Composable
        @ReadOnlyComposable
        get() = LocalCustomColorsPalette.current
    

    Now it will be possible to call the colors like this:

    Text(
        text = "Default color scheme remains available",
        color = MaterialTheme.colorScheme.onBackground
    )
    
    Text(
        text = "One of customs colors",
        color = MaterialTheme.customColorsPalette.extraColor1
    )
    
    Text(
        text = "Other custom color",
        color = MaterialTheme.customColorsPalette.extraColor2
    )