How to Build a Consistent Custom App Theme in Jetpack Compose (Material 3)

Table of Contents

Creating a polished Android app starts with one crucial decision: your app’s visual identity. If you’ve been wondering how to make your Jetpack Compose app look consistent and professional across every screen, you’re in the right place.

In this guide, I’ll walk you through building a Custom App Theme in Jetpack Compose using Material 3. Whether you’re building your first app or refining an existing one, you’ll learn how to create a theming system that’s both flexible and maintainable.

Why Material 3 Makes Custom Theming Easier

Material 3 (also called Material You) isn’t just another design system update. It’s Google’s most flexible theming framework yet, and it plays beautifully with Jetpack Compose.

Here’s what makes it special:

Dynamic color support — Your app can adapt to the user’s wallpaper colors (on Android 12+) 

Improved design tokens — More granular control over colors, typography, and shapes Better accessibility — Built-in contrast and readability improvements

The best part..? 

Once you set up your Custom App Theme in Jetpack Compose, Material 3 handles the heavy lifting of maintaining consistency throughout your app.

Understanding the Theme Building Blocks

Before we dive into code, let’s understand what makes up a theme in Jetpack Compose. Think of it like building a house — you need a solid foundation.

Your theme consists of three main pillars:

  1. Color Scheme — All the colors your app uses
  2. Typography — Font families, sizes, and weights
  3. Shapes — Corner radiuses and component shapes

When these three work together harmoniously, your app feels intentional and polished.

Setting Up Your Project Dependencies

First things first — make sure you have the right dependencies in your build.gradle.kts file:

Kotlin
dependencies {
    implementation("androidx.compose.material3:material3:1.2.0")
    implementation("androidx.compose.ui:ui:1.6.0")
    implementation("androidx.compose.ui:ui-tooling-preview:1.6.0")
}

These dependencies give you access to Material 3 components and theming capabilities. 

Always check for the latest stable versions on the official documentation.

Creating Your Custom Color Scheme

Colors are the personality of your app. Let’s create a custom color palette that reflects your brand.

Define Your Color Palette

Create a new Kotlin file called Color.kt in your UI theme package:

Kotlin
package com.yourapp.ui.theme

import androidx.compose.ui.graphics.Color

// Light Theme Colors
val PrimaryLight = Color(0xFF6750A4)
val OnPrimaryLight = Color(0xFFFFFFFF)
val PrimaryContainerLight = Color(0xFFEADDFF)
val OnPrimaryContainerLight = Color(0xFF21005D)
val SecondaryLight = Color(0xFF625B71)
val OnSecondaryLight = Color(0xFFFFFFFF)
val SecondaryContainerLight = Color(0xFFE8DEF8)
val OnSecondaryContainerLight = Color(0xFF1D192B)
val TertiaryLight = Color(0xFF7D5260)
val OnTertiaryLight = Color(0xFFFFFFFF)
val TertiaryContainerLight = Color(0xFFFFD8E4)
val OnTertiaryContainerLight = Color(0xFF31111D)
val ErrorLight = Color(0xFFB3261E)
val OnErrorLight = Color(0xFFFFFFFF)
val ErrorContainerLight = Color(0xFFF9DEDC)
val OnErrorContainerLight = Color(0xFF410E0B)
val BackgroundLight = Color(0xFFFFFBFE)
val OnBackgroundLight = Color(0xFF1C1B1F)
val SurfaceLight = Color(0xFFFFFBFE)
val OnSurfaceLight = Color(0xFF1C1B1F)

// Dark Theme Colors
val PrimaryDark = Color(0xFFD0BCFF)
val OnPrimaryDark = Color(0xFF381E72)
val PrimaryContainerDark = Color(0xFF4F378B)
val OnPrimaryContainerDark = Color(0xFFEADDFF)
val SecondaryDark = Color(0xFFCCC2DC)
val OnSecondaryDark = Color(0xFF332D41)
val SecondaryContainerDark = Color(0xFF4A4458)
val OnSecondaryContainerDark = Color(0xFFE8DEF8)
val TertiaryDark = Color(0xFFEFB8C8)
val OnTertiaryDark = Color(0xFF492532)
val TertiaryContainerDark = Color(0xFF633B48)
val OnTertiaryContainerDark = Color(0xFFFFD8E4)
val ErrorDark = Color(0xFFF2B8B5)
val OnErrorDark = Color(0xFF601410)
val ErrorContainerDark = Color(0xFF8C1D18)
val OnErrorContainerDark = Color(0xFFF9DEDC)
val BackgroundDark = Color(0xFF1C1B1F)
val OnBackgroundDark = Color(0xFFE6E1E5)
val SurfaceDark = Color(0xFF1C1B1F)
val OnSurfaceDark = Color(0xFFE6E1E5)

Here,

Each color has a specific role in Material 3. The naming convention follows a pattern:

  • Primary – Your brand’s main color
  • OnPrimary – Text/icon color that appears on top of primary
  • PrimaryContainer – A lighter shade for containers
  • OnPrimaryContainer – Text/icons on primary containers

This naming system ensures your Custom App Theme in Jetpack Compose maintains proper contrast and readability automatically.

Build Your Color Schemes

Now let’s create the actual color scheme objects. Add this to a new file called Theme.kt:

Kotlin
package com.yourapp.ui.theme

import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme

private val LightColorScheme = lightColorScheme(
    primary = PrimaryLight,
    onPrimary = OnPrimaryLight,
    primaryContainer = PrimaryContainerLight,
    onPrimaryContainer = OnPrimaryContainerLight,
    
    secondary = SecondaryLight,
    onSecondary = OnSecondaryLight,
    secondaryContainer = SecondaryContainerLight,
    onSecondaryContainer = OnSecondaryContainerLight,
    
    tertiary = TertiaryLight,
    onTertiary = OnTertiaryLight,
    tertiaryContainer = TertiaryContainerLight,
    onTertiaryContainer = OnTertiaryContainerLight,
    
    error = ErrorLight,
    onError = OnErrorLight,
    errorContainer = ErrorContainerLight,
    onErrorContainer = OnErrorContainerLight,
    
    background = BackgroundLight,
    onBackground = OnBackgroundLight,
    surface = SurfaceLight,
    onSurface = OnSurfaceLight,
)

private val DarkColorScheme = darkColorScheme(
    primary = PrimaryDark,
    onPrimary = OnPrimaryDark,
    primaryContainer = PrimaryContainerDark,
    onPrimaryContainer = OnPrimaryContainerDark,
    
    secondary = SecondaryDark,
    onSecondary = OnSecondaryDark,
    secondaryContainer = SecondaryContainerDark,
    onSecondaryContainer = OnSecondaryContainerDark,
    
    tertiary = TertiaryDark,
    onTertiary = OnTertiaryDark,
    tertiaryContainer = TertiaryContainerDark,
    onTertiaryContainer = OnTertiaryContainerDark,
    
    error = ErrorDark,
    onError = OnErrorDark,
    errorContainer = ErrorContainerDark,
    onErrorContainer = OnErrorContainerDark,
    
    background = BackgroundDark,
    onBackground = OnBackgroundDark,
    surface = SurfaceDark,
    onSurface = OnSurfaceDark,
)

These functions (lightColorScheme and darkColorScheme) are provided by Material 3 to ensure your colors meet accessibility standards.

Customizing Typography

Typography shapes how users read and understand your content. Let’s create a type scale that’s both beautiful and functional.

Defining Custom Fonts

Create a Type.kt file:

Kotlin
package com.yourapp.ui.theme

import androidx.compose.material3.Typography
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.sp

// Define your custom font families
// Make sure to add your font files to res/font/
val Montserrat = FontFamily(
    Font(R.font.montserrat_regular, FontWeight.Normal),
    Font(R.font.montserrat_medium, FontWeight.Medium),
    Font(R.font.montserrat_semibold, FontWeight.SemiBold),
    Font(R.font.montserrat_bold, FontWeight.Bold)
)

val Roboto = FontFamily(
    Font(R.font.roboto_regular, FontWeight.Normal),
    Font(R.font.roboto_medium, FontWeight.Medium),
    Font(R.font.roboto_bold, FontWeight.Bold)
)


// Custom Typography
val AppTypography = Typography(
    // Display styles - for large, impactful text
    displayLarge = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Bold,
        fontSize = 57.sp,
        lineHeight = 64.sp,
        letterSpacing = (-0.25).sp
    ),
    displayMedium = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Bold,
        fontSize = 45.sp,
        lineHeight = 52.sp,
        letterSpacing = 0.sp
    ),
    displaySmall = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Bold,
        fontSize = 36.sp,
        lineHeight = 44.sp,
        letterSpacing = 0.sp
    ),
    
    // Headline styles - for section headers
    headlineLarge = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.SemiBold,
        fontSize = 32.sp,
        lineHeight = 40.sp,
        letterSpacing = 0.sp
    ),
    headlineMedium = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.SemiBold,
        fontSize = 28.sp,
        lineHeight = 36.sp,
        letterSpacing = 0.sp
    ),
    headlineSmall = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.SemiBold,
        fontSize = 24.sp,
        lineHeight = 32.sp,
        letterSpacing = 0.sp
    ),
    
    // Title styles - for card titles and important text
    titleLarge = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Medium,
        fontSize = 22.sp,
        lineHeight = 28.sp,
        letterSpacing = 0.sp
    ),
    titleMedium = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Medium,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.15.sp
    ),
    titleSmall = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Medium,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 0.1.sp
    ),
    
    // Body styles - for main content
    bodyLarge = TextStyle(
        fontFamily = Roboto,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.5.sp
    ),
    bodyMedium = TextStyle(
        fontFamily = Roboto,
        fontWeight = FontWeight.Normal,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 0.25.sp
    ),
    bodySmall = TextStyle(
        fontFamily = Roboto,
        fontWeight = FontWeight.Normal,
        fontSize = 12.sp,
        lineHeight = 16.sp,
        letterSpacing = 0.4.sp
    ),
    
    // Label styles - for buttons and small UI elements
    labelLarge = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Medium,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 0.1.sp
    ),
    labelMedium = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Medium,
        fontSize = 12.sp,
        lineHeight = 16.sp,
        letterSpacing = 0.5.sp
    ),
    labelSmall = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Medium,
        fontSize = 11.sp,
        lineHeight = 16.sp,
        letterSpacing = 0.5.sp
    )
)

Understanding the type scale:

Material 3 provides 15 different text styles organized into five categories. This gives you flexibility while maintaining consistency in your Custom App Theme in Jetpack Compose:

  • Display — Hero text, splash screens
  • Headline — Page titles, section headers
  • Title — Card titles, dialog headers
  • Body — Paragraphs, main content
  • Label — Buttons, tabs, small UI elements

Defining Custom Shapes

Shapes add personality to your UI. From rounded corners to sharp edges, shapes influence how modern or traditional your app feels.

Create a Shape.kt file:

Kotlin
package com.yourapp.ui.theme

import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp

val AppShapes = Shapes(
    // Extra small - chips, small buttons
    extraSmall = RoundedCornerShape(4.dp),
    
    // Small - buttons, text fields
    small = RoundedCornerShape(8.dp),
    
    // Medium - cards, dialogs
    medium = RoundedCornerShape(12.dp),
    
    // Large - bottom sheets, large cards
    large = RoundedCornerShape(16.dp),
    
    // Extra large - special components
    extraLarge = RoundedCornerShape(28.dp)
)

Shape usage tips:

  • Use extraSmall for chips and toggles
  • Use small for buttons and input fields
  • Use medium for cards and elevated surfaces
  • Use large for bottom sheets and modals
  • Use extraLarge for floating action buttons

You can also create asymmetric shapes or custom shapes using GenericShape for more creative designs.

Bringing It All Together: Your Theme Composable

Now comes the exciting part — assembling everything into your main theme composable. Update your Theme.kt file:

Kotlin
package com.yourapp.ui.theme

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
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

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true, // Dynamic color available on Android 12+
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        // Dynamic color (Material You) - uses user's wallpaper colors
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) 
            else dynamicLightColorScheme(context)
        }
        // Dark theme
        darkTheme -> DarkColorScheme
        // Light theme
        else -> LightColorScheme
    }
    
    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,
        typography = AppTypography,
        shapes = AppShapes,
        content = content
    )
}
  1. Dark theme detection — Automatically detects if the user prefers dark mode
  2. Dynamic color support — On Android 12+, colors adapt to the user’s wallpaper
  3. Status bar styling — Ensures the status bar matches your theme
  4. Fallback colors — Uses your custom colors on older Android versions

This is the heart of your Custom App Theme in Jetpack Compose. Every screen that uses this theme will automatically have consistent colors, typography, and shapes.

Using Your Theme in the App

Now let’s see how to apply your theme to your app. In your MainActivity.kt:

Kotlin
package com.yourapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.yourapp.ui.theme.AppTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AppTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    HomeScreen()
                }
            }
        }
    }
}

@Composable
fun HomeScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        // Using themed text styles
        Text(
            text = "Welcome to My App",
            style = MaterialTheme.typography.headlineLarge,
            color = MaterialTheme.colorScheme.primary
        )
        
        Text(
            text = "This is a subtitle showing our custom typography",
            style = MaterialTheme.typography.titleMedium,
            color = MaterialTheme.colorScheme.onSurface
        )
        
        Text(
            text = "Body text looks great with our custom font family. " +
                  "Notice how everything feels cohesive and professional.",
            style = MaterialTheme.typography.bodyLarge,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )
        
        // Using themed button with custom shapes
        Button(
            onClick = { /* Handle click */ },
            shape = MaterialTheme.shapes.medium
        ) {
            Text("Primary Button")
        }
        
        // Using themed card
        Card(
            modifier = Modifier.fillMaxWidth(),
            shape = MaterialTheme.shapes.large,
            colors = CardDefaults.cardColors(
                containerColor = MaterialTheme.colorScheme.primaryContainer
            )
        ) {
            Column(modifier = Modifier.padding(16.dp)) {
                Text(
                    text = "Card Title",
                    style = MaterialTheme.typography.titleLarge,
                    color = MaterialTheme.colorScheme.onPrimaryContainer
                )
                Spacer(modifier = Modifier.height(8.dp))
                Text(
                    text = "This card uses our theme colors and shapes automatically.",
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.onPrimaryContainer
                )
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun HomeScreenPreview() {
    AppTheme {
        HomeScreen()
    }
}

Key takeaways from this example:

  • Wrap your content in AppTheme { } to apply your custom theme
  • Access colors via MaterialTheme.colorScheme.primary (not hardcoded values!)
  • Access typography via MaterialTheme.typography.headlineLarge
  • Access shapes via MaterialTheme.shapes.medium

This approach ensures your Custom App Theme in Jetpack Compose is applied consistently throughout your app.

Creating Theme-Aware Components

Let’s build a custom component that respects your theme. This is where the real power of theming shines:

Kotlin
@Composable
fun ThemedInfoCard(
    title: String,
    description: String,
    modifier: Modifier = Modifier
) {
    Card(
        modifier = modifier.fillMaxWidth(),
        shape = MaterialTheme.shapes.large,
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.secondaryContainer
        ),
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            Text(
                text = title,
                style = MaterialTheme.typography.titleLarge,
                color = MaterialTheme.colorScheme.onSecondaryContainer
            )
            
            Divider(
                color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
                thickness = 1.dp
            )
            
            Text(
                text = description,
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSecondaryContainer
            )
        }
    }
}

// Usage
@Composable
fun ExampleScreen() {
    Column(modifier = Modifier.padding(16.dp)) {
        ThemedInfoCard(
            title = "Themed Component",
            description = "This card automatically adapts to light/dark theme changes!"
        )
    }
}

This component will automatically look perfect in both light and dark modes because it uses theme values instead of hardcoded colors.

Advanced: Adding Custom Theme Values

Sometimes you need values beyond what Material 3 provides. Here’s how to extend your theme:

Kotlin
// Create custom theme values
data class CustomColors(
    val success: Color,
    val onSuccess: Color,
    val warning: Color,
    val onWarning: Color,
    val info: Color,
    val onInfo: Color
)

// Light theme custom colors
val LightCustomColors = CustomColors(
    success = Color(0xFF4CAF50),
    onSuccess = Color(0xFFFFFFFF),
    warning = Color(0xFFFF9800),
    onWarning = Color(0xFF000000),
    info = Color(0xFF2196F3),
    onInfo = Color(0xFFFFFFFF)
)

// Dark theme custom colors
val DarkCustomColors = CustomColors(
    success = Color(0xFF81C784),
    onSuccess = Color(0xFF000000),
    warning = Color(0xFFFFB74D),
    onWarning = Color(0xFF000000),
    info = Color(0xFF64B5F6),
    onInfo = Color(0xFF000000)
)

// Create a CompositionLocal
val LocalCustomColors = staticCompositionLocalOf { LightCustomColors }

// Extend your theme
@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val customColors = if (darkTheme) DarkCustomColors else LightCustomColors
    
    // ... existing color scheme code ...
    
    CompositionLocalProvider(LocalCustomColors provides customColors) {
        MaterialTheme(
            colorScheme = colorScheme,
            typography = AppTypography,
            shapes = AppShapes,
            content = content
        )
    }
}

// Access custom colors
@Composable
fun CustomThemedComponent() {
    val customColors = LocalCustomColors.current
    
    Button(
        onClick = { },
        colors = ButtonDefaults.buttonColors(
            containerColor = customColors.success
        )
    ) {
        Text("Success Action", color = customColors.onSuccess)
    }
}

This technique lets you add semantic colors like success, warning, and info to your Custom App Theme in Jetpack Compose.

Best Practices for Theme Consistency

Here are proven strategies to keep your theme consistent as your app grows:

1. Never Hardcode Colors

Bad:

Kotlin
Text(
    text = "Hello",
    color = Color(0xFF6750A4) // Hardcoded!
)

Good:

Kotlin
Text(
    text = "Hello",
    color = MaterialTheme.colorScheme.primary
)

2. Create Reusable Components

Instead of repeating styling code, create themed components:

Kotlin
@Composable
fun AppButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    isPrimary: Boolean = true
) {
    Button(
        onClick = onClick,
        modifier = modifier,
        colors = ButtonDefaults.buttonColors(
            containerColor = if (isPrimary) 
                MaterialTheme.colorScheme.primary 
            else 
                MaterialTheme.colorScheme.secondary
        ),
        shape = MaterialTheme.shapes.medium
    ) {
        Text(
            text = text,
            style = MaterialTheme.typography.labelLarge
        )
    }
}

3. Use Semantic Naming

Choose color roles based on meaning, not appearance:

Kotlin
// Bad naming
val BlueColor = Color(0xFF2196F3)

// Good naming
val LinkColor = MaterialTheme.colorScheme.primary
val SuccessColor = customColors.success

4. Test Both Themes

Always preview your screens in both light and dark modes:

Kotlin
@Preview(name = "Light Mode", showBackground = true)
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun ScreenPreview() {
    AppTheme {
        YourScreen()
    }
}

5. Keep Typography Consistent

Use the defined typography scale instead of creating custom text styles on the fly:

Kotlin
// Avoid this
Text(
    text = "Title",
    fontSize = 24.sp,
    fontWeight = FontWeight.Bold
)

// Do this
Text(
    text = "Title",
    style = MaterialTheme.typography.headlineSmall
)

Debugging Theme Issues

Sometimes things don’t look right. Here’s how to troubleshoot:

Preview Your Theme Values

Create a debug screen to visualize your theme:

Kotlin
@Composable
fun ThemeDebugScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
            .verticalScroll(rememberScrollState())
    ) {
        Text("Color Scheme", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(8.dp))
        
        ColorBox("Primary", MaterialTheme.colorScheme.primary)
        ColorBox("Secondary", MaterialTheme.colorScheme.secondary)
        ColorBox("Tertiary", MaterialTheme.colorScheme.tertiary)
        // ... add more colors
        
        Spacer(modifier = Modifier.height(16.dp))
        Text("Typography", style = MaterialTheme.typography.headlineMedium)
        
        Text("Display Large", style = MaterialTheme.typography.displayLarge)
        Text("Headline Medium", style = MaterialTheme.typography.headlineMedium)
        Text("Body Large", style = MaterialTheme.typography.bodyLarge)
        // ... add more styles
    }
}

@Composable
fun ColorBox(name: String, color: Color) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp),
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        Text(name)
        Box(
            modifier = Modifier
                .size(40.dp)
                .background(color, MaterialTheme.shapes.small)
        )
    }
}

This screen helps you visually verify all your theme values at a glance.

Supporting Dynamic Color (Material You)

Material You allows users to personalize their experience. Here’s how to give users control:

Kotlin
// In your ViewModel or state management
class ThemeViewModel : ViewModel() {
    private val _useDynamicColor = MutableStateFlow(true)
    val useDynamicColor: StateFlow<Boolean> = _useDynamicColor.asStateFlow()
    
    fun toggleDynamicColor() {
        _useDynamicColor.value = !_useDynamicColor.value
    }
}

// In your settings screen
@Composable
fun SettingsScreen(viewModel: ThemeViewModel = viewModel()) {
    val useDynamicColor by viewModel.useDynamicColor.collectAsState()
    
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text("Use Material You colors")
        Switch(
            checked = useDynamicColor,
            onCheckedChange = { viewModel.toggleDynamicColor() }
        )
    }
}

Users on Android 12+ can then choose between your custom colors and colors that match their wallpaper.

Common Mistakes to Avoid

Mistake 1: Mixing Hardcoded and Theme Values

Don’t mix approaches in the same project. Pick theme values and stick with them:

Kotlin
// Inconsistent - Don't do this
Column {
    Text(text = "Title", color = MaterialTheme.colorScheme.primary)
    Text(text = "Subtitle", color = Color.Gray) // Hardcoded!
}

// Consistent - Do this
Column {
    Text(text = "Title", color = MaterialTheme.colorScheme.primary)
    Text(text = "Subtitle", color = MaterialTheme.colorScheme.onSurfaceVariant)
}

Mistake 2: Ignoring Accessibility

Always ensure sufficient contrast between text and backgrounds:

Kotlin
// Check contrast ratios
val backgroundColor = MaterialTheme.colorScheme.primary
val textColor = MaterialTheme.colorScheme.onPrimary // Guaranteed good contrast

Material 3 handles this automatically if you use the correct color pairs (primary/onPrimary, surface/onSurface, etc.).

Mistake 3: Over-Customizing

Not every component needs custom styling. Sometimes the default Material 3 styling is perfect:

Kotlin
// Often unnecessary
Button(
    onClick = { },
    colors = ButtonDefaults.buttonColors(/* custom colors */),
    shape = RoundedCornerShape(/* custom shape */),
    elevation = ButtonDefaults.elevatedButtonElevation(/* custom elevation */)
) { }

// Usually sufficient
Button(onClick = { }) {
    Text("Click Me")
}

Migration from Material 2 to Material 3

If you’re upgrading an existing app, here’s a quick migration guide:

Color Migration

Material 2 → Material 3:

  • primaryprimary (similar)
  • primaryVariantprimaryContainer
  • secondarysecondary (similar)
  • secondaryVariantsecondaryContainer
  • backgroundbackground (same)
  • surfacesurface (same)

Typography Migration

Material 3 has more granular typography styles. Map your old styles:

  • h1displayLarge
  • h2displayMedium
  • h3displaySmall
  • h4headlineLarge
  • h5headlineMedium
  • h6headlineSmall
  • subtitle1titleLarge
  • subtitle2titleMedium
  • body1bodyLarge
  • body2bodyMedium

Conclusion

Building a Custom App Theme in Jetpack Compose with Material 3 might seem complex at first, but it’s an investment that pays dividends. You get:

  • Consistency — Every screen automatically follows your design system
  • Maintainability — Change one value, update the entire app
  • Flexibility — Support light/dark themes and Material You effortlessly
  • Professionalism — Your app looks polished and well-crafted
  • Accessibility — Built-in contrast and readability standards

The key is starting with a solid foundation: well-defined colors, typography, and shapes. Once your theme is set up, building new screens becomes faster because you’re working with a consistent design language.

Remember, your Custom App Theme in Jetpack Compose isn’t set in stone. As your app evolves and your brand matures, you can refine your theme values. The beauty of this system is that those updates propagate throughout your entire app automatically.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!