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
)
@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 astaticCompositionLocalOf
are not tracked by the composer, and changing the value provided in theCompositionLocalProvider
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 astaticCompositionLocalOf
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 theCompositionLocalProvider
and should consider using astaticCompositionLocalOf
. A color, or another theme, like value, might change or even be animated; therefore, acompositionLocalOf
should be used.
staticCompositionLocalOf
creates aProvidableCompositionLocal
which can be used in a call toCompositionLocalProvider
. Similar toMutableList
vs.List
, if the key is made public asCompositionLocal
instead ofProvidableCompositionLocal
, it can be read usingCompositionLocal.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
)
}
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
}
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
)
Adjusting colors based on the system theme
@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.
@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.
@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.
Top comments (0)