Instruction

Material Theming in Jetpack Compose

Theming in Jetpack Compose has a different take compared to how it’s done in the UI toolkit. Instead of inheriting from a Material components style, extending the theme as per your requirement, and applying them to your views, you define:

  • Your color palette for the app.
  • The typographic scale for your app.
  • The shape profile for your app.

You then use the above to build an instance of a material theme with custom properties. Then, to apply this theme, you create a parent composable that wraps your top-level app composable.

Here’s what the MaterialTheme composable looks like:

@Composable  
fun MaterialTheme(  
  colors: Colors = MaterialTheme.colors,  
  typography: Typography = MaterialTheme.typography,  
  shapes: Shapes = MaterialTheme.shapes,  
  content: @Composable () -> Unit  
)

It accepts four arguments:

  • colors: A custom definition of the Material Color scheme for your app.
  • typography: A set of custom text styles to be used as the typographic hierarchy for your app.
  • shapes: A set of shapes to be used by the components of your theme.
  • content: The composables that’ll use the defined theme.

Exploring Colors in Material Design

A Material Design compliant color system consists of the following key colors:

  • Surface: Colors used for the background and large, low-emphasis areas of the screen.
  • Primary, secondary, and tertiary: Primary is the color used most often in your app, while secondary and tertiary accent it.
  • On: Colors with this prefix indicate a color for text or icons on top of its paired parent color.
  • Variant: Colors with this suffix offer a lower emphasis alternative to its non-variant pair.

It’s difficult to communicate a visual concept like colors in plain text, so here’s a sample palette based on the above scheme:

There’s a lot more nuance to each color, the role it plays in the material palette, and what pairing works best, which is beyond the scope of this module. So, I encourage you to explore further on the Material Design docs.

Implementing a Color Palette

Based on the schematic discussed above, a Material compliant palette can be created as follows:

val yellow200 = Color(0xffffeb46)
val yellow400 = Color(0xffffc000)
val yellow500 = Color(0xffffde03)
val yellowDarkPrimary = Color(0xff242316)

val blue200 = Color(0xff91a4fc)
val blue700 = Color(0xff0336ff)
val blue800 = Color(0xff0035c9)
val blueDarkPrimary = Color(0xff1c1d24)

Using this palette, you can create a material color scheme for light and dark modes, as shown below:

private val LightPalette = lightColors(
  primary = yellow500,
  primaryVariant = yellow400,
  onPrimary = Color.Black,
  secondary = blue700,
  secondaryVariant = blue800,
  onSecondary = Color.White
)

private val DarkPalette = darkColors(
  primary = yellow200,
  secondary = blue200,
  onSecondary = Color.Black,
  surface = yellowDarkPrimary
)

The color scheme can then be used in a custom Material theme as follows:

@Composable
fun MyCustomYellowTheme(
  darkTheme: Boolean = isSystemInDarkTheme(),
  content: @Composable () -> Unit
) {
  val colors = if (darkTheme) {
    DarkPalette
  } else {
    LightPalette
  }

  MaterialTheme(  
    colors = colors,  
    content = content  
  )
}

In the snippet above, you:

  • Determined which palette to pick based on whether the system is in dark mode.
  • Passed the palette to the MaterialTheme composable, alongside the content, which represents the parent composable of your app.

The above theme can then be used by wrapping the parent app composable with our custom theme.

override fun onCreate(savedInstanceState: Bundle?) {  
  super.onCreate(savedInstanceState)  
  setContent {  
    MyCustomYellowTheme {  
      TaxiApp()  
    }    
  }  
}

In cases where you need to manually customize/specify the color, you can do so as follows:

TopAppBar(
  backgroundColor = MaterialTheme.colors.primarySurface,
  // ...
)

Typography in Material Design

A typographic scale is a collection of font styles that can be used across an app for different levels of emphasis and for establishing visual hierarchy.

Material Design defines a typographic scale, which includes styles like H1-H6, subtitle, caption, body etc.

For reference, a sample type scale using the Roboto font family is shown below.

Typography has many nuances, like understanding a font property and determining what fonts to pair and their different roles. Going deep into the topic could easily be another chapter on its own and is beyond the scope of this lesson. I encourage you to read further on the Material Design typography section.

Implementing a Typographic Scale

Building the typographic scale becomes quite straightforward once you’ve determined what font pairing you’d like to use in your app.

After selecting your fonts, download their .ttf (TrueType Font) versions and place them in the res/font directory, as shown below:

Compose implements the type system with the TypographyTextStyle, and font-related classes. You can initialize these font files in code as shown below:

val raleway = FontFamily(
  Font(R.font.raleway_regular),
  Font(R.font.raleway_medium, FontWeight.W500),
  Font(R.font.raleway_semibold, FontWeight.SemiBold)
)

Here, you’re referencing the font files and creating a FontFamily instance by passing in the font’s resource ID along with its corresponding weight for each font.

You can then use the font family to create your typographic scale as follows:

val typography = Typography(
  h1 = TextStyle(
    fontFamily = raleway,
    fontWeight = FontWeight.W300,
    fontSize = 96.sp),

  body1 = TextStyle(
    fontFamily = raleway,
    fontWeight = FontWeight.W600,
    fontSize = 16.sp),

  button = TextStyle(  
    fontSize = 14.sp,  
    fontFamily = raleway,  
    fontWeight = FontWeight.Normal)

  /*... Define more styles as needed ...*/
)

In the snippet above, you’re overriding the type specification for h1, body1 and button using your custom font family.

Once you’re done defining your type scale, you can then plug it into your theme by passing it as the typography parameter.

@Composable
fun MyCustomYellowTheme(
  darkTheme: Boolean = isSystemInDarkTheme(),
  content: @Composable () -> Unit
) {
  /*...*/

  MaterialTheme(  
    colors = colors, 
    typography = Typography, 
    content = content  
  )
}

You can then use the text styles in your app as shown below, and your customization will get applied:

Text(
  text = "Heading",
  style = MaterialTheme.typography.h1
)

Exploring Shapes in Material Design

Material Design uses shapes extensively. According to the guidelines, surfaces can be displayed in different shapes. Shapes direct attention, identify components, communicate state, and express brand.

Material Design uses a rectangular shape by default with 4dp rounded corners. This default can be further customized by tweaking the size, edge angles, curves etc.

You can read more details about shapes in the Shape section of the Material Design docs.

In Compose, you can implement the shape system with the Shapes class and customize it using the small, medium, and large variants. Here’s what that would look like:

val customShapes = Shapes(
  small = RoundedCornerShape(percent = 50),
  medium = RoundedCornerShape(0f),
  large = CutCornerShape(
    topStart = 16.dp,
    topEnd = 0.dp,
    bottomEnd = 0.dp,
    bottomStart = 16.dp
  )
)

You can then use your custom shape specification in your theme as follows:

@Composable
fun MyCustomYellowTheme(
  darkTheme: Boolean = isSystemInDarkTheme(),
  content: @Composable () -> Unit
) {
  /*...*/

  MaterialTheme(  
    colors = colors, 
    typography = Typography,
    shapes = customShapes, 
    content = content  
  )
}

Then, you can apply these shapes in your composables via MaterialTheme.shapes as shown below.

Card(shape = MaterialTheme.shapes.large) {
  /*...*/
}

Great job. Now, move on to the demo.

See forum comments
Download course materials from Github
Previous: Introduction Next: Demo