Jetpack Compose themes are built around the Material Design system, providing a structured approach to styling your application. The theming system in Jetpack Compose revolves around three core components that work together to create cohesive user experiences.
The MaterialTheme
composable is the cornerstone of Jetpack Compose themes. It provides access to Material Design's color system, typography, and shapes throughout your application. When you wrap your UI content with MaterialTheme
, all child composables automatically inherit the theme properties.
MaterialTheme(
colorScheme = lightColorScheme(),
typography = Typography(),
shapes = Shapes()
) {
// Your UI content here
}
The MaterialTheme composable accepts three main parameters that define your Jetpack Compose themes:
Accessing theme properties in Jetpack Compose themes is straightforward through the MaterialTheme object. You can retrieve current theme values anywhere within your composable hierarchy:
@Composable
fun ThemedButton() {
Button(
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text(
text = "Themed Button",
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelLarge
)
}
}
Color schemes form the visual foundation of Jetpack Compose themes. The Material Design 3 color system provides a comprehensive palette that ensures accessibility and visual harmony across your application.
Jetpack Compose themes support both light and dark color schemes out of the box. You can create color schemes using the built-in functions or customize them to match your brand colors:
val LightColorScheme = lightColorScheme(
primary = Color(0xFF6200EE),
onPrimary = Color.White,
primaryContainer = Color(0xFF3700B3),
onPrimaryContainer = Color.White,
secondary = Color(0xFF03DAC6),
onSecondary = Color.Black
)
val DarkColorScheme = darkColorScheme(
primary = Color(0xFFBB86FC),
onPrimary = Color.Black,
primaryContainer = Color(0xFF3700B3),
onPrimaryContainer = Color.White,
secondary = Color(0xFF03DAC6),
onSecondary = Color.Black
)
Android 12 introduced dynamic colors, which Jetpack Compose themes can leverage to create personalized user experiences. Dynamic colors extract accent colors from the user's wallpaper:
@Composable
fun dynamicColorScheme(): ColorScheme {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val context = LocalContext.current
if (isSystemInDarkTheme()) {
dynamicDarkColorScheme(context)
} else {
dynamicLightColorScheme(context)
}
} else {
if (isSystemInDarkTheme()) DarkColorScheme else LightColorScheme
}
}
While Material Design provides a comprehensive color system, you might need additional colors for your Jetpack Compose themes. You can extend the color scheme with custom properties:
val LocalCustomColors = compositionLocalOf {
CustomColors(
success = Color(0xFF4CAF50),
warning = Color(0xFFFF9800),
error = Color(0xFFF44336)
)
}
data class CustomColors(
val success: Color,
val warning: Color,
val error: Color
)
Typography plays a crucial role in Jetpack Compose themes, defining how text appears throughout your application. The Material Design typography system provides a scale of text styles that create visual hierarchy and improve readability.
Jetpack Compose themes include a predefined typography scale with different text styles for various use cases:
val CustomTypography = Typography(
displayLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = 0.sp
),
headlineLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
)
Jetpack Compose themes allow you to incorporate custom fonts to match your brand identity. You can load fonts from resources and apply them across your typography system:
val PoppinsFontFamily = FontFamily(
Font(R.font.poppins_regular, FontWeight.Normal),
Font(R.font.poppins_medium, FontWeight.Medium),
Font(R.font.poppins_semibold, FontWeight.SemiBold),
Font(R.font.poppins_bold, FontWeight.Bold)
)
val AppTypography = Typography(
displayLarge = Typography().displayLarge.copy(fontFamily = PoppinsFontFamily),
headlineLarge = Typography().headlineLarge.copy(fontFamily = PoppinsFontFamily),
bodyLarge = Typography().bodyLarge.copy(fontFamily = PoppinsFontFamily)
)
Shapes define the corner styling and overall geometric appearance of components in Jetpack Compose themes. The shape system provides consistency across buttons, cards, and other UI elements.
The default shape system in Jetpack Compose themes includes three categories of shapes with predefined corner radius values:
val CustomShapes = Shapes(
extraSmall = RoundedCornerShape(4.dp),
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(16.dp),
extraLarge = RoundedCornerShape(28.dp)
)
You can create custom shapes for specific components in your Jetpack Compose themes. This flexibility allows you to create unique visual identities:
val AppShapes = Shapes(
small = CutCornerShape(
topStart = 8.dp,
topEnd = 0.dp,
bottomEnd = 8.dp,
bottomStart = 0.dp
),
medium = RoundedCornerShape(
topStart = 16.dp,
topEnd = 4.dp,
bottomEnd = 16.dp,
bottomStart = 4.dp
),
large = RoundedCornerShape(24.dp)
)
Building custom Jetpack Compose themes requires combining color schemes, typography, and shapes into a cohesive design system. This approach ensures consistency while allowing for brand differentiation.
Creating a custom theme composable encapsulates your design system and makes it reusable across different parts of your application:
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
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 -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content
)
}
Managing theme state in Jetpack Compose themes often involves handling user preferences for dark mode, dynamic colors, and other customization options:
@Composable
fun ThemeProvider(content: @Composable () -> Unit) {
var isDarkTheme by remember { mutableStateOf(false) }
var useDynamicColors by remember { mutableStateOf(true) }
CompositionLocalProvider(
LocalThemeState provides ThemeState(
isDarkTheme = isDarkTheme,
useDynamicColors = useDynamicColors,
toggleDarkTheme = { isDarkTheme = !isDarkTheme },
toggleDynamicColors = { useDynamicColors = !useDynamicColors }
)
) {
MyAppTheme(
darkTheme = isDarkTheme,
dynamicColor = useDynamicColors,
content = content
)
}
}
data class ThemeState(
val isDarkTheme: Boolean,
val useDynamicColors: Boolean,
val toggleDarkTheme: () -> Unit,
val toggleDynamicColors: () -> Unit
)
val LocalThemeState = compositionLocalOf<ThemeState> {
error("ThemeState not provided")
}
Advanced Jetpack Compose themes implementation involves handling complex scenarios like component-specific theming, animation support, and accessibility considerations.
Sometimes you need to apply different styling to specific components while maintaining the overall Jetpack Compose themes structure:
@Composable
fun ThemedCard(
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
Card(
modifier = modifier,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
),
shape = MaterialTheme.shapes.large,
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
content = content
)
}
}
Jetpack Compose themes can include smooth transitions when switching between light and dark modes or applying different color schemes:
@Composable
fun AnimatedTheme(
targetColorScheme: ColorScheme,
content: @Composable () -> Unit
) {
val animatedColorScheme by animateColorSchemeAsState(
targetColorScheme = targetColorScheme,
animationSpec = tween(durationMillis = 600)
)
MaterialTheme(
colorScheme = animatedColorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content
)
}
@Composable
fun animateColorSchemeAsState(
targetColorScheme: ColorScheme,
animationSpec: AnimationSpec<Color> = spring()
): State<ColorScheme> {
val primary by animateColorAsState(targetColorScheme.primary, animationSpec)
val onPrimary by animateColorAsState(targetColorScheme.onPrimary, animationSpec)
val surface by animateColorAsState(targetColorScheme.surface, animationSpec)
val onSurface by animateColorAsState(targetColorScheme.onSurface, animationSpec)
return remember(primary, onPrimary, surface, onSurface) {
derivedStateOf {
targetColorScheme.copy(
primary = primary,
onPrimary = onPrimary,
surface = surface,
onSurface = onSurface
)
}
}
}
Here's a comprehensive example that demonstrates how to implement a complete theming system with all the concepts we've covered:
// File: Theme.kt
package com.example.myapp.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
// Custom Colors
data class CustomColors(
val success: Color = Color(0xFF4CAF50),
val warning: Color = Color(0xFFFF9800),
val error: Color = Color(0xFFF44336)
)
val LocalCustomColors = compositionLocalOf { CustomColors() }
// Color Schemes
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF6200EE),
onPrimary = Color.White,
primaryContainer = Color(0xFF3700B3),
onPrimaryContainer = Color.White,
secondary = Color(0xFF03DAC6),
onSecondary = Color.Black,
tertiary = Color(0xFF018786),
onTertiary = Color.White,
background = Color(0xFFFFFBFE),
onBackground = Color(0xFF1C1B1F),
surface = Color(0xFFFFFBFE),
onSurface = Color(0xFF1C1B1F),
surfaceVariant = Color(0xFFE7E0EC),
onSurfaceVariant = Color(0xFF49454F),
outline = Color(0xFF79747E)
)
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFFBB86FC),
onPrimary = Color.Black,
primaryContainer = Color(0xFF3700B3),
onPrimaryContainer = Color.White,
secondary = Color(0xFF03DAC6),
onSecondary = Color.Black,
tertiary = Color(0xFF03DAC6),
onTertiary = Color.Black,
background = Color(0xFF1C1B1F),
onBackground = Color(0xFFE6E1E5),
surface = Color(0xFF1C1B1F),
onSurface = Color(0xFFE6E1E5),
surfaceVariant = Color(0xFF49454F),
onSurfaceVariant = Color(0xFFCAC4D0),
outline = Color(0xFF938F99)
)
// Typography
private val AppTypography = Typography(
displayLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
),
displayMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 45.sp,
lineHeight = 52.sp,
letterSpacing = 0.sp
),
displaySmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 36.sp,
lineHeight = 44.sp,
letterSpacing = 0.sp
),
headlineLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
),
headlineMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 28.sp,
lineHeight = 36.sp,
letterSpacing = 0.sp
),
headlineSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp
),
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
titleMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp
),
titleSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
bodyMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
),
bodySmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.4.sp
),
labelLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
labelMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
)
// Shapes
private val AppShapes = Shapes(
extraSmall = RoundedCornerShape(4.dp),
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(16.dp),
extraLarge = RoundedCornerShape(28.dp)
)
// Theme State
data class ThemeState(
val isDarkTheme: Boolean,
val useDynamicColors: Boolean,
val toggleDarkTheme: () -> Unit,
val toggleDynamicColors: () -> Unit
)
val LocalThemeState = compositionLocalOf<ThemeState> {
error("ThemeState not provided")
}
// Main Theme Composable
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
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 -> DarkColorScheme
else -> LightColorScheme
}
val customColors = if (darkTheme) {
CustomColors(
success = Color(0xFF81C784),
warning = Color(0xFFFFB74D),
error = Color(0xFFE57373)
)
} else {
CustomColors()
}
CompositionLocalProvider(LocalCustomColors provides customColors) {
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content
)
}
}
// Theme Provider with State Management
@Composable
fun ThemeProvider(content: @Composable () -> Unit) {
var isDarkTheme by remember { mutableStateOf(false) }
var useDynamicColors by remember { mutableStateOf(true) }
CompositionLocalProvider(
LocalThemeState provides ThemeState(
isDarkTheme = isDarkTheme,
useDynamicColors = useDynamicColors,
toggleDarkTheme = { isDarkTheme = !isDarkTheme },
toggleDynamicColors = { useDynamicColors = !useDynamicColors }
)
) {
MyAppTheme(
darkTheme = isDarkTheme,
dynamicColor = useDynamicColors,
content = content
)
}
}
// Usage Example Activity
// File: MainActivity.kt
package com.example.myapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.myapp.ui.theme.*
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ThemeProvider {
MyAppContent()
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyAppContent() {
val themeState = LocalThemeState.current
val customColors = LocalCustomColors.current
Scaffold(
topBar = {
TopAppBar(
title = { Text("Jetpack Compose Themes Demo") },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Theme Controls
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "Theme Controls",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Dark Theme")
Switch(
checked = themeState.isDarkTheme,
onCheckedChange = { themeState.toggleDarkTheme() }
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Dynamic Colors")
Switch(
checked = themeState.useDynamicColors,
onCheckedChange = { themeState.toggleDynamicColors() }
)
}
}
}
// Typography Examples
Card(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "Typography Scale",
style = MaterialTheme.typography.titleMedium
)
Text(
text = "Display Large",
style = MaterialTheme.typography.displayLarge
)
Text(
text = "Headline Large",
style = MaterialTheme.typography.headlineLarge
)
Text(
text = "Title Large",
style = MaterialTheme.typography.titleLarge
)
Text(
text = "Body Large - This demonstrates the body text style used for regular content in your application.",
style = MaterialTheme.typography.bodyLarge
)
Text(
text = "Label Large",
style = MaterialTheme.typography.labelLarge
)
}
}
// Color Examples
Card(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "Color Palette",
style = MaterialTheme.typography.titleMedium
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { },
modifier = Modifier.weight(1f)
) {
Text("Primary")
}
OutlinedButton(
onClick = { },
modifier = Modifier.weight(1f)
) {
Text("Secondary")
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { },
colors = ButtonDefaults.buttonColors(
containerColor = customColors.success
),
modifier = Modifier.weight(1f)
) {
Text("Success", color = MaterialTheme.colorScheme.onPrimary)
}
Button(
onClick = { },
colors = ButtonDefaults.buttonColors(
containerColor = customColors.warning
),
modifier = Modifier.weight(1f)
) {
Text("Warning", color = MaterialTheme.colorScheme.onPrimary)
}
}
}
}
// Shape Examples
Card(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "Shape System",
style = MaterialTheme.typography.titleMedium
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Card(
modifier = Modifier
.size(60.dp)
.weight(1f),
shape = MaterialTheme.shapes.small
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Small", style = MaterialTheme.typography.labelSmall)
}
}
Card(
modifier = Modifier
.size(60.dp)
.weight(1f),
shape = MaterialTheme.shapes.medium
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Medium", style = MaterialTheme.typography.labelSmall)
}
}
Card(
modifier = Modifier
.size(60.dp)
.weight(1f),
shape = MaterialTheme.shapes.large
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Large", style = MaterialTheme.typography.labelSmall)
}
}
}
}
}
}
}
}
This comprehensive example demonstrates how to implement Jetpack Compose themes in a real Android application. The code includes all necessary imports, dependencies, and a complete working implementation that you can run directly in your Android project. The example showcases color schemes, typography, shapes, theme state management, and custom color extensions, providing a solid foundation for building themed applications with Jetpack Compose themes.
To use this code, simply create the theme files in your project's ui.theme
package and replace your MainActivity with the provided implementation. The theming system will automatically handle light/dark mode switching and dynamic colors where supported, giving you a fully functional Jetpack Compose themes implementation for your Android applications.