Many Android applications have a complex User Interface because designers invest a lot of effort into adding more features to the apps while still having a simple User Interface. As a result, applications contain custom UI components and color palettes. However, often we have a case where we have multiple places where we can find manually-configured colors and font sizes. When we want to apply a new font to the whole app, it can be time-consuming because we then have to update a lot of places. 

So, let’s take a look at Jetpack Compose and try to solve both of these problems with this framework. I propose starting with color palettes and afterwards moving to fonts. Samples in this article will be based on an app from the “CoffeeDrinksWithJetpackCompose” repository. CoffeeDrinks is an Android application which shows a list of coffee drinks with a little bit of information about each of them.

Coffee Drinks application

Theme

Right now, you can easily find a situation when all the colors in an application are extracted to a colors.xml file:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#855446</color>
    <color name="colorAccentPink">#F5B8C9</color>
    <color name="colorAccent">#993215</color>

    <color name="colorGray">#858585</color>
    <color name="colorBottomNavigationInactive">#3d1e04</color>
    ...
</resources>

These colors usually used in the layouts of the application:

<androidx.constraintlayout.widget.ConstraintLayout
    ...

    <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:background="?attr/colorPrimary"
            ... />
    ...
</androidx.constraintlayout.widget.ConstraintLayout>

The main problem with this approach is that the color palette is not connected to the theme of the application, and when you want to support both Dark and Light modes, you might have a problem.

The material design proposes a list of predefined colors to use them across the application. We can split them into two groups:

  • Background colors
    • Primary color displayed most frequently across your app’s screens and components.
    • Primary Variant color is used to distinguish elements using primary colors, such as top app bar and the system bar.
    • Secondary color provides more ways to accent and distinguish your product. Having a secondary color is optional, and should be applied sparingly to accent select parts of your UI.
    • Secondary Variant color is used to distinguish elements using secondary colours.
    • Background color appears behind scrollable content.
    • Surface color uses on surfaces of components, like cards and menus.
    • Error color used for indicating an error.
  • Typography and icon colors
    • On Primary color of text and icons displayed on top of the primary color.
    • On Secondary color of text and icons displayed on top of the secondary color.
    • On Background color of text and icons displayed on top of the background color.
    • On Surface color of text and icons displayed on top of the surface color.
    • On Error color of text and icons displayed on top of the error color.

If we apply these colors to our application, we can easily support both light and dark themes. However, this is only one of the options which allows us to simplify working with color palettes. 

So, let’s move on to Jetpack Compose and try to improve the sample application, which uses colors inside @Composable functions. Jetpack Compose has a “MaterialTheme” function, and we can wrap another @Composable function, like Crossfade which can be used as navigation between different screens in the app. You can read more about the definitions of the colors in the “The color system” section of Material Design website.

MaterialTheme(
    colors = colorPalette, 
    typography = appTypography
) {
    Crossfade(current = AppState.currentScreen) { screen ->
        when (screen) {
            is Screen.CoffeeDrinks -> CoffeeDrinksScreen(
                    repository, 
                    coffeeDrinkItemMapper
                )
            is Screen.CoffeeDrinkDetails -> CoffeeDrinkDetailsScreen(
                    repository, 
                    coffeeDrinkDetailMapper, 
                    screen.coffeeDrinkId
                )
            ...
        }
    }
}

The “MaterialTheme” function has parameters and allows us to customize the colors and typography of the application.

The colors parameter allows us to provide a color palette for the application.

The typographyparameter allows us to configure the typography system. You can read more about typography in the next section.

So, let’s start with the object of ColorPalettetype and create light and dark palettes for the application. The ColorPalette interface has properties according to Material Design.

interface ColorPalette {
    val primary: Color
    val primaryVariant: Color
    val secondary: Color
    val secondaryVariant: Color
    val background: Color
    val surface: Color
    val error: Color
    val onPrimary: Color
    val onSecondary: Color
    val onBackground: Color
    val onSurface: Color
    val onError: Color
    val isLight: Boolean
}

