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?
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() }
CustomColorsPalette data class
has 3 colors.CustomColorsPalette
were created, one with light colors and another with dark colors.staticCompositionLocalOf
is created based on docs from CompositionLocalProvider.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
)
}
}
}
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
)