Building a Design System implementation using Jetpack Compose — Part1 (Theme)

Part 1 (You are here)

Part 2

Recently working on a Design System project using Jetpack Compose building the implementation on Android application. I Hope can through this article to share with you how I did it and get feedback from the world.

Google already provided a great Design System framework called Material Design which lets you easily change the styles and has build-in components to cover most of the common use cases. But in the real world, especially if the Design System you are using is platform united, (like the project I’m working on, the Design System is supporting Android, iOS, and Web at the same time), the Material Design doesn’t fit perfectly.

Before

Knowledge required:

  • Understanding the concept and key elements of Design System.
  • Understanding Jetpack Compose and at least went through the tutorial provided by Google.

If you are looking for how to build a Design System implementation using the traditional XML UI system, check these articles I wrote.

The code examples below are built on Jetpack Compose 0.1.0-dev13 using Android Studio 4.2 Canary 7, APIs may change in the future updates.

Design

I’m going to use the simple design example I made to demonstrate how to build the implementation, it’s a simplified “unreal-world” design system, but the idea is close enough I guarantee.

Image for post
Image for post

As you can see, I created three styles (color, size, typography) which are basically included in any common Design System, let me implement them first.

Styles

DLS here stands for Design (Language) System used as a prefix for avoiding conflict

Colors

Nothing fancy here.

object DlsColors {
val primary = Color(0xFF3366FF)
val background = Color(0xFFFFFFFF)
val backgroundReverse = Color(0xFF192038)
val basic = Color(0xFF8F9BB3)
val disable = basic.copy(alpha = 0.24f)
val text = Color(0xFF192038)
val textReverse = Color(0xFFFFFFFF)
val success = Color(0xFF00E096)
val link = Color(0xFF0095FF)
val warning = Color(0xFFFFAA00)
val error = Color(0xFFFF3D71)
}

For supporting dark mode or other color themes if needed in the future. An interface is needed for the color palette.

interface DlsColorPalette {
val primary: Color
val background: Color
val basic: Color
val disable: Color
val text: Color
val textReverse: Color
val success: Color
val link: Color
val warning: Color
val error: Color

val materialColors: ColorPalette
}

The materialColors here is used for overriding the MaterialTheme color, you will see the code example later.

fun dlsLightColorPalette(): DlsColorPalette = object : DlsColorPalette {
override val primary: Color = DlsColors.primary
override val background: Color = DlsColors.background
override val basic: Color = DlsColors.basic
override val disable: Color = DlsColors.disable
override val text: Color = DlsColors.text
override val textReverse: Color = DlsColors.textReverse
override val success: Color = DlsColors.success
override val link: Color = DlsColors.link
override val warning: Color = DlsColors.warning
override val error: Color = DlsColors.error

override val materialColors: ColorPalette = lightColorPalette(
primary = DlsColors.primary,
surface = DlsColors.backgroundReverse,
onSurface = DlsColors.textReverse
)
}

Function dlsLightColorPalette returns a DlsColorPalette and overrides a few of material colors.

About which material colors should be overridden is totally up to you, changing the default colors of the MaterialTheme can reduce color setting codes, but either way I recommend always try to explicitly set the colors when building the UI for better future maintenance.

Add another DlsColorPalette for dark mode:

fun dlsDarkColorPalette(): DlsColorPalette = object : DlsColorPalette {
override val primary: Color = DlsColors.primary
override val background: Color = DlsColors.backgroundReverse
override val basic: Color = DlsColors.basic
override val disable: Color = DlsColors.disable
override val text: Color = DlsColors.textReverse
override val textReverse: Color = DlsColors.text
override val success: Color = DlsColors.success
override val link: Color = DlsColors.link
override val warning: Color = DlsColors.warning
override val error: Color = DlsColors.error

override val materialColors: ColorPalette = darkColorPalette(
primary = DlsColors.primary,
surface = DlsColors.background,
onSurface = DlsColors.textReverse
)
}

You can see I switched the colors on background and text to make the dark mode look obvious for demonstration. (In the real design there might be different color definitions for each light and dark mode.)

Sizes

Unlike colors, sizes usually only have one set of variations, so it’s much simpler.

data class DlsSize internal constructor(
val smaller: Dp = 4.dp,
val small: Dp = 8.dp,
val medium: Dp = 16.dp,
val large: Dp = 32.dp,
val larger: Dp = 64.dp
)

Typography

Similar to sizes, typography here is quite simple too. If need to support multiple themes with different fonts, for example, please reference the way how the color palette be made.

data class DlsTypography internal constructor(
val headline1: TextStyle = TextStyle(
fontSize = 36.sp,
fontWeight = FontWeight.Bold,
lineHeight = 48.sp
)
,
val headline2: TextStyle = TextStyle(
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
lineHeight = 40.sp
)
,
val headline3: TextStyle = TextStyle(
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
lineHeight = 40.sp
)
,

......

val label: TextStyle = TextStyle(
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
lineHeight = 16.sp
)
,
val button: TextStyle = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
lineHeight = 16.sp
)
,

val materialTypography: Typography = Typography(
body1 = paragraph1
)
)

The materialTypography here is used for overriding the typography of MaterialTheme, you will see the example later.

Theme

When building an application using Jetpack Compose, probably the whole view is wrapped by MaterialTheme which gives a nice style to the UI. But MaterialTheme doesn’t fit our design since it’s not technically built on material design. So a new theme is needed, but in the meantime, we definitely dont’t want to lose the functionality provided by MaterialTheme.

@Composable
fun DlsTheme(
colors: DlsColorPalette = dlsLightColorPalette(),
typography: DlsTypography = DlsTypography(),
children: @Composable() () -> Unit
) {
Providers(
DlsColorAmbient
provides colors,
DlsTypographyAmbient provides typography,
) {
MaterialTheme(
colors = colors.materialColors,
typography = typography.materialTypography
) {
children()
}
}
}

DlsTheme accepts color palette and typography from outside and passes down the default materialColors and materialTypography to the MaterialTheme. Thinking DlsTheme as a style provider to the MaterialTheme.

One more class contains functions for accessing the current style values provided by them.

object DlsTheme {
@Composable
val colors: DlsColorPalette
get() = DlsColorAmbient.current

@Composable
val typography: DlsTypography
get() = DlsTypographyAmbient.current

@Composable
val sizes: DlsSize
get() = DlsSize()
}

Now I have all the styles and theme defined, let’s move on to see how to use them to build UIs.

Demo

Keeping things simple, this is just a single screen with a text view.

DlsTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = DlsTheme.colors.background
) {
Text(
text = "TEXT",
color = DlsTheme.colors.text,
style = DlsTheme.typography.headline1
)
}
}

You can see the views are wrapped inside DlsTheme and use colors and typography defined in the Design System before.

Image for post
Image for post

Now to make things more fun, let’s add a customized button component using Design System styles, and let the app switch between light and dark mode each time click on it.

@Composable
fun CustomButton(
text: String,
onClick: () -> Unit
) {
Button(
onClick = onClick,
backgroundColor = DlsTheme.colors.primary,
padding = InnerPadding(
start = DlsTheme.sizes.large,
top = DlsTheme.sizes.medium,
end = DlsTheme.sizes.large,
bottom = DlsTheme.sizes.medium
)
)
{
Text(
text = text,
color = DlsTheme.colors.textReverse,
style = DlsTheme.typography.button
)
}
}

And the code of the new screen:

val isDarkState = mutableStateOf(false)

setContent
{
DlsTheme(
colors = if (isDarkState.value) dlsDarkColorPalette() else dlsLightColorPalette()
)
{
Surface(
modifier = Modifier.fillMaxSize(),
color = DlsTheme.colors.background
) {

Column(
modifier = Modifier.gravity(Alignment.CenterVertically)
.wrapContentSize(),
horizontalGravity = Alignment.CenterHorizontally
) {

Text(
text = if (isDarkState.value) "Is Dark" else "Is Light",
color = DlsTheme.colors.text,
style = DlsTheme.typography.headline1
)

Spacer(
modifier = Modifier.preferredHeight(DlsTheme.sizes.medium))

CustomButton(
text = "Button",
onClick = {
isDarkState.value = !isDarkState.value
}
)
}
}
}
}

Here is the final application looks like:

Image for post
Image for post

More

A few more things:

  • MaterialTheme is still not covering all the UI settings, for example, you still need use XML to change the status bar background
  • In Part 2 I will talk about how to build design system components
  • Please leave comments or reach me out on Twitter

Software developer, Tokyo based

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store