Custom Jetpack Compose Themes

The appearance of Android apps is intended to conform with a set of guidelines defined by Material Design. Material Design was developed by Google to provide a level of design consistency between different apps, while also allowing app developers to include their own branding in terms of color, typography, and shape choices (a concept referred to as Material theming). In addition to design guidelines, Material Design also includes a set of UI components for use when designing user interface layouts, many of which we have been using throughout this book.

In this chapter, we will provide an overview of how theming works within an Android Studio Compose project and explore how the default design configurations provided for newly created projects can be modified to meet your branding requirements.

Material Design 2 vs Material Design 3

Before beginning, it is important to note that Google is currently transitioning from Material Design 2 to Material Design 3 and that the current version of Android Studio defaults to Material Design 2. Material Design 3 provides the basis for Material You, a feature introduced in Android 12 that allows an app to automatically adjust theme elements to compliment preferences configured by the user on the device. Dynamic color support provided by Material Design 3, for example, allows the colors used in apps to automatically adapt to match the user’s wallpaper selection.

So that this book will be useful for as long as possible, this chapter will focus on color and typography theming using Material Design 3. At the time of writing, shape theming was not yet supported by Material Design 3, though the concepts covered in this chapter for color and typography will apply to shapes when support is available.

Material Design 2 Theming

Before exploring Material Design 3, we first need to look at how Material Design 2 is used in an Android Studio project created using the Empty Compose Activity template. The first point to note is that both calls to the top-level composable in the onCreate() method and the DefaultPreview function are embedded in a theme composable. The following, for example, is the code generated for a project named MyApp:

 

You are reading a sample chapter from Jetpack Compose Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
.
.
            MyAppTheme {
.
.
                    Greeting("Android")
                }
            }
        }
    }
}
 
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyAppTheme {
        Greeting("Android")
    }
}

All of the files associated with MyAppTheme are contained with the ui.theme sub-package of the project as shown in Figure 49-1:

Figure 49-1

The theme itself is declared in the Theme.kt file which begins by declaring different color palettes for use when the device is in light or dark mode. These palettes are created by calling the darkColors() and lightColors() builder function and specifying the colors for the different Material Theme color slots:

private val DarkColorPalette = darkColors(
    primary = Purple200,
    primaryVariant = Purple700,
    secondary = Teal200
)
 
private val LightColorPalette = lightColors(
    primary = Purple500,
    primaryVariant = Purple700,
    secondary = Teal200
}

This is just a subset of the slots available for color theming. For Material Design 3, for example, there is a total of 24 color slots available for use when designing a theme. In the absence of a slot assignment, the Material components use built-in default colors. More information about the color slots available in Material Design 2 can be found at the following URL:

https://material.io/design/color/the-color-system.html

 

You are reading a sample chapter from Jetpack Compose Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

These color slots are used by the Material components to set color attributes. For example, the primary color slot is used as the background color for the Material Button component. The actual colors assigned to the slots are declared in the Colors.kt file as follows:

val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)

Our example MyAppTheme composable is declared in the Theme.kt file as follows:

@Composable
fun MyApplicationTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }
 
    MaterialTheme(
        colors = colors,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}

Note that the theme makes use of the slot API (introduced in the chapter entitled An Overview of Jetpack Compose Slot APIs) to display the content. The declaration begins by checking whether the device is in light or dark mode by calling the isSystemInDarkTheme() function. The result of this call is then used to decide if the dark or light color palette is to be passed as a parameter to the MaterialTheme call. In addition to the color palette, MaterialTheme is also passed typography and shape settings which are declared in the Type.kt and Shape.kt files respectively.

In terms of typography, Material Design has a set of type scales, three of which are declared in the Type.kt file (albeit with two commented out):

val Typography = Typography(
    body1 = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    )
    /* Other default text styles to override
    button = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    caption = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 12.sp
    )
    */
)

As with the color slots, this is only a subset of the type scales supported by Material Design. The full list can be found online at:

 

You are reading a sample chapter from Jetpack Compose Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

https://material.io/design/typography/the-type-system.html

The Shape.kt file is used to define how the corners of Material components are to be rendered:

val Shapes = Shapes(
    small = RoundedCornerShape(4.dp),
    medium = RoundedCornerShape(4.dp),
    large = RoundedCornerShape(0.dp)
)

