Custom Theming in Jetpack Compose

Custom Theming in Jetpack Compose

·

3 min read

The material theme API in Jetpack compose comes furnished with a set of predefined colors, typography, themes etc.

On many occasions, we may want to create custom themes to use throughout our compose UI.

Let's get started.

Layout

In our root directory, you will come across this folder quite often:

Theme Folder

Here, you have predefined Kotlin files that contain the colors, material themes and typography respectively.

It is quite common to call the color schemes stored in our theme file as shown below:

@Composable
fun MyBox() {
    Box(
        modifier = Modifier
            .background(MaterialTheme.colorScheme.background)
            .padding(16.dp)
    ) {
        Text(text = "Hello there")
    }
}

Here, we call MaterialTheme.colorScheme.background to get a background color defined in the MaterialTheme class.

However, looking at the padding modifier, we are passing a hard-coded value i.e 16.dp. Not only is this not a good practice, but it also makes our code hard to maintain. Hence, we need a way to go around this.

Let us now add an extra file that will hold for us a custom class that contains spacing which we can use throughout our UI instead of using the hard-coded values everywhere.

Spacing.kt

Let us define a new package inside the ui package called custom that will contain all our custom themes.

Inside our Spacing.kt file, let's define a data class that will hold the values of our custom spacing options.

data class Spacing(
    val default: Dp = 0.dp,
    val extraSmall: Dp = 4.dp,
    val small: Dp = 8.dp,
    val medium: Dp = 16.dp,
    val large: Dp = 24.dp,
    val extraLarge: Dp = 32.dp
)

Here, our Spacing data class contains values that we can use in our UI to specify the amount of spacing in Dp that we require.

Next, we need to create an instance of our class that compose can recognize as part of the MaterialTheme Local property by doing it as below.

val LocalSpacing = compositionLocalOf { Spacing() }

However, this is not all as we need to explicitly define the LocalSpacing variable inside the MaterialTheme composable.

Inside our Theme.kt file, we do the following inside our MyTheme composable.

@Composable
fun MyTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    // ...

    // we enclose MaterialTheme composable within CompositionLocalProvider
    CompositionLocalProvider(
        LocalSpacing provides Spacing()
    ) {
        MaterialTheme(
            colorScheme = colorScheme,
            typography = Typography,
            content = content
        )
    }
}

Here, we enclose the MaterialTheme composable within the CompositionLocalProvider composable function to provide an instance of our Spacing class that MaterialTheme can recognize.

Wouldn't it be cool if we could call our spacing values the way we would call values specified in the MaterialTheme class? eg. MaterialTheme.spacing.medium

Well, we can extend the MaterialTheme class inside our Spacing.kt file to get access to this functionality.

val MaterialTheme.spacing: Spacing
    @Composable
    @ReadOnlyComposable
    get() = LocalSpacing.current

And that's it! Here's how our final Spacing.kt file looks:

data class Spacing(
    val default: Dp = 0.dp,
    val extraSmall: Dp = 4.dp,
    val small: Dp = 8.dp,
    val medium: Dp = 16.dp,
    val large: Dp = 24.dp,
    val extraLarge: Dp = 32.dp
)

val LocalSpacing = compositionLocalOf { Spacing() }

val MaterialTheme.spacing: Spacing
    @Composable
    @ReadOnlyComposable
    get() = LocalSpacing.current

Looking back at our initial composable, MyBox composable, we can now modify our padding to use the defined values in our Spacing class as shown below:

@Composable
fun MyBox() {
    Box(
        modifier = Modifier
            .background(MaterialTheme.colorScheme.background)
            .padding(MaterialTheme.spacing.medium)
    ) {
        Text(text = "Hello there")
    }
}

Hurray! We have successfully gotten rid of the hardcoded values and now our code is all clean.

You can view the full project on this GitHub Repository

Thank you and see you soon.