Featured in Android Weekly #640
Jetpack Compose, Android’s modern UI toolkit, comes with Material Design out of the box. But what if you want something different? What if your designer has built his design system and does not want to follow the material guidelines or the whole system?
Well! You don’t have to worry its pretty easy to do with Jetpack/Multiplatform Compose as compared to what we used to do with color files and style files with XML back in the day.
There are a few options to implement it and we shall be discussing all those one by one.
Option 1: Extending Material Theme
The easiest way to customize is by adding to the existing Material Theme. Just like an extension method you can add extension properties to a class and using that concept let’s say you want to add a new color for warning messages using material 3:
val ColorScheme.warning: Color
@Composable
get() = if (isSystemInDarkTheme()) Blue300 else Blue700
With material 2:
val Colors.warning: Color
@Composable
get() = if (isSystemInDarkTheme()) DarkBlue50 else DarkBlue
Now to use this
//material 2
MaterialTheme.colors.warning
//Material 3
MaterialTheme.colorScheme.warning
Simple, right?
Option 2: Customising Parts of Material Theme
Maybe you like Material’s colors, but want your own typography and shapes. No problem! Here’s how you could do that:
@Composable
fun MyCustomTheme(content: @Composable () -> Unit) {
val myTypography = Typography(
bodyLarge = TextStyle(fontSize = 16.sp, fontFamily = FontFamily.Your_fontfamily),
titleLarge = TextStyle(fontSize = 32.sp, fontFamily = FontFamily.Your_other_fontfamily)
)
val myShapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(8.dp),
large = RoundedCornerShape(12.dp)
)
MaterialTheme(
typography = myTypography,
shapes = myShapes,
content = content
)
}
Option 3: Creating a Totally New Design System
Option 1 and 2 are pretty nice but still they might not be the right choice in some cases. What if you need to define more shape style or you want to use consistent spacing all around the app? how would you do it? for scenarios like these you would want to create your own custom design system and its actually very easy to do so with the power of Compose.
Before starting first we need to get to know about an important topic of Compose which is Composition Local. I will suggest you to read this if you arent familiar with this concept already I will be waiting for you ❤
Using Composition Locals you can create your own theme entirely.
Firstly we will device a mechanism for all the extra colors that we need which are not in the Material Theme. We will start by creating a class called
`ExtendedColors`.
/**
* Data class representing the extended colors used in the custom theme.
* These are defined in figma design system
*/
data class ExtendedColors(
val primaryVariantLight: Color,
val primaryVariantLightBG: Color,
val errorLight: Color,
val primaryLight: Color,
val primaryLightBG: Color,
val lightGreenBG1: Color,
val primaryLightBG2: Color,
val neutralBlue: Color,
val neutralWhite: Color,
val colorWhiteTransparent: Color,
val greyLight: Color,
val grey: Color,
val greyMid: Color,
val fieldPlaceHolderText: Color,
val greyDark: Color,
val greyBg: Color,
val greyDarkText: Color,
val warning: Color,
val warningLight: Color,
val secondarySurface: Color,
val secondaryVariant : Color,
val background : Color,
val backgroundExtraLight : Color,
val error : Color,
val onError : Color,
)
This is an Example of how your extended colors class might look. Once we have the class, lets create the dark and light object of this class.
I will be only sharing example of light but its the same for dark as well. Just another object with dark color scheme.
val lightExtendedColors =
ExtendedColors(
background = Grey20,
error = Red50,
onError = Grey10,
primaryVariantLight = Blue20,
primaryVariantLightBG = Blue20,
errorLight = Red20,
primaryLight = Green10,
primaryLightBG = Green10,
greyLight = Grey30,
grey = Grey40,
greyMid = Grey50,
greyDark = Grey60,
fieldPlaceHolderText = Grey60,
greyBg = Grey40,
greyDarkText = Grey60,
warningLight = Yellow20,
warning = Yellow50,
primaryDark = Green70,
youtube = Red70,
secondarySurface = Grey60,
productViewBG = Grey110,
toastBackground = Grey110,
toastText = Grey90,
neutralBlue = DarkBlue50,
neutralWhite = Grey10,
primaryLightBG2 = Green30,
colorWhiteTransparent = WhiteTransparent,
lightGreenBG1 = Green20,
backgroundTwo = Grey110
)
Now that we have defined the dark and light color scheme now we need to create a composition local which will provide us these schemes.
/**
* A [CompositionLocal] that provides the light theme color palette.
*/
val LocalAppColors = staticCompositionLocalOf<ExtendedColors?> { null }
/**
* A [CompositionLocal] that provides the light theme color palette.
*/
val LocalAppColors = staticCompositionLocalOf { lightExtendedColors }
You can either initialise this as null or use one of the color schemes you defined.
Now we need to add these to the top level function which in my case is the theme
@Composable
fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
// CompositionLocalProvider is used to provide custom theme values to the Compose hierarchy.
CompositionLocalProvider(
// Provide the extended colors based on the darkTheme parameter.
LocalAppColors provides if (darkTheme) darkExtendedColors else lightExtendedColors,
) {
MaterialTheme(
typography = Type.typography,
content = content,
colors = getColorPallete(darkTheme)
)
}
}
and once thats done you can start using these colors in your code like this
@Composable
fun CustomButtonAndText() {
Column(modifier = Modifier.padding(16.dp)) {
Button(
onClick = { /* Handle click */ },
colors = ButtonDefaults.buttonColors(
backgroundColor = LocalAppColors.current.primary,
contentColor = LocalAppColors.current.onPrimary
)
) {
Text("Click Me")
}
Text(
text = "Hello, World!",
color = LocalAppColors.current.primaryLightBG,
modifier = Modifier.padding(top = 16.dp)
)
}
}
Job Offers
Thats all about colors but using the same process you can define anything that your theme needs for example Shapes.
@Immutable
data class ExtendedShapes(
val cardShape: Shape,
val bottomSheetShape: Shape,
val buttonShape: Shape,
val chipShape: Shape,
val textFieldShape: Shape,
val variantShape: Shape,
val imageShape: Shape,
val pillShape: Shape,
val statusShape: Shape
)
/**
* An instance of the custom [ExtendedShapes] class with specific rounded corner shapes for cards
* and bottom sheets.
*/
val appShapes =
ExtendedShapes(
cardShape = RoundedCornerShape(12.dp),
bottomSheetShape = RectangleShape,
buttonShape = RoundedCornerShape(8.dp),
chipShape = RoundedCornerShape(14.dp),
textFieldShape = RoundedCornerShape(8.dp),
variantShape = RoundedCornerShape(8.dp),
imageShape = RoundedCornerShape(8.dp),
pillShape = RoundedCornerShape(30.dp),
statusShape = RoundedCornerShape(6.dp)
)
/**
* A [CompositionLocal] to store the custom shapes, making them accessible throughout the Compose
* hierarchy.
*/
val LocalAppShapes = compositionLocalOf { appShapes }
Spacing:
/**
* Data class representing the custom spacing values used in the custom theme.
* @param xxs The extra extra small spacing value.
* @param xs The extra small spacing value.
* @param s The small spacing value.
* @param m The medium spacing value.
* @param semiLargeSpacing The semi-large spacing value for internal padding of cards.
* @param l The large spacing value this will be used for horizontal paddings of all screens.
* @param xl The extra large spacing value.
* @param xxl The humungous spacing value for topbars name derived from figma.
*/
@Immutable
data class ExtendedSpacing (
val xxs:Dp =2.dp ,
val xs: Dp = 4.dp,
val s : Dp = 8.dp,
val m : Dp = 12.dp,
val semiLargeSpacing: Dp = 14.dp,
val l : Dp = 16.dp,
val lHalf : Dp = 16.5.dp,
val xl : Dp = 20.dp,
val xxl : Dp = 24.dp,
val iconSmall : Dp = 20.dp,
val pillIcon : Dp = 15.dp
)
val LocalAppSpacing = compositionLocalOf {
ExtendedSpacing()
}
and adding them to the Composition Local Provider
@Composable
fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
// CompositionLocalProvider is used to provide custom theme values to the Compose hierarchy.
CompositionLocalProvider(
// Provide the extended colors based on the darkTheme parameter.
LocalAppColors provides lightExtendedColors,
// Provide the custom shapes defined by markazShapes.
LocalAppShapes provides appShapes,
// Provide the custom spacing values.
LocalAppSpacing provides ExtendedSpacing(),
) {
// Apply the MaterialTheme with the custom typography and the content provided in the composable.
MaterialTheme(
typography = Type.typography,
content = content,
colors = getColorPallete(darkTheme)
)
}
}
Using the Shapes, Colors and Spacing
@Composable
fun CustomCard() {
Card(
backgroundColor = LocalAppColors.current.surface,
contentColor = LocalAppColors.current.onSurface,
shape = LocalAppShapes.current.cardShape,
modifier = Modifier
.padding(LocalMarkazSpacing.current.m)
.fillMaxWidth()
) {
Column(
modifier = Modifier
.padding(LocalAppSpacing.current.l)
) {
Text(
text = "Card Title",
color = LocalAppColors.current.primary,
modifier = Modifier.padding(bottom = LocalMarkazSpacing.current.s)
)
Text(
text = "This is an example of a card using extended spacing, colors, and shapes.",
color = LocalAppColors.current.onSurface
)
}
}
}
Tip:
Instead of using local every time you can use a variable to store it and then use the variable to access the values
@Composable
fun CustomCard() {
// Store the local values in variables
val colors = LocalAppColors.current
val shapes = LocalAppShapes.current
val spacing = LocalAppSpacing.current
Card(
backgroundColor = colors.surface,
contentColor = colors.onSurface,
shape = shapes.cardShape,
modifier = Modifier
.padding(spacing.m)
.fillMaxWidth()
) {
Column(
modifier = Modifier
.padding(spacing.l)
) {
Text(
text = "Card Title",
color = colors.primary,
modifier = Modifier.padding(bottom = spacing.s)
)
Text(
text = "This is an example of a card using extended spacing, colors, and shapes.",
color = colors.onSurface
)
}
}
}
Wrapping Up
Creating a custom design system in Jetpack Compose can be as simple or as complex as you need. Start by extending Material, replace parts of it, or build something entirely new. The choice is yours!
Remember, a consistent design system makes your app look professional and helps users navigate with ease. So have fun creating, but keep user experience in mind.
Code can be found in this GitHub repo:
GitHub – Kashif-E/Custom-Design-System-Compose-Mutiplatform: This is a Kotlin-based project that…
This is a Kotlin-based project that leverages Jetpack/Muliplatform Compose for building modern, native UI. It includes…
github.com
I am open to work you can connect with me on Linkedin or Twitter or maybe hit me up on Instagram and I create videos on Youtube all about Kotlin.
Happy coding! ❤
This article is previously published on proandroiddev.com