Material Theming with Jetpack Compose

1. Before you begin

Material Design is a design system built and supported by Google designers and developers to build high-quality digital experiences for Android, as well as other mobile and web platforms. It provides guidelines on how to build your app UI in a readable, attractive, and consistent manner.

In this codelab, you learn about Material Theming, which allows you to use Material Design in your app, with guidance on customizing colors, typography, and shapes. You can customize as little, or as much, as you like for your app. You also learn how to add a top app bar to display the app's name and icon.

Prerequisites

  • Familiar with the Kotlin language, including syntax, functions, and variables.
  • Able to build layouts in Compose, including rows and columns with padding.
  • Able to create simple lists in Compose.

What you'll learn

  • How to apply Material Theming to a Compose app.
  • How to add a custom color palette to your app.
  • How to add custom fonts to your app.
  • How to add custom shapes to elements in your app.
  • How to add a top app bar to your app.

What you'll build

  • You will build a beautiful app that incorporates Material Design best practices.

What you'll need

  • The latest version of Android Studio.
  • An internet connection to download the starter code and fonts.

2. App overview

In this codelab, you create Woof, an app that displays a list of dogs and uses Material Design to create a beautiful app experience.

Woof final app

Through this codelab, we will show you some of what is possible using Material Theming. Use this codelab for ideas of how to use Material Theming to improve the look and feel of the apps you create in the future.

Color palette

Below are the color palettes for both light and dark themes that we will create.

This image has the light color scheme for the Woof app.

This image has the dark color scheme for the Woof app.

Here is the final app in both light theme and dark theme.

Light theme

Dark theme

Woof final app

Woof final app - Dark Theme

Typography

Below are the type styles you will use in the app.

8ea685b3871d5ffc.png

Theme file

The Theme.kt file is the file that holds all the information about the theme of the app, which is defined through color, typography, and shape. This is an important file for you to know. Inside of the file is the composable WoofTheme(), which sets the colors, typography, and shapes of the app.

@Composable
fun WoofTheme(
   darkTheme: Boolean = isSystemInDarkTheme(),
   dynamicColor: Boolean = false,
   content: @Composable () -> Unit
) {
   val colorScheme = when {
       dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
           val context = LocalContext.current
           if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
       }

       darkTheme -> DarkColors
       else -> LightColors
   }
   val view = LocalView.current
   if (!view.isInEditMode) {
       SideEffect {
           val window = (view.context as Activity).window
           window.statusBarColor = colorScheme.primary.toArgb()
           WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
       }
   }

   MaterialTheme(
       colorScheme = colorScheme,
       shapes = Shapes,
       typography = Typography,
       content = content
   )
}

In MainActivity.kt, the WoofTheme() is added to provide the Material Theming for the entire app.

class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           WoofTheme {
               Surface(
                   modifier = Modifier.fillMaxSize()
               ) {
                   WoofApp()
               }
           }
       }
   }
}

Take a look at the WoofPreview(). The WoofTheme() is added to provide the Material Theming you see in the WoofPreview().

@Preview
@Composable
fun WoofPreview() {
    WoofTheme(darkTheme = false) {
        WoofApp()
    }
}

3. Get the starter code

  1. Navigate to the provided GitHub repository page for the project.
  2. Click the Code button. This opens a dialog.

1debcf330fd04c7b.png

  1. In the dialog, click the HTTPS tab and then click d80cda5fcac5cc83.png next to the URL to copy it. You use this URL in Android Studio.

52117cef9280e2c3.png

  1. If Android Studio isn't open, open it and then click Get from VCS in the Welcome to Android Studio window.

38e3cb13f6cbf3b8.png

  1. If Android Studio is already open, select File > New > Project from Version Control...

eb859f5f2f920118.png

  1. In the Get from Version Control window's Version control menu, select Git.

c1ff21b37a87da88.png

  1. In the URL text box, enter the link that you copied from the GitHub repository page.
  2. In the Directory menu, select the directory where you want to clone the source code from the GitHub repository.

d872acd8fb88d532.png

  1. Click Clone.
  2. Wait for Android Studio to download and open the project.
  3. In the navigation menu of Android Studio, click 8de56cba7583251f.png ‘Run app' and then ensure that the app builds as expected.
  4. In the navigation menu, click Git > Branches...

3b3d9c44dcc1327a.png

  1. In the Git Branches dialog under the Remote Branches section, select the branch name provided earlier and then click Checkout.

4d189e9e70df0234.png

  1. If you need to switch branches, click Git > Branches, and then select the branch to which you want to switch and click Smart Checkout.

596d8337aba9e937.png

Explore the starter code

  1. Open the starter code in Android Studio.
  2. Open com.example.woof > data > Dog.kt. This contains the Dog data class that will be used to represent the dog's photo, name, age, and hobbies. It also contains a list of dogs and the information that you will use as the data in your app.
  3. Open res > drawable. This contains all the image assets that you need for this project, including the app icon, dog images, and icons.
  4. Open res > values > strings.xml. This contains the strings you use in this app, including the app name, dog names, their descriptions, and more.
  5. Open MainActivity.kt. This contains the code to create a simple list that displays a photo of a dog, the dog's name, and the dog's age.
  6. WoofApp() contains a LazyColumn that displays the DogItems.
  7. DogItem() contains a Row that displays a photo of the dog and information about it.
  8. DogIcon() displays a photo of the dog.
  9. DogInformation() contains the dog's name and age.
  10. WoofPreview() allows you to see a preview of the app in the Design pane.

