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() }
- The
CustomColorsPalette data class
has 3 colors.
- 6 colors were added, where 3 are light and 3 are dark.
- Two val types of
CustomColorsPalette
were created, one with light colors and another with dark colors.
- A
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
)
CompositionLocalProvider
is what are you looking for, this answer can help you to achieve that. The example was written with M2, but it is independent of it, it will work the same in M3 and can even be used for other things, likedp
,Shapes
, etc. – GrantgrantaMaterialTheme.colorScheme.background
, right? Or it allows both to be used without issue? I already have alightColorScheme
/darkColorScheme
configured, but your suggestion appears to be incompatible with this, as in it's either one or the other. – BeeneTheme.kt
here for example of what I currently have developer.android.com/codelabs/jetpack-compose-theming#3 – BeenelightColorScheme
anddarkColorScheme
to pass into myMaterialTheme
color scheme, I cannot understand how I can also include theCustomColorsPalette
you've suggested since it appears that I can only use one OR the other, but not both at the same time. If I am misunderstanding this, could you please post an answer to this question with an example of achieving it so I can understand it better? – Beene