Jetpack Compose: Implementing Custom Color Schemes for Enhanced UI

Jetpack Compose is revolutionizing Android UI development, offering a declarative and modern approach to building user interfaces. One of the critical aspects of UI design is theming, and Jetpack Compose provides robust capabilities for customizing color schemes. By defining custom color schemes, developers can ensure consistency and create visually appealing applications tailored to their brand.

What are Color Schemes in Jetpack Compose?

Color schemes in Jetpack Compose define the colors used across your application’s UI components. They provide a systematic way to manage and apply colors consistently, supporting both light and dark themes. Compose’s MaterialTheme uses these color schemes to style UI elements like buttons, text fields, and backgrounds.

Why Customize Color Schemes?

  • Branding: Ensures your application aligns with your brand’s visual identity.
  • Consistency: Provides a unified look and feel across the entire app.
  • Accessibility: Helps create accessible themes that meet the needs of all users, including those with visual impairments.
  • Theming: Supports multiple themes (e.g., light and dark) to provide users with personalized experiences.

How to Implement Custom Color Schemes in Jetpack Compose

To implement custom color schemes, follow these steps:

Step 1: Define Your Custom Colors

First, define your color palette using Color objects in Kotlin:


import androidx.compose.ui.graphics.Color

val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8BC)

val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

val CustomGreen = Color(0xFF3DDC84)
val CustomBlue = Color(0xFF29ABE2)
val CustomOrange = Color(0xFFF7941D)

Step 2: Create a Custom ColorScheme

Next, create a custom ColorScheme using the lightColorScheme or darkColorScheme builder functions. These functions allow you to override the default Material Design colors with your custom colors:


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

val CustomLightColorScheme = lightColorScheme(
    primary = CustomGreen,
    secondary = CustomBlue,
    tertiary = Pink40,
    background = Color.White,
    surface = Color.White,
    onPrimary = Color.White,
    onSecondary = Color.Black,
    onTertiary = Color.White,
    onBackground = Color.Black,
    onSurface = Color.Black,
)

val CustomDarkColorScheme = darkColorScheme(
    primary = CustomGreen,
    secondary = CustomBlue,
    tertiary = Pink80,
    background = Color.Black,
    surface = Color.Black,
    onPrimary = Color.Black,
    onSecondary = Color.White,
    onTertiary = Color.Black,
    onBackground = Color.White,
    onSurface = Color.White,
)

Here’s what each color role represents:

  • primary: The main color used for key components like buttons and active elements.
  • secondary: Used for less prominent UI elements.
  • tertiary: An accent color used for balancing primary and secondary colors.
  • background: The color of the screen background.
  • surface: Used for cards, sheets, and menus.
  • onPrimary: The color applied to content displayed on top of the primary color.
  • onSecondary: The color applied to content displayed on top of the secondary color.
  • onTertiary: The color applied to content displayed on top of the tertiary color.
  • onBackground: The color applied to content displayed on top of the background color.
  • onSurface: The color applied to content displayed on top of the surface color.

Step 3: Apply the Custom Color Scheme to Your App

To apply your custom color scheme, wrap your composable content with the MaterialTheme composable and pass your CustomLightColorScheme or CustomDarkColorScheme to the colorScheme parameter.


import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun CustomThemeExample() {
    MaterialTheme(colorScheme = CustomLightColorScheme) {
        // Your app content here
        Text("Hello, Custom Theme!")
    }
}

@Preview(showBackground = true)
@Composable
fun CustomThemeExamplePreview() {
    CustomThemeExample()
}

Step 4: Implement Dark Mode Support

To support dark mode, you can dynamically switch between CustomLightColorScheme and CustomDarkColorScheme based on the system’s current theme setting.


import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable

@Composable
fun MyApp() {
    val isDarkTheme = isSystemInDarkTheme()
    val colorScheme = if (isDarkTheme) CustomDarkColorScheme else CustomLightColorScheme

    MaterialTheme(colorScheme = colorScheme) {
        // Your app content here
        Text("Hello, Themed App!")
    }
}

Complete Example:


import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

// Define Custom Colors
val CustomGreen = Color(0xFF3DDC84)
val CustomBlue = Color(0xFF29ABE2)
val CustomOrange = Color(0xFFF7941D)

// Define Custom Light Color Scheme
val CustomLightColorScheme = lightColorScheme(
    primary = CustomGreen,
    secondary = CustomBlue,
    tertiary = CustomOrange,
    background = Color.White,
    surface = Color.White,
    onPrimary = Color.White,
    onSecondary = Color.Black,
    onTertiary = Color.Black,
    onBackground = Color.Black,
    onSurface = Color.Black,
)

// Define Custom Dark Color Scheme
val CustomDarkColorScheme = darkColorScheme(
    primary = CustomGreen,
    secondary = CustomBlue,
    tertiary = CustomOrange,
    background = Color.Black,
    surface = Color.Black,
    onPrimary = Color.Black,
    onSecondary = Color.White,
    onTertiary = Color.Black,
    onBackground = Color.White,
    onSurface = Color.White,
)

@Composable
fun ThemedContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = "This is a themed text.")
        Button(onClick = { /* do something */ }) {
            Text(text = "Themed Button")
        }
    }
}


@Composable
fun MyAppTheme() {
    val isDarkTheme = isSystemInDarkTheme()
    val colorScheme = if (isDarkTheme) CustomDarkColorScheme else CustomLightColorScheme

    MaterialTheme(colorScheme = colorScheme) {
        ThemedContent()
    }
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyAppTheme()
}

Advanced Theming Concepts

Dynamic Color

Jetpack Compose also supports dynamic color schemes using the dynamicLightColorScheme and dynamicDarkColorScheme APIs. These schemes generate colors based on the user’s current wallpaper.


import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import android.content.Context
import androidx.compose.runtime.Composable

@Composable
fun DynamicThemeExample(context: Context) {
    val isDarkTheme = isSystemInDarkTheme()
    val colorScheme = if (isDarkTheme) {
        dynamicDarkColorScheme(context)
    } else {
        dynamicLightColorScheme(context)
    }

    MaterialTheme(colorScheme = colorScheme) {
        // Your app content here
        Text("Hello, Dynamic Theme!")
    }
}

Custom Theme Attributes

For more advanced theming, you can define custom theme attributes to manage more than just colors. For example, you might want to manage typography or spacing in your theme.


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

data class CustomTypography(
    val heading: TextStyle = TextStyle(fontSize = 24.sp),
    val body: TextStyle = TextStyle(fontSize = 16.sp)
)

val LocalCustomTypography = staticCompositionLocalOf { CustomTypography() }

@Composable
fun ProvideCustomTypography(typography: CustomTypography, content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalCustomTypography provides typography) {
        content()
    }
}

@Composable
fun ThemedText(text: String) {
    val typography = LocalCustomTypography.current
    Text(text = text, style = typography.body)
}

Conclusion

Custom color schemes are essential for creating visually consistent and appealing applications in Jetpack Compose. By defining and applying custom color schemes, you can align your app with your brand and ensure a cohesive user experience. Furthermore, supporting dark mode and leveraging dynamic color schemes enhances the usability and personalization of your application. Mastering theming in Jetpack Compose allows you to create truly unique and engaging Android experiences.