Ensure your emulator/device is in light theme

In this codelab, you will be working with both light and dark themes, however, most of the codelab is in light theme. Before you get started, ensure that your device/emulator is in light theme.

In order to view your app in light theme, on your emulator or physical device:

  1. Go to the Settings app on the device.
  2. Search for Dark Theme and click into it.
  3. If Dark Theme is on, switch it off.

Run the starter code to see what you're starting with; it's a list that displays dogs with their photos, names, and ages. It is functional, but it doesn't look great, so we are going to fix that.

9d9b9f2ced33646b.png

4. Add color

The first thing you are going to modify in the Woof app is the color scheme.

A color scheme is the combination of colors that your app uses. Different color combinations evoke different moods, which influences how people feel when they use your app.

Color, in the Android system, is represented by a hexadecimal (hex) color value. A hex color code starts with a pound (#) character, and is followed by six letters and/or numbers that represent the red, green, and blue (RGB) components of that color. The first two letters/numbers refer to red, the next two refer to green, and the last two refer to blue.

This shows the hexadecimal numbers that is used to create colors.

A color can also include an alpha value—letters and/or numbers—which represents the transparency of the color (#00 is 0% opacity (fully transparent), #FF is 100% opacity (fully opaque)). When included, the alpha value is the first two characters of the hex color code after the pound (#) character. If an alpha value is not included, it is assumed to be #FF, which is 100% opacity (fully opaque).

Below are some example colors and their hex values.

2753d8cdd396c449.png

Use Material Theme Builder to create a color scheme

To create a custom color scheme for our app, we will use the Material Theme Builder.

  1. Click this link to go to the Material Theme Builder.
  2. On the left pane you will see the Core Colors, click on Primary:

This shows four core colors in the Material Theme Builder

  1. The HCT color picker will open.

This is the HCT Color Picker to choose a custom color in the Material Theme Builder.

  1. To create the color scheme shown in the app screenshots, you will change the primary color in this color picker. In the text box replace the current text with #006C4C. This will make the primary color of the app green.

This shows the HCT Color picker set to green

Notice how this updates the apps on the screen to adopt a green color scheme.

This shows the Material Theme Builder's apps reacting to the change in color from the HCT color picker.

  1. Scroll down the page and you will see the full color scheme for light and dark theme generated off of the color you inputted.

Material Theme Builder Light Scheme

Dark Scheme generated by Material Theme Builder

You may wonder what all these roles are and how they are utilized, here are a few of the main ones:

  • The primary colors are used for key components across the UI.
  • The secondary colors are used for less prominent components in the UI.
  • The tertiary colors are used for contrasting accents that can be used to balance primary and secondary colors or bring heightened attention to an element, such as an input field.
  • The on color elements appear on top of other colors in the palette, and are primarily applied to text, iconography, and strokes. In our color palette, we have an onSurface color, which appears on top of the surface color, and an onPrimary color, which appears on top of the primary color.

Having these slots leads to a cohesive design system, where related components are colored similarly.

Enough theory about colors—time to add this beautiful color palette to the app!

Add color palette to theme

On the Material Theme Builder page, there is the option to click the Export button to download a Color.kt file and Theme.kt file with the custom theme you created in the Theme Builder.

This will work to add the custom theme we create to your app. However, because the generated Theme.kt file does not include the code for dynamic color which we will cover later in the codelab, copy the files in.

  1. Open the Color.kt file and replace the contents with the code below to copy in the new color scheme.
package com.example.woof.ui.theme

import androidx.compose.ui.graphics.Color

val md_theme_light_primary = Color(0xFF006C4C)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFF89F8C7)
val md_theme_light_onPrimaryContainer = Color(0xFF002114)
val md_theme_light_secondary = Color(0xFF4D6357)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFCFE9D9)
val md_theme_light_onSecondaryContainer = Color(0xFF092016)
val md_theme_light_tertiary = Color(0xFF3D6373)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFC1E8FB)
val md_theme_light_onTertiaryContainer = Color(0xFF001F29)
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFBFDF9)
val md_theme_light_onBackground = Color(0xFF191C1A)
val md_theme_light_surface = Color(0xFFFBFDF9)
val md_theme_light_onSurface = Color(0xFF191C1A)
val md_theme_light_surfaceVariant = Color(0xFFDBE5DD)
val md_theme_light_onSurfaceVariant = Color(0xFF404943)
val md_theme_light_outline = Color(0xFF707973)
val md_theme_light_inverseOnSurface = Color(0xFFEFF1ED)
val md_theme_light_inverseSurface = Color(0xFF2E312F)
val md_theme_light_inversePrimary = Color(0xFF6CDBAC)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFF006C4C)
val md_theme_light_outlineVariant = Color(0xFFBFC9C2)
val md_theme_light_scrim = Color(0xFF000000)

