DEV Community

Muhammed Esad Cömert
Muhammed Esad Cömert

Posted on • Edited on

Custom Theming in Jetpack Compose

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.

@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
)
Enter fullscreen mode Exit fullscreen mode

@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.

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
    )
}
Enter fullscreen mode Exit fullscreen mode

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.

@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
}
Enter fullscreen mode Exit fullscreen mode

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.

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
)
Enter fullscreen mode Exit fullscreen mode

Adjusting colors based on the system theme

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

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.

@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()
            }
        }
    )
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

Top comments (0)