The default rounded corners of an OutlinedTextField will, for example, be controlled by the above shape values.

Creating a custom theme simply involves editing these files to use different colors, typography, and shape settings. These changes will then be used by the Material components that make up the user interface of the app.

Material Design 3 Theming

The key difference between Material Design 2 (MD2) and Material Design 3 (MD3) is support for dynamic colors and the use of color schemes instead of palettes. Typography is implemented in the same way as with MD2 and we do not yet know how shapes will be supported. Color schemes are created via calls to the lightColorScheme() and darkColorScheme() builder functions, for example:

 

You are reading a sample chapter from Jetpack Compose Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

private val DarkColorPalette = darkColorScheme(
    primary = ...,
    onPrimary = ...,
    secondary = ....,
.
.
)
 
private val LightColorPalette = lightColorScheme(
.
.
}

When the theme is created, the color schemes are now assigned using the colorSchemes parameter instead of the colors parameter used in MD2:

@Composable
fun MyAppTheme(
    useDarkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable() () -> Unit
) {
    val colors = if (!useDarkTheme) {
        LightThemeColors
    } else {
        DarkThemeColors
    }
 
    MaterialTheme(
        colorScheme = colors,
        typography = AppTypography,
        content = content
    )
}

Although the typography and declaration of theme colors are much the same between MD2 and MD3, the color slots and typography types have different names in many cases. A full listing of MD3 color slot names can be found at:

https://developer.android.com/reference/kotlin/androidx/compose/material3/ColorScheme

Similarly, a list of typography options is available at the following URL:

https://developer.android.com/reference/kotlin/androidx/compose/material3/Typography

 

You are reading a sample chapter from Jetpack Compose Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

To add support for dynamic colors, dynamic color schemes need to be generated via calls to the dynamicDarkColorScheme() and dynamicLightColorScheme() functions passing through the current local context as a parameter. These functions will then generate color schemes that match the user’s settings on the device (for example wallpaper selection). Since dynamic colors are only supported on Android 12 (S) or later, defensive code needs to be added when creating the MaterialTheme instance:

val useDynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
 
val colors = when {
    useDynamicColor && useDarkTheme -> 
                  dynamicDarkColorScheme(LocalContext.current)
    useDynamicColor && !useDarkTheme -> 
                  dynamicLightColorScheme(LocalContext.current)
    useDarkTheme -> DarkColorScheme
    else -> LightColorScheme
}

Note that dynamic colors only take effect when enabled on the device by the user within the wallpaper and styles section of the Android Settings app.

Building a Custom Theme

As we have seen so far, the coding work in implementing a theme is relatively simple. The difficult part, however, is often choosing a set of complementary colors to make up the theme. Fortunately, Google has developed a tool that makes it easy to design custom color themes for your apps. This tool is called the Material Theme Builder and is available at:

https://material-foundation.github.io/material-theme-builder

From within the builder tool, select the Custom tab (marked A in Figure 49-2) and make a color selection for the primary color key (B) by clicking on the color rectangle to display the color selection dialog. Once a color has been selected, the theme panel (C) will change to reflect the recommended colors for all of the MD3 color slots. The generated colors for the Secondary, Tertiary, and Neutral slots can be overridden by clicking on the boxes (D) and selecting different colors from the color selection panel:

 

You are reading a sample chapter from Jetpack Compose Essentials. Buy the full book now in Print or eBook format. Learn more.

Preview  Buy eBook  Buy Print

 

Figure 49-2

To incorporate the theme into your design, click on the Export button (F) and select the Jetpack Compose (Theme.kt) option. Once downloaded, place the Color.kt, Theme.kt, and Type.kt files into the ui.themes folder of your project, replacing the existing files if they are present. Note that the package names in each file and theme composable names in the Theme.kt file will need to be changed to match your project.

Summary

Material Design provides guidelines and components that define how Android apps look and appear. Individual branding can be applied to an app by designing themes that specify the colors, fonts, and shapes that should be used when the app is displayed. Google is currently introducing Material Design 3 which replaces Material Design 2 and supports the new features of Material Me including dynamic colors. For designing your own themes, Google also provides the Material Theme Builder which eases the task of choosing complementary theme colors. Once this tool has been used to design a theme, the corresponding files can be exported and used within an Android Studio project.