val md_theme_dark_primary = Color(0xFF6CDBAC)
val md_theme_dark_onPrimary = Color(0xFF003826)
val md_theme_dark_primaryContainer = Color(0xFF005138)
val md_theme_dark_onPrimaryContainer = Color(0xFF89F8C7)
val md_theme_dark_secondary = Color(0xFFB3CCBE)
val md_theme_dark_onSecondary = Color(0xFF1F352A)
val md_theme_dark_secondaryContainer = Color(0xFF354B40)
val md_theme_dark_onSecondaryContainer = Color(0xFFCFE9D9)
val md_theme_dark_tertiary = Color(0xFFA5CCDF)
val md_theme_dark_onTertiary = Color(0xFF073543)
val md_theme_dark_tertiaryContainer = Color(0xFF244C5B)
val md_theme_dark_onTertiaryContainer = Color(0xFFC1E8FB)
val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onError = Color(0xFF690005)
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
val md_theme_dark_background = Color(0xFF191C1A)
val md_theme_dark_onBackground = Color(0xFFE1E3DF)
val md_theme_dark_surface = Color(0xFF191C1A)
val md_theme_dark_onSurface = Color(0xFFE1E3DF)
val md_theme_dark_surfaceVariant = Color(0xFF404943)
val md_theme_dark_onSurfaceVariant = Color(0xFFBFC9C2)
val md_theme_dark_outline = Color(0xFF8A938C)
val md_theme_dark_inverseOnSurface = Color(0xFF191C1A)
val md_theme_dark_inverseSurface = Color(0xFFE1E3DF)
val md_theme_dark_inversePrimary = Color(0xFF006C4C)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFF6CDBAC)
val md_theme_dark_outlineVariant = Color(0xFF404943)
val md_theme_dark_scrim = Color(0xFF000000)
  1. Open the Theme.kt file and replace the contents with the code below to add the new colors to the theme.
package com.example.woof.ui.theme

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat

private val LightColors = lightColorScheme(
   primary = md_theme_light_primary,
   onPrimary = md_theme_light_onPrimary,
   primaryContainer = md_theme_light_primaryContainer,
   onPrimaryContainer = md_theme_light_onPrimaryContainer,
   secondary = md_theme_light_secondary,
   onSecondary = md_theme_light_onSecondary,
   secondaryContainer = md_theme_light_secondaryContainer,
   onSecondaryContainer = md_theme_light_onSecondaryContainer,
   tertiary = md_theme_light_tertiary,
   onTertiary = md_theme_light_onTertiary,
   tertiaryContainer = md_theme_light_tertiaryContainer,
   onTertiaryContainer = md_theme_light_onTertiaryContainer,
   error = md_theme_light_error,
   errorContainer = md_theme_light_errorContainer,
   onError = md_theme_light_onError,
   onErrorContainer = md_theme_light_onErrorContainer,
   background = md_theme_light_background,
   onBackground = md_theme_light_onBackground,
   surface = md_theme_light_surface,
   onSurface = md_theme_light_onSurface,
   surfaceVariant = md_theme_light_surfaceVariant,
   onSurfaceVariant = md_theme_light_onSurfaceVariant,
   outline = md_theme_light_outline,
   inverseOnSurface = md_theme_light_inverseOnSurface,
   inverseSurface = md_theme_light_inverseSurface,
   inversePrimary = md_theme_light_inversePrimary,
   surfaceTint = md_theme_light_surfaceTint,
   outlineVariant = md_theme_light_outlineVariant,
   scrim = md_theme_light_scrim,
)


private val DarkColors = darkColorScheme(
   primary = md_theme_dark_primary,
   onPrimary = md_theme_dark_onPrimary,
   primaryContainer = md_theme_dark_primaryContainer,
   onPrimaryContainer = md_theme_dark_onPrimaryContainer,
   secondary = md_theme_dark_secondary,
   onSecondary = md_theme_dark_onSecondary,
   secondaryContainer = md_theme_dark_secondaryContainer,
   onSecondaryContainer = md_theme_dark_onSecondaryContainer,
   tertiary = md_theme_dark_tertiary,
   onTertiary = md_theme_dark_onTertiary,
   tertiaryContainer = md_theme_dark_tertiaryContainer,
   onTertiaryContainer = md_theme_dark_onTertiaryContainer,
   error = md_theme_dark_error,
   errorContainer = md_theme_dark_errorContainer,
   onError = md_theme_dark_onError,
   onErrorContainer = md_theme_dark_onErrorContainer,
   background = md_theme_dark_background,
   onBackground = md_theme_dark_onBackground,
   surface = md_theme_dark_surface,
   onSurface = md_theme_dark_onSurface,
   surfaceVariant = md_theme_dark_surfaceVariant,
   onSurfaceVariant = md_theme_dark_onSurfaceVariant,
   outline = md_theme_dark_outline,
   inverseOnSurface = md_theme_dark_inverseOnSurface,
   inverseSurface = md_theme_dark_inverseSurface,
   inversePrimary = md_theme_dark_inversePrimary,
   surfaceTint = md_theme_dark_surfaceTint,
   outlineVariant = md_theme_dark_outlineVariant,
   scrim = md_theme_dark_scrim,
)