In addition to the ColorPalette interface we have two functions which help us to create color pallets:

  • darkColorPalette
  • lightColorPalette

The ColorPalette interface has an isLight property which is used for some @Composable functions (AppBar and Snackbar, etc.) and can be used for custom components for colors. In addition to this, the “secondary” and the “secondaryVariant” colors are the same for the dark color palette.

So, let’s create both light and dark color palettes for the application.

Light and Dark color palettes
Light and Dark color palettes

The dark color palette:

Dark color palette

We can easily convert them to light and dark palettes for the Jetpack Compose framework:

val lightThemeColors  = lightColorPalette(
    primary = Color(0xFF855446),
    primaryVariant = Color(0xFF9C684B),
    secondary = Color(0xFF03DAC5),
    secondaryVariant = Color(0xFF0AC9F0),
    background = Color.White,
    surface = Color.White,
    error = Color(0xFFB00020),
    onPrimary = Color.White,
    onSecondary = Color.White,
    onBackground = Color.Black,
    onSurface = Color.Black,
    onError = Color.White
)

val darkThemeColors = darkColorPalette(
    primary = Color(0xFF1F1F1F),
    primaryVariant = Color(0xFF3E2723),
    secondary = Color(0xFF03DAC5),
    background = Color(0xFF121212),
    surface = Color.Black,
    error = Color(0xFFCF6679),
    onPrimary = Color.White,
    onSecondary = Color.White,
    onBackground = Color.White,
    onSurface = Color.White,
    onError = Color.Black
)

The final step is to add color palettes to the MaterialTheme function and apply it to the colors of our screen. However, the MaterialTheme function has only the colors parameter, meaning we can pass light or dark color palettes. Fortunately, we can easily check the system themes as needed for our application.

MaterialTheme(colors = lightThemeColors) {
    Crossfade(current = AppState.currentScreen) { screen ->
        ...
    }
}

We can use any color from the active “Color Palette” inside @Composable function.

FloatingActionButton(
    backgroundColor = MaterialTheme.colors.secondary,
    ...
)

The ColorPalette interface allows us to configure multiple palettes, and we can use them depending on the system theme with the isSystemInDarkTheme function. By default, some of the existing composable functions check if light or dark themes are active and use different properties for them. However, we can check it manually with the following code:

val colorPalette = if (isSystemInDarkTheme()) {
    darkThemeColors
} else {
    lightThemeColors
}

MaterialTheme(colors = colorPalette) {
    Crossfade(current = AppState.currentScreen) { screen ->
        ...
    }
}

Typography

The last step for the proper configuration of the theme is to set up the fonts of the application. Jetpack Compose has an API for working with fonts. Android devices from different vendors have different fonts in the system by default. In this example, I want to add Roboto fonts and apply them to the application. You can download the Roboto font here.

The first step is adding fonts to the project. All fonts should be added to “module/src/main/res/font/”.

Note: The “-” is not a valid character for the resource folder. When you download a new font, I recommend you rename them to avoid issues with fonts.

The next step is to configure the fonts for the Jetpack Compose framework. We should start by creating a FontListFontFamily object with all fonts, as well as their weights and styles. Let’s create a font family which will be used for the whole application.

private val appFontFamily = fontFamily(
    fonts = listOf(
        ResourceFont(
            resId = R.font.roboto_black, 
            weight = FontWeight.W900, 
            style = FontStyle.Normal
        ),
        ResourceFont(
            resId = R.font.roboto_black_italic, 
            weight = FontWeight.W900, 
            style = FontStyle.Italic
        ),
        ResourceFont(
            resId = R.font.roboto_bold, 
            weight = FontWeight.W700, 
            style = FontStyle.Normal
        ),
        ...
    )
)

