Custom Theming in Jetpack Compose
Tags: android, kotlin, jetpackcompose, mobile
Published: 2023-03-06 13:12:29

Can't you fully reflect the colors of your brand when using Material Theme? Or does Material Theme just sound boring? Or don't you want your app to be some kind of stereotypical Google app?

You can create your custom design system in Compose even if Google recommends the Material Design system. But, Composables is built on `MaterialTheme`, so if you want to create your custom design system, you must also create your custom Composables.

## Creating Custom Classes

First of all, you need to create your custom data classes, such as CustomColors, CustomTypography, and CustomElevation.

```kotlin
@Immutable
data class CustomColors(
    val container: Color,
    val content: Color,
    val background: Color,
    val onBackground: Color
)

@Immutable
data class CustomTypography(
    val title: TextStyle,
    val body: TextStyle,
    val label: TextStyle
)

@Immutable
data class CustomElevation(
    val default: Dp,
    val pressed: Dp
)
```

> [`@Immutable`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/Immutable) annotation to tell Compose compiler that this object is immutable for optimization, so without using it there will be unnecessary re-composition that might get triggered.

In addition, a nice thing about creating Custom Theming is that you can define properties according to your needs without being tied to the Material Design system.

You just created your classes for the customized theme, now you will see how to use them.

## Creating CompositionLocal Values

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 the `CompositionLocalProvider` and should consider using a `staticCompositionLocalOf`. A color, or another theme, like value, might change or even be animated; therefore, a `compositionLocalOf` should be used.
`staticCompositionLocalOf` creates a `ProvidableCompositionLocal` which can be used in a call to `CompositionLocalProvider`. Similar to `MutableList` vs. `List`, if the key is made public as `CompositionLocal` instead of `ProvidableCompositionLocal`, it can be read using `CompositionLocal.current` but not re-provided.

```kotlin
val LocalCustomColors = staticCompositionLocalOf {
    CustomColors(
        container = Color.Unspecified,
        content = Color.Unspecified,
        background = Color.Unspecified,
        onBackground = Color.Unspecified
    )
}

val LocalCustomTypography = staticCompositionLocalOf {
    CustomTypography(
        title = TextStyle.Default,
        body = TextStyle.Default,
        label = TextStyle.Default
    )
}

val LocalCustomElevation = staticCompositionLocalOf {
    CustomElevation(
        default = Dp.Unspecified,
        pressed = Dp.Unspecified
    )
}
```

## Creating Custom Theme

After creating `CompositionLocal` values, you need to assign them their actual values and provide them with `CompositionLocalProvider`. Finally, you will be able to access these provided objects by creating the `CustomTheme` object.

```kotlin
@Composable
fun CustomTheme(content: @Composable () -> Unit) {
    val colors = CustomColors(
        container = container,
        content = content,
        background = background,
        onBackground = onBackground
    )
    val elevation = CustomElevation(
        default = 0.dp,
        pressed = 0.dp
    )
    val typography = CustomTypography(
        title = TextStyle(
            fontSize = 32.sp,
            fontWeight = FontWeight.Bold
        ),
        body = TextStyle(fontSize = 20.sp),
        label = TextStyle(fontSize = 16.sp)
    )
    CompositionLocalProvider(
        LocalCustomColors provides colors,
        LocalCustomTypography provides typography,
        LocalCustomElevation provides elevation,
        content = content
    )
}

object CustomTheme {
    val colors: CustomColors
        @Composable
        get() = LocalCustomColors.current
    val typography: Typography
        @Composable
        get() = LocalCustomTypography.current
    val elevation: CustomElevation
        @Composable
        get() = LocalCustomElevation.current
}
```

> Remember that you need to create the color values given to the CustomColors class. You can use them by defining them into ui/theme/Color.kt.

## Dark Theme Integration

Even if you do not use the Material Design system, you can easily integrate the dark theme with a few changes.

### Adding color schemes

You need to define 2 different color schemes because the background and onBackground colors will change even if the primary colors do not change.

```kotlin
val lightColors = CustomColors(
        container = light_container,
        content = light_content,
        background = light_background,
        onBackground = light_onBackground
)

val darkColors = CustomColors(
        container = dark_container,
        content = dark_content,
        background = dark_background,
        onBackground = dark_onBackground
)
```

### Adjusting colors based on the system theme

```kotlin
@Composable
fun CustomTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = when {
        darkTheme -> darkColors
        else -> lightColors
    }
    . . .
}
```

## Applying Design to Custom Composables

If you remember, I mentioned at the beginning of the article that if you want to use a custom theme, you need to create your Composables. Composables are based on `MaterialTheme`. Namely, if you are using a custom theme and you do not assign this theme to them, they will still use `MaterialTheme`.

You can create your custom button and use it in your code.

```kotlin
@Composable
fun CustomButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        onClick = onClick,
        modifier = modifier,
        colors = ButtonDefaults.buttonColors(
            containerColor = CustomTheme.colors.container,
            contentColor = CustomTheme.colors.content
        ),
        elevation = ButtonDefaults.buttonElevation(
            defaultElevation = CustomTheme.elevation.default,
            pressedElevation = CustomTheme.elevation.pressed
        ),
        content = {
            // The text style inside the button can also be 
            // applied this way.
            ProvideTextStyle(CustomTheme.typography.label) {
                content()
            }
        }
    )
}
```

> You should remember that the limits for customizing custom composables defined in this way are limited by the parameters you give them. If you need more customization, you have to define related parameters as well.

### Creating custom surface for dark theme compatibility

We just defined separate background colors for light and dark themes. By defining a custom Surface, you can have the background color change to suit these colors.

```kotlin
@Composable
fun CustomSurface(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Surface(
        modifier = modifier.fillMaxSize(),
        color = CustomTheme.colors.background,
        content = content
    )
}
```

> You can use the background() modifier to set the background color for composables that do not have colors attribute, such as DropDownMenu.

## Conclusion

While developing your application with Compose, creating your custom design system is a nice option, but it comes with a lot of challenges. That's why I say don't forget the fact that you have to customize each composable you will use while making your selection.

If you have any questions feel free to leave a comment. You can also find the official document [here](https://developer.android.com/jetpack/compose/designsystems/custom#implementing-fully-custom).