@Composable
fun WoofTheme(
   darkTheme: Boolean = isSystemInDarkTheme(),
   dynamicColor: Boolean = false,
   content: @Composable () -> Unit
) {
   val colorScheme = when {
       dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
           val context = LocalContext.current
           if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
       }

       darkTheme -> DarkColors
       else -> LightColors
   }
   val view = LocalView.current
   if (!view.isInEditMode) {
       SideEffect {
           val window = (view.context as Activity).window
           window.statusBarColor = colorScheme.primary.toArgb()
           WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
       }
   }

   MaterialTheme(
       colorScheme = colorScheme,
       shapes = Shapes,
       typography = Typography,
       content = content
   )
}

In WoofTheme() the colorScheme val uses a when statement

  • If dynamicColor is true and the build version is S or higher, it checks if the device is in darkTheme or not.
  • If it is in dark theme, colorScheme will be set to dynamicDarkColorScheme.
  • If it is not in dark theme, it will be set to dynamicLightColorScheme.
  • If the app is not using dynamicColor, it checks if your app is in darkTheme. If so then colorScheme will be set to DarkColors.
  • If neither of those are true then colorScheme will be set to LightColors.

The copied in Theme.kt file has dynamicColor set to false and the devices we are working with are in light mode so the colorScheme will be set to LightColors.

val colorScheme = when {
       dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
           val context = LocalContext.current
           if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
       }

       darkTheme -> DarkColors
       else -> LightColors
   }
  1. Re-run your app, notice that the app bar has automatically changed color.

ec3217eebf9b1c12.png

Color mapping

Material components are automatically mapped to color slots. This is how the status bar changed to the Primary color without any code changing. Other key components across the UI like Floating Action Buttons also default to the Primary color. This means that you don't need to explicitly assign a color to a component; it is automatically mapped to a color slot when you set the color theme in your app. You can override this by explicitly setting a color in the code. Read more about color roles here.

In this section we will wrap the Row that contains the DogIcon() and DogInformation() with a Card to differentiate the list item colors with the background.

  1. In DogItem(), wrap the Row() with a Card().
Card() {
   Row(
       modifier = modifier
           .fillMaxWidth()
           .padding(dimensionResource(id = R.dimen.padding_small))
   ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
   }
}
  1. Since Card is now the first child composable in DogItem(), pass in the modifier from DogItem() to the Card, and update the Row's modifier to a new instance of Modifier.
Card(modifier = modifier) {
   Row(
       modifier = Modifier
           .fillMaxWidth()
           .padding(dimensionResource(id = R.dimen.padding_small))
   ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
   }
}
  1. Take a look at WoofPreview(). The list items have now automatically changed color because of the Card Composables. The colors look great, but there is no spacing between the list items.

Preview with a light green background to differentiate list items from the background

Dimens file

Just like you use the strings.xml to store the strings in your app, it is also a good practice to use a file called dimens.xml to store dimension values. This is helpful so you don't hard code values and so if you need to, you can change them in a single place.

Go to app > res > values > dimens.xml and take a look at the file. It stores dimension values for padding_small, padding_medium, and image_size. These dimensions will be used throughout the app.

<resources>
   <dimen name="padding_small">8dp</dimen>
   <dimen name="padding_medium">16dp</dimen>
   <dimen name="image_size">64dp</dimen>
</resources>

To add a value from the dimens.xml file, this the correct format:

Shows how to properly format adding values from the dimension resource

For example, to add padding_small, you would pass in dimensionResource(id = R.dimen.padding_small).

  1. In WoofApp(), add a modifier with padding_small in the call to DogItem().
@Composable
fun WoofApp() {
   LazyColumn() {
       items(dogs) {
           DogItem(
               dog = it,
               modifier = Modifier.padding(dimensionResource(id = R.dimen.padding_small))
           )
       }
   }
}

In WoofPreview(), there is now more definition between list items.

Woof Preview with padding in between list items

Dark theme

In the Android system, there is the option to switch your device to a dark theme. A dark theme uses darker, more subdued colors, and:

  • Can reduce power usage by a significant amount (depending on the device's screen technology).
  • Improves visibility for users with low vision and those who are sensitive to bright light.
  • Makes it easier for anyone to use a device in a low-light environment.

Your app can opt-in to Force Dark, which means the system will implement a dark theme for you. However, it is a better experience for your users if you implement the dark theme, so that you maintain full control over the app theme.

When choosing your own dark theme, it is important to note that colors for a dark theme need to meet accessibility contrast standards. Dark themes use a dark surface color, with limited color accents.

View dark theme in preview

You already added the colors for dark theme in the previous step. To see the dark theme in action, you will add another Preview Composable to MainActivity.kt. That way, when you change the UI layout in your code, you can see how the light theme and dark theme previews look simultaneously.

  1. Under WoofPreview(), create a new function called WoofDarkThemePreview() and annotate it with @Preview and @Composable.
@Preview
@Composable
fun WoofDarkThemePreview() {

}
  1. Inside DarkThemePreview(), add the WoofTheme(). Without adding WoofTheme(), you wouldn't see any of the styling we added in the app. Set the darkTheme parameter to true.
@Preview
@Composable
fun WoofDarkThemePreview() {
   WoofTheme(darkTheme = true) {

   }
}
  1. Call WoofApp() inside of WoofTheme().
@Preview
@Composable
fun WoofDarkThemePreview() {
   WoofTheme(darkTheme = true) {
       WoofApp()
   }
}

Now scroll down in the Design pane to see the app in dark theme, including the darker app/list item background and the lighter text. Compare the differences between the dark and light themes.

Dark theme

Light theme

Woof Preview in Light Theme

View dark theme on your device or emulator

In order to view your app in dark theme on the emulator or physical device:

  1. Go to the Settings app on the device.
  2. Search for Dark theme and click into it.
  3. Toggle on Dark theme.
  4. Reopen the Woof app and it will be in dark theme.

Woof app in dark theme.

This codelab focuses more on the light theme, so before you move forward with the app, turn off the dark theme.

  1. Go to the Settings app on the device.
  2. Select Display.
  3. Toggle off Dark theme.

Compare how the app looked at the beginning of the section versus now. The list items and text are more defined, and the color scheme is more visually appealing.

Without color

With color (light theme)

With color (dark theme)

Woof base app

Woof app in Light Theme

Woof App in Dark Theme

Dynamic Color

Material 3 strongly focuses on user personalization - a new feature in Material 3 is Dynamic Color which creates a theme for your app based on the user's wallpaper. This way if the user loves green and has a blue phone background, their Woof app will also be blue to reflect that. Dynamic theming is only on certain devices that are running Android 12 and up.

A custom theme can be used for apps that have strong branding colors and also needs to be implemented for devices that don't support dynamic theming so that your app is still themed.

  1. To enable dynamic color, open Theme.kt and go to the WoofTheme() composable and set the dynamicColor parameter to true.
@Composable
fun WoofTheme(
   darkTheme: Boolean = isSystemInDarkTheme(),
   dynamicColor: Boolean = true,
   content: @Composable () -> Unit
)
  1. To change the background of a device or an emulator, go to Settings, then search for Wallpaper.
  2. Change the wallpaper to a color or set of colors.
  3. Rerun your app to see the dynamic theme (note that your device or emulator must be Android 12+ to see dynamic color), feel free to play around with this with different wallpapers!

a66547acbca96d61.png

  1. This codelab does focus on custom theming, so disable dynamicColor before you move on.
@Composable
fun WoofTheme(
   darkTheme: Boolean = isSystemInDarkTheme(),
   dynamicColor: Boolean = false,
   content: @Composable () -> Unit
)

5. Add shape

Applying a shape can change so much about the look and feel of a composable. Shapes direct attention, identify components, communicate state, and express brand.

Many shapes are defined using RoundedCornerShape, which describes a rectangle with rounded corners. The number passed in defines how round the corners are. If RoundedCornerShape(0.dp) is used, the rectangle has no rounded corners; if RoundedCornerShape(50.dp) is used, the corners will be fully circular.

0.dp

25.dp

50.dp

Woof list item with shaping

Woof list item with shaping

Woof list item with shaping

You can also customize shapes further by adding different rounding percentages on each corner. It's pretty fun to play around with shapes!

Top left: 50.dp
Bottom left: 25.dp
Top right: 0.dp
Bottom right: 15.dp

Top left: 15.dp
Bottom left: 50.dp
Top right: 50.dp
Bottom right: 15.dp

Top left: 0.dp
Bottom left: 50.dp
Top right: 0.dp
Bottom right: 50.dp

Woof list item with shaping

Woof list item with shaping

Woof list item with shaping

The Shape.kt file is used to define shapes of components in Compose. There are three types of components: small, medium, and large. In this section, you will modify the Card component, which is defined as medium size. Components are grouped into shape categories based on their size.

In this section, you will shape the image of the dog into a circle, and modify the shape of the list item.

Shape the dog image into a circle

  1. Open the Shape.kt file and notice that the small parameter is set to RoundedCornerShape(50.dp). This will be used to shape the image into a circle.
val Shapes = Shapes(
   small = RoundedCornerShape(50.dp),
)
  1. Open MainActivity.kt. In DogIcon(), add a clip attribute to the modifier of the Image; this will clip the image into a shape. Pass in the MaterialTheme.shapes.small.
import androidx.compose.ui.draw.clip
import androidx.compose.foundation.shape.RoundedCornerShape

@Composable
fun DogIcon(
   @DrawableRes dogIcon: Int,
   modifier: Modifier = Modifier
) {
   Image(
       modifier = modifier
           .size(dimensionResource(id = R.dimen.image_size))
           .padding(dimensionResource(id = R.dimen.padding_small))
           .clip(MaterialTheme.shapes.small),

When you look at the WoofPreview(), you'll notice that the dog icons are circular! However, some photos are cut off on the sides and not showing up as fully circular.

Woof Preview with circular images

  1. To make all the photos circular, add in a ContentScale and a Crop attribute; this crops the image to fit. Note that contentScale is an attribute of Image, and not part of the modifier.
import androidx.compose.ui.layout.ContentScale

@Composable
fun DogIcon(
   @DrawableRes dogIcon: Int,
   modifier: Modifier = Modifier
) {
   Image(
       modifier = modifier
           .size(dimensionResource(id = R.dimen.image_size))
           .padding(dimensionResource(id = R.dimen.padding_small))
           .clip(MaterialTheme.shapes.small),
       contentScale = ContentScale.Crop,

This is the full DogIcon() Composable.

@Composable
fun DogIcon(
   @DrawableRes dogIcon: Int,
   modifier: Modifier = Modifier
) {
   Image(
       modifier = modifier
           .size(dimensionResource(R.dimen.image_size))
           .padding(dimensionResource(R.dimen.padding_small))
           .clip(MaterialTheme.shapes.small),
       contentScale = ContentScale.Crop,
       painter = painterResource(dogIcon),
       contentDescription = null
   )
}

Now in WoofPreview(), the icons are circular.

Woof Preview with circular images

Add a shape to the list item

In this section, you will add a shape to the list item. The list item is already being displayed through a Card. A Card is a surface that can contain a single composable and contains options for decoration. The decoration can be added through the border, shape, and more. In this section, you will use the Card to add a shape to the list item.

The Woof list item with shape dimensions added to it

  1. Open the Shape.kt file. A Card is a medium component, so you add the medium parameter of the Shapes object. For this app, the top right and bottom left corners of the list item, but not make them fully circular. To achieve that, pass in 16.dp to the medium attribute.
medium = RoundedCornerShape(bottomStart = 16.dp, topEnd = 16.dp)

Since a Card, by default, already uses the medium shape, you do not have to explicitly set it to the medium shape. Check out the Preview and to see the newly shaped Card!

Woof preview with shaped cards

If you go back to the Theme.kt file in WoofTheme(), and look at the MaterialTheme(), you'll see the shapes attribute is set to the Shapes val that you just updated.

MaterialTheme(
   colors = colors,
   typography = Typography,
   shapes = Shapes,
   content = content
)

Below is a side-by-side look of the list items before and after shaping. Notice how much more visually appealing the app is with shaping added to it.

Without shaping

With shaping

Woof list items without shaping

Woof list items with shaping

6. Add typography

The Material Design type scale

A type scale is a selection of font styles that can be used across an app, ensuring a flexible, yet consistent, style. The Material Design type scale includes fifteen font styles that are supported by the type system. The naming and grouping have been simplified to: display, headline, title, body, and label, with large, medium, and small sizes for each. You only need to use these choices if you want to customize your app. If you don't know what to set for each type scale category, know that there is a default typography scale that you can use.

999a161dcd9b0ec4.png

The type scale contains reusable categories of text, each with an intended application and meaning.

Display

As the largest text on the screen, display styles are reserved for short, important text or numerals. They work best on large screens.

Headline

Headlines are best-suited for short, high-emphasis text on smaller screens. These styles can be good for marking primary passages of text or important regions of content.

Title

Titles are smaller than headline styles, and should be used for medium-emphasis text that remains relatively short.

Body

Body styles are used for longer passages of text in your app.

Label

Label styles are smaller, utilitarian styles, used for things like the text inside components or for very small text in the content body, such as captions.

Fonts

The Android platform provides a variety of fonts, but you may want to customize your app with a font not provided by default. Custom fonts can add personality and be used for branding.

In this section, you will add custom fonts called Abril Fatface, Montserrat Bold, and Montserrat Regular. You will use the displayLarge and displayMedium headlines, and the bodyLarge text from the Material Type system, and add these to the text in your app.

Create a font Android Resource Directory.

Before you add fonts to your app, you will need to add a font directory.

  1. In the project view of Android Studio, right-click on the res folder.
  2. Select New > Android Resource Directory.

This image shows navigating the file structure to the Android Resource Directory.

  1. Name the Directory font, set the Resource type as font, and click OK.

This image shows adding a font directory using the New Resource Directory.

  1. Open your new font resource directory located at res > font.

Download custom fonts

Since you are using fonts that are not provided by the Android platform, you need to download the custom fonts.

  1. Go to https://fonts.google.com/.
  2. Search for Montserrat and click Download family.
  3. Unzip the zip file.
  4. Open the downloaded Montserrat folder. In the static folder, find Montserrat-Bold.ttf and Montserrat-Regular.ttf (ttf stands for TrueType Font and is the format for font files). Select both fonts and drag them into the font resource directory in your project in Android Studio.

This image shows the contents of the static folder of Montserrat fonts.

  1. In your font folder, rename Montserrat-Bold.ttf to montserrat_bold.ttf and rename Montserrat-Regular.ttf to montserrat_regular.ttf.
  2. Search for Abril Fatface and click Download family.
  3. Open the downloaded Abril_Fatface folder. Select AbrilFatface-Regular.ttf and drag it into the font resource directory.
  4. In your font folder, rename Abril_Fatface_Regular.ttf to abril_fatface_regular.ttf.

This is what the font resource directory in your project should look like with the three custom font files:

This image shows the font files being added to the font folder.

Initialize fonts

  1. In the project window, open ui.theme > Type.kt. Initialize the downloaded fonts below the import statements and above the Typography val. First, initialize Abril Fatface by setting it equal to FontFamily and passing in Font with the font file abril_fatface_regular.
​​import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import com.example.woof.R

val AbrilFatface = FontFamily(
   Font(R.font.abril_fatface_regular)
)
  1. Initialize Montserrat, underneath Abril Fatface, by setting it equal to FontFamily and passing in Font with the font file montserrat_regular. For montserrat_bold, also include FontWeight.Bold. Even though you do pass in the bold version of the font file, Compose doesn't know that the file is bold, so you need to explicitly link the file to FontWeight.Bold.
import androidx.compose.ui.text.font.FontWeight

val AbrilFatface = FontFamily(
   Font(R.font.abril_fatface_regular)
)

val Montserrat = FontFamily(
   Font(R.font.montserrat_regular),
   Font(R.font.montserrat_bold, FontWeight.Bold)
)

Next, you set the different types of headlines to the fonts you just added. The Typography object has parameters for 13 different typefaces discussed above. You can define as many as you need. In this app, we will set displayLarge, displayMedium, and bodyLarge. In the next part of this app, you use labelSmall, so you will add it here.

Below is a table that shows you the font, weight and size of each headline you are adding.

8ea685b3871d5ffc.png

  1. For the displayLarge attribute, set it equal to TextStyle, and fill in the fontFamily, fontWeight, and fontSize with the information from the table above. This means that all the text set to displayLarge will have Abril Fatface as the font, with a normal font weight, and a fontSize of 36.sp.

Repeat this process for displayMedium, labelSmall, and bodyLarge.

import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.sp


val Typography = Typography(
   displayLarge = TextStyle(
       fontFamily = AbrilFatface,
       fontWeight = FontWeight.Normal,
       fontSize = 36.sp
   ),
   displayMedium = TextStyle(
       fontFamily = Montserrat,
       fontWeight = FontWeight.Bold,
       fontSize = 20.sp
   ),
   labelSmall = TextStyle(
       fontFamily = Montserrat,
       fontWeight = FontWeight.Bold,
       fontSize = 14.sp
   ),
   bodyLarge = TextStyle(
       fontFamily = Montserrat,
       fontWeight = FontWeight.Normal,
       fontSize = 14.sp
   )
)

If you go to the Theme.kt file in WoofTheme() and look at the MaterialTheme(), the typography parameter is equal to the Typography val that you just updated.

MaterialTheme(
   colors = colors,
   typography = Typography,
   shapes = Shapes,
   content = content
)

Add typography to app text

Now you'll add the headline types to each instance of text in the app.

  1. Add displayMedium as the style for the dogName because it is a short, important piece of information. Add bodyLarge as the style for the dogAge because it works well with smaller text sizes.
@Composable
fun DogInformation(
   @StringRes dogName: Int,
   dogAge: Int,
   modifier: Modifier = Modifier
) {
   Column(modifier = modifier) {
       Text(
           text = stringResource(dogName),
           style = MaterialTheme.typography.displayMedium,
           modifier = Modifier.padding(top = dimensionResource(id = R.dimen.padding_small))
       )
       Text(
           text = stringResource(R.string.years_old, dogAge),
           style = MaterialTheme.typography.bodyLarge
       )
   }
}
  1. Now in the WoofPreview(), the dog's name displays the bold Montserrat font in 20.sp, and the dog's age displays the normal Montserrat font in 14.sp.

Woof preview with typography added

Below is a side-by-side look at the list items before and after adding typography. Notice the font difference between the dog name and the dog age.

Without typography

With typography

Woof list item without typography

Woof list item with typography

7. Add a top bar

A Scaffold is a layout that provides slots for various components and screen elements, such as an Image, Row, or Column. A Scaffold also provides a slot for a TopAppBar, which you will use in this section.

A TopAppBar can be used for many purposes, but in this case, you will use it for branding and to give your app personality. There are four different types of TopAppBar: center, small, medium and large. In this codelab you will implement a center top app bar. You will create a composable that looks like the screenshot below, and slot it into the topBar section of a Scaffold.

172417c7b64372f7.png

For this app, our top bar consists of a Row with an image of a logo and text of the app title. The logo features a cute gradient paw and the title of the app!

736f411f5067e0b5.png

Add image and text to the top bar

  1. In MainActivity.kt, create a composable called WoofTopAppBar() with an optional modifier.
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {
  
}
  1. Next, Add the WoofTopAppBar() to the layout. In WoofApp(), add a Scaffold that surrounds the LazyColumn.
import androidx.compose.material3.Scaffold

@Composable
fun WoofApp() {
   Scaffold(

   ) {
       LazyColumn() {
           items(dogs) {
               DogItem(dog = it)
           }
       }
   }
}
  1. Scaffold supports the contentWindowInsets parameter which can help to specify insets for the scaffold content. WindowInsets are the parts of your screen where your app can intersect with the system UI, these ones are to be passed to the content slot via the PaddingValues parameters. Read more here.

The contentWindowInsets value is passed to the LazyColumn as the contentPadding.

@Composable
fun WoofApp() {
   Scaffold(

   ) { it ->
       LazyColumn(contentPadding = it) {
           items(dogs) {
               DogItem(
                   dog = it,
                   modifier = Modifier.padding(dimensionResource(id = R.dimen.padding_small))
               )
           }
       }
   }
}
  1. Within the Scaffold, add a topBar attribute and set it to WoofTopAppBar().
Scaffold(
   topBar = {
       WoofTopAppBar()
   }
)

Below is how the WoofApp() composable will look:

@Composable
fun WoofApp() {
   Scaffold(
       topBar = {
           WoofTopAppBar()
       }
   ) { it ->
       LazyColumn(contentPadding = it) {
           items(dogs) {
               DogItem(
                   dog = it,
                   modifier = Modifier.padding(dimensionResource(id = R.dimen.padding_small))
               )
           }
       }
   }
}

Nothing has changed in WoofPreview() because there is nothing in the WoofTopAppBar(). Let's change that!

Woof Preview with typography

  1. Within the WoofTopAppBar() Composable, add in a CenterAlignedTopAppBar() and set the modifier parameter to the modifier passed into WoofTopAppBar().
import androidx.compose.material3.CenterAlignedTopAppBar

@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {
   CenterAlignedTopAppBar(
       modifier = modifier
   )
}
  1. For the title parameter, pass in a Row which will hold the Image and Text of the CenterAlignedTopAppBar.
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier){
   CenterAlignedTopAppBar(
       title = {
           Row() {
              
           }
       },
       modifier = modifier
   )
}
  1. Add the logo Image into the Row.
  • Set the image size in the modifier as the image_size in the dimens.xml file and the padding as padding_small from the dimens.xml file.
  • Use painter to set the Image as ic_woof_logo from the drawable folder.
  • Set the contentDescription as null. In this situation, the app logo does not add any semantic information for users with impaired vision, so we do not have to add a content description.
Row() {
   Image(
       modifier = Modifier
           .size(dimensionResource(id = R.dimen.image_size))
           .padding(dimensionResource(id = R.dimen.padding_small)),
       painter = painterResource(R.drawable.ic_woof_logo),
       contentDescription = null
   )
}
  1. Next, add a Text Composable inside the Row after the Image.
  • Use stringResource() to set it to the value of app_name. This sets the text to the name of the app, which is stored in strings.xml.
  • Set the style of the text to displayLarge since the app name is short and important text.
Text(
   text = stringResource(R.string.app_name),
   style = MaterialTheme.typography.displayLarge
)

Woof Preview with top app bar

This is what shows up in WoofPreview(), it looks a little off because the icon and text are not vertically aligned.

  1. To fix that, add in a verticalAlignment value-parameter to the Row and set it equal to Alignment.CenterVertically.
import androidx.compose.ui.Alignment

Row(
   verticalAlignment = Alignment.CenterVertically
)

Woof Preview with vertically centered top app bar

That looks a lot better!

This is the full WoofTopAppBar() Composable:

@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {
   CenterAlignedTopAppBar(
       title = {
           Row(
               verticalAlignment = Alignment.CenterVertically
           ) {
               Image(
                   modifier = Modifier
                       .size(dimensionResource(id = R.dimen.image_size))
                       .padding(dimensionResource(id = R.dimen.padding_small)),
                   painter = painterResource(R.drawable.ic_woof_logo),

                   contentDescription = null
               )
               Text(
                   text = stringResource(R.string.app_name),
                   style = MaterialTheme.typography.displayLarge
               )
           }
       },
       modifier = modifier
   )
}

Run the app and admire how beautifully the TopAppBar ties the app together.

Without top app bar

With top app bar

Woof app without top app bar

Now, check out the final app in dark theme!

9a207c214269615.png

Congratulations, you made it to the end of the codelab!

8. Get the solution code

To download the code for the finished codelab, you can use these git commands:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git
$ cd basic-android-kotlin-compose-training-woof
$ git checkout material

Alternatively, you can download the repository as a zip file, unzip it, and open it in Android Studio.

If you want to see the solution code, view it on GitHub.

9. Conclusion

You just created your first Material app! You created a custom color palette for both light and dark themes, created shapes for different components, downloaded fonts and added them to the app, and created a beautiful top bar to tie it all together. Take the skills you learned in this codelab and change colors, shapes, and typography to make apps completely your own!

Summary

  • Material Theming allows you to use Material Design in your app, with guidance on customizing colors, typography, and shapes.
  • The Theme.kt file is where the theme is defined, through a composable named [your app name]+Theme()WoofTheme() in the case of this app. Within this function, the MaterialTheme object sets the color, typography, shapes, and content of the app.
  • Color.kt is where you list the colors you use in the app. Then in Theme.kt, you assign the colors in LightColorPalette and DarkColorPalette to specific slots. Not all slots need to be assigned.
  • Your app can opt-in to Force Dark, which means the system will implement a dark theme for you. However, it is a better experience for your users if you implement the dark theme so that you maintain full control over the app theme.
  • Shape.kt is where you define your app shapes. There are three shape sizes (small, medium, large), and you can specify how the corners are rounded.
  • Shapes direct attention, identify components, communicate state, and express brand.
  • Type.kt is where you initialize your fonts and assign fontFamily, fontWeight, and fontSize for the Material Design type scale.
  • The Material Design type scale includes a range of contrasting styles that support the needs of your app and its content. The type scale is a combination of 15 styles that are supported by the type system.

10. Learn more