private val defaultTypography = Typography()
val appTypography = Typography(
    h1 = defaultTypography.h1.copy(fontFamily = appFontFamily),
    h2 = defaultTypography.h2.copy(fontFamily = appFontFamily),
    h3 = defaultTypography.h3.copy(fontFamily = appFontFamily),
    h4 = defaultTypography.h4.copy(fontFamily = appFontFamily),
    h5 = defaultTypography.h5.copy(fontFamily = appFontFamily),
    h6 = defaultTypography.h6.copy(fontFamily = appFontFamily),
    subtitle1 = defaultTypography.subtitle1.copy(fontFamily = appFontFamily),
    subtitle2 = defaultTypography.subtitle2.copy(fontFamily = appFontFamily),
    body1 = defaultTypography.body1.copy(fontFamily = appFontFamily),
    body2 = defaultTypography.body2.copy(fontFamily = appFontFamily),
    button = defaultTypography.button.copy(fontFamily = appFontFamily),
    caption = defaultTypography.caption.copy(fontFamily = appFontFamily),
    overline = defaultTypography.overline.copy(fontFamily = appFontFamily )
)

The combination of weight and style allows us to cover all cases for the application. You can find information about the weight of each Roboto font here.

In addition to this, we should configure a Typography object with the following parameters:

  • h1 is the largest headline, reserved for short and important text.
  • h2 is the second-largest headline, reserved for short and important text.
  • h3 is the third-largest headline, reserved for short and important text.
  • h4 is the fourth-largest headline, reserved for short and important text.
  • h5 is the fifth-largest headline, reserved for short and important text.
  • h6 is the sixth-largest headline, reserved for short and important text.
  • subtitle1 is the largest subtitle and is typically reserved for medium-emphasis text that is shorter in length.
  • subtitle2 is the smallest subtitle and is typically reserved for medium-emphasis text that is shorter in length.
  • body1 is the largest body and is typically reserved for a long-form text that is shorter in length.
  • body2 is the smallest body and is typically reserved for a long-form text that is shorter in length.
  • button is reserved for a button text.
  • caption is one of the smallest font sizes it reserved for annotating imagery or introduce a headline.
  • overline is one of the smallest font sizes.

I recommend checking the “Typography: Type scale” section of the material design website.

The last step is adding the Typography object to MaterialTheme function as a parameter:

MaterialTheme(typography = appTypography) {
    ...
}

Note: I recommend configuring all @Preview functions in the same way to have a correct preview. 

So, let’s rework the ingredients text for coffee list item. 

@Composable
private fun CoffeeDrinkIngredient(ingredients: String) {
    Text(
        text = ingredients,
        modifier = Modifier.padding(end = 8.dp) + 
                Modifier.drawOpacity(0.54f),
        maxLines = 1,
        overflow = TextOverflow.Ellipsis,
        style = MaterialTheme.typography.body1,
        color = MaterialTheme.colors.onSurface
    )
}

As you can see we can easily specify Text functions with the font using MaterialTheme.typography.body1. However, sometimes we need to customize font style and also make changes to the color. We can use the “copy” function to create a new font size object based on an existing one.

Text(
    style = MaterialTheme.typography.h4.copy(
            MaterialTheme.colors.onSurface
    ),
    ...
)

As you can see, we can change the configuration of the typography application and the style of the application will be changed, too.

Summary

Jetpack Compose has a powerful mechanism for working with color palettes and fonts. We can easily configure them with Color Palette and Typography objects which will be used for the MaterialTheme function to customize colors and font styles for child @Composable functions. 

The “ColorPalette” interface allows us to configure multiple palettes, and we can use them depending on the system theme with the isSystemInDarkTheme function. By default, some of the existing composable functions check if light or dark themes are active and use different properties for them.

The Typography object allows us to configure fonts and font styles to set up fonts in the whole application from a single source of truth. 
Both the ColorPalette interface and the Typography data class are based on Material Design guidelines and use terms described in the specification, which can simplify the designing and development processes.

0 CommentsClose Comments

Leave a comment

Newsletter Subscribe

Get the Latest Posts & Articles in Your Email

We Promise Not to Send Spam:)