Jetpack Compose Theming: Colors. Exploring colors in Android apps | by Gözde Kaval | May, 2022

Exploring colors in Android apps

Photo by Robert Katzki on Unsplash

Creating screens is much easier with Jetpack compose, however it comes with its own way of working and it might be challenging sometimes.

One topic I was struggling with is colors.

According to Jetpack examples, we should use MaterialTheme rather than old school XML theme. I was never an expert on XML theming either, that’s why I decided to dig Material Theme. Since it’s a big topic, I will cover only colors for this article.

As you may know, whenever we create an app Jetpack Compose feature, the project adds the theme automatically, lets’s have a look.

@Composable
fun MaterialTheme(
colors: Colors = MaterialTheme.colors, // WILL BE FOCUSED
typography: Typography = MaterialTheme.typography, // WILL BE COVERED LATER
shapes: Shapes = MaterialTheme.shapes, // WILL BE COVERED LATER
content: @Composable () -> Unit
)

MaterialTheme comes with these default colors:

But what do these colors mean, what are their usages in the app, and how should we use them? Let’s have a look at the material design website:

A primary color is the color displayed most frequently across your app’s screens and components.

A secondary color provides more ways to accent and distinguish your product.

Your primary color can be used to make a color theme for your app, including dark and light primary color variants.

Just like the primary color, your secondary color can have dark and light variants.

Surface colors affect the surfaces of components.

The background color appears behind scrollable content.

Error color errors indicating in components.

To sum up:

Primary and Secondary(optional) colors represent the brand.

Surface, background, and error colors typically don’t represent the brand.

It means primary/secondary colors must be remarkable and representative of your brand and the rest must be more inconspicuous.

Material Theme default colors are usually white and black for non-brand colors, however, to understand better, we will give different colors to each variable:

We will work with backgroundColor In this article, because content color is decided by background in most cases.

I add Card layouts with Text (without specific color) and give our theme’s colors as background colors.

The code:

The result:

So, how does it work? How do our view components read the colors and apply color changes to inner components? It’s a Text view in our case.

If we go through in Card Layout (or any), we see this extension function. It tells us how the color is picked for the content in most cases. Read more here

fun Colors.contentColorFor(backgroundColor: Color): Color {
return when (backgroundColor) {
primary -> onPrimary
primaryVariant -> onPrimary
secondary -> onSecondary
secondaryVariant -> onSecondary
background -> onBackground
surface -> onSurface
error -> onError
else -> Color.Unspecified
}
}

As we observed from the screen and the theme we have, the colors matched.

Ideally, we shouldn’t need to set the color for every UI. Some of the UIs read from the different color sections by default, it is good to mention some of them.

To understand it better, I added these UI components respectively and all colors are read from the theme. No color is added manually.

Let’s have a look at them one-by-one.

StatusBar

  • Default color: primaryVariant

Scaffold

Used to show different layouts on the screen egbottomSheet or floating action button.

  • Default color: background
  • Content color: onBackground
Scaffold(
topBar = {
/****/

}
)

TopAppBar

Used to display information about the current screen, including actions. It uses different colors by app theme.

  • Default color: primarySurface (light theme:primary/dark theme:surface)
  • Content color: onPrimary(L)/onSurface(D)
TopAppBar(
title = {
Text(text = "Toolbar text")
},
actions = {
IconButton(onClick = { }) {
Icon(Icons.Filled.Favorite, contentDescription = null)
}
}
)

Card

Used for content and actions about a single subject.

  • Default color: surface
  • Content color: onSurface
Card(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.height(100.dp)
) {
Text
(text = "Card Default")
}

Surface

Each surface exists at a given elevation, which influences how that piece of surface visually relates to other surfaces and how that surface casts shadows.

  • Default color: surface
  • Content color: onSurface

FloatingActionButton

Used to define the primary action of a screen.

  • Default color: secondary
  • Content color: onSecondary
FloatingActionButton(onClick = { }) {
Text(text = "FAB", textAlign = TextAlign.Center)
}

Button

Used for user actions.

  • background color: primary
  • Content color: onPrimary
Button(onClick = { }) {
Text(text = "Regular button")
}

TextButton

  • background color: transparent
  • Content color: primary
TextButton(onClick = { }) {
Text(text = "Text button")
}

OutlinedButton

  • background color: surface
  • Content color: primary
OutlinedButton(onClick = { }) {
Text(text = "Outlined button")
}

Switch

Toggle the state of a single item on or off.

  • Default color (Checked): secondaryVariant/secondaryVariantAlpha
  • Default color (Unchecked): surface/onSurface
Switch(checked = true, onCheckedChange = {})

Switch
(checked = false, onCheckedChange = {})

TextField

Used for user input.

  • background color: onSurface.copy(alpha = BackgroundOpacity)
  • Text color: onBackground
  • Bottom Line (Regular): onSurface.copy(alpha =
    TextFieldDefaults.UnfocusedIndicatorLineOpacity)
  • Bottom Line (Error): onError

The original onSurface color is light blue, therefore the color was not visible on the screenshot — especially with opacity. Therefore I used onSecondary color as onSurface only for this screenshot.

To sum up:

  • Not every UI follows color/<onColor> pattern.
  • If we go through the implementation of a UI element, default colors are set. We don’t have to memorize. However, it is useful to know the basics.
  • Let’s set onPrimary,onSecondary,onError, onSurface, onBackground as background color respectively.

The background color is taken from content colors and the content color is read from contentColorFor extension else case, which is Color.Unspecified in this case. If we check the code, Color.Unspecified is black.

val Unspecified = Color(0f, 0f, 0f, 0f, ColorSpaces.Unspecified)

As we observe, the last one doesn’t show the text because the onBackground color and chosen content color are the same.

onBackground = Color(0xFF000000)

It definitely causes some problems:

  • The UI test will pass because the text is actually there, we will not realize it.
  • Take time to find the issue, because the color is actually set properly and who would think about Color.Unspecified color in the first place?
  • User change theme (light->dark), we might end up with a suboptimal user experience.

Therefore, we need to be very careful about the theme colors.

We should always consider the color palette for the dark theme as well, otherwise, users will end up with a bad experience on our app, even frustration! One of the most often-ignored part was applying a dark theme — at least for me. With material compose, it’s very easy to implement.

First of all, we need to decide on the color palette. I follow the same, giving different colors for all.

Now, let’s palette into our theme and run the app.

MaterialTheme(
colors = if (isSystemInDarkTheme()) darkColors else lightColors,
/***/
)

As we see, our app adapts the color changes.

But what a minute, why does our main background stay as white? This is not something we want. Here is why:

  • All UI components are added in Columnelement which doesn’t follow any theme, that’s why our background actually doesn’t have any color.
@Composable
fun Surfaces() {
Column(modifier = Modifier.filllMaxSize()) {
Cards()
}
}
  • To have proper UIs, we always need to use a component that actually applies a color theme. Let’s add Surface on top of the composable function.
@Composable
fun Surfaces() {
Surface {
Column(modifier = Modifier.fillMaxSize()) {
Cards()
}
}
}

Voila, problem solved.

One of my UI elements must be different from the rest of the apps — none of my theme colors are suitable. However, I don’t want to face problems.

We can override the colors of the UI element. The class representation is different for all UI elements, but the implementation is the same. Let’s have a look on Button and TextField.

For the button, we will use ButtonDefaults.buttonColors() extension and override.

Similar for TextField, TextFieldDefaults.textFieldColors() will be overridden.

  • It’s important to keep in mind that, we should consider providing different color palettes for light/dark mode. Otherwise, we might end up with the problems that we mentioned before. (Background/text color same issue)

Let’s check our latest app that contains regular and custom components for light or dark theme.

We cover these topics:

  • How colors are applied in Material Theme, both Light and Dark
  • Default colors for UI elements
  • Common mistakes
  • The overriding color palette for a UI element

I hope the article was useful. In the next ones, I will investigate shapes and typography.

Leave a Comment