Jetpack Compose Custom Theme

I am sure that everyone who has used color schemes in Android applications to color the interface has at least once asked the question “How can I change the boring colors of primary, secondary, tertiary, etc. to my own?” This article will talk about using my own color scheme to work with the application.

Boring default theme

The Android app theme that comes with the Android project out of the box seems to me quite inconvenient, because sometimes confusion and headaches begin, which comes with the need to determine in the process of coding in which theme variable the colors you need are located. The article is 0+, so let's start in order. This theme looks like this:

  1. Color palettes defining colors in dark and light themes

  2. Theme Method (Small font to fit everything on one image)

  3. It's weird, but there used to be a theme object where you could get these colors for other components.

Please note that this theme has a dynamic color scheme. Unfortunately or fortunately, this will not work with a custom color scheme!!

A bit of wiki: With the release of Android 12, a dynamic color scheme for apps appeared on devices, allowing them to be colored depending on the wallpaper. This all caused a stormy reaction from users, as a result of which everyone was divided into 3 groups: 1 – those who liked it, 2 – those who didn't care before, and 3 – haters. I belong to the third group, since determining the main colors of the wallpaper sometimes happens in the most funny way. I had black wallpaper on my phone – just a black background, with a small “powder” of red dots in the corner, which I would not have even noticed if all my Google apps had not turned pink 🙂 After that, I conducted several more experiments with black wallpaper, and one of them recolored all the apps in, for some reason, a swamp color, and then I picked up a wallpaper that returned the good old blue-gray color.

So, let's begin!

Palettes

Palettes are a regular data class in which you can define any colors you like.

data class ColorPalette(
    val mainColor: Color,
    val singleTheme: Color,
    val oppositeTheme: Color,
    val buttonColor: Color,
)

I found the first 3 colors convenient for myself: I like to make applications not rainbow, but with one or two main colors, different from the basic ones (Black, White, Gray) – I'm talking about mainColor. Next, singleTheme and OppositeTheme – here I throw white and black in the white theme and vice versa – in the dark theme. Well, and buttonColor for variety in the palette. Now we define palettes for both themes:

val baseLightPalette = ColorPalette(
    mainColor = mainLightColor, // просто цвет из Color.kt 
    singleTheme = Color.White,
    oppositeTheme = Color.Black,
    buttonColor = Color(0xFFEFEEEE)
)
val baseDarkPalette = baseLightPalette.copy(
    mainColor = mainDarkColor,// просто цвет из Color.kt 
    singleTheme = Color.Black,
    oppositeTheme = Color.White,
    buttonColor = Color(0xFF2D2D31)
)

Theme Method

The function that describes the color change will look like this:

fun MainTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) {

    val colors = if (!darkTheme) baseLightPalette
    else baseDarkPalette

    // Место для вызова MaterialTheme 
}

I didn't add MaterialTheme to the code above because Material only uses its own color schemes and that won't work for us. Let's dive in!

The picture above is the code from the MaterialTheme.kt library. Under the hood, this method calls CompositionLocalProvier –
(It’s boring, you don’t have to read it, the rest will be in simple words) CompositionLocalProvider binds values ​​to ProvidableCompositionLocal keys. Reading the CompositionLocal using CompositionLocal. current will return the value provided in CompositionLocalProvider's values ​​parameter for all composable functions called directly or indirectly in the content lambda

In simple words: This will always allow you to have the variable (in our case) colors correctly re-linked depending on the theme, because notice that there is no use of remember here, which allows the code to be re-linked when the value being tracked changes.

Using the example image, we have seen what colorScheme is, and now we will figure out LocalColorScheme, which calls provides.

Here staticCompositionLocalOf is used. Now there will also be a lot of text that can be skipped:
Create a CompositionLocal key that can be provided using CompositionLocalProvider.
Unlike compositionLocalOf, reads of a staticCompositionLocalOf are not tracked by the composer and changing the value provided in the CompositionLocalProvider call will cause the entirety of the content to be recomposed instead of just the places where in the composition the local value is used. This lack of tracking, however, makes a staticCompositionLocalOf more efficient when the value provided is highly unlikely to or will never change. For example, the android context, font loaders, or similar shared values, are unlikely to change for the components in the content of a CompositionLocalProvider and should consider using a staticCompositionLocalOf. A color, or other theme like value, might change or even be animated therefore a compositionLocalOf should be used.

In simple words: If something changes often – compositionLocalOf, rarely – staticCompositionLocalOf. staticCompositionLocalOf causes recomposition of all content, and compositionLocalOf – only those places where it is contained.

Bottom line: staticCompositionLocalOf will help you avoid unnecessary percompositions because the color scheme is used throughout the entire application.

val LocalColors = staticCompositionLocalOf<ColorPalette> {
    error("Colors composition error")
}

Here the color scheme is specified as the type, and in the lambda the parameter defaultFactory – a value factory to supply a value when a value is not provided. This factory is called when no value is provided through a CompositionLocalProvider of the caller of the component using CompositionLocal.current. If no reasonable default can be provided then consider throwing an exception.

In simple words: the lambda will be called if the LocalColors parameter is not handled via CompositionLocalProvider. I don't do this, because we will explicitly write this in CompositionLocalProvider later.

Let's finish the Method of the topic:

@Composable
fun MainTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) {

    val colors = if (!darkTheme) baseLightPalette
    else baseDarkPalette

    CompositionLocalProvider(
        LocalColors provides colors,
        content = content
    )
}

Similarly, you can place absolutely any variables in CompositionLocalProvider.

The main theme is ready, but for now we can only “mentally observe” the changes in the color scheme, since in Material this theme is already built into all components, but we do not have it.

Subject matter

With a singleton we will be able to get colors from any part of the application, and here is how we will do it:

object ScheduleTheme {
    val colors: ColorPalette
        @Composable
        get() = LocalColors.current
}

Now it's still worth going back and reading more about the staticCompositionLocalOf factory, here's a quote:

This factory is called when no value is provided through a CompositionLocalProvider of the caller of the component using CompositionLocal.current.

What this says is that the current value can be obtained using the current property, so we simply make a Composable getter in the object and return that value there.

And now you can get the current theme palette using ScheduleTheme.colors.

Let's check? One of my pet projects with a schedule.

Conclusion

This article covered the process of creating a custom color scheme and theme for an Android app.

Moreover, the article included data classes with the expectation that the reader has a minimal understanding of how they differ from regular classes and why they are convenient in this case.

No errors, no warnings, gentlemen and ladies!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *