Creating a Dark Theme in Jetpack Compose: Best Practices

Dark themes have become an essential part of modern app design, offering aesthetic appeal, better accessibility, and reduced eye strain for users in low-light environments. With Jetpack Compose, Android’s modern declarative UI toolkit, implementing a dark theme is straightforward yet highly customizable. In this blog post, we will explore best practices for creating a robust, maintainable, and visually appealing dark theme in Jetpack Compose.

Why Implement a Dark Theme?

Before diving into the technical details, let’s briefly discuss why dark themes are important for modern apps:

  1. User Preference: Many users prefer dark themes for their sleek appearance and reduced brightness.

  2. Accessibility: Dark themes can help reduce eye strain and improve readability in low-light conditions.

  3. Battery Efficiency: On OLED and AMOLED screens, dark themes save battery life by reducing the need for pixel illumination.

  4. Design Consistency: Offering both light and dark themes ensures a cohesive experience across devices and user preferences.

Setting Up Theme Colors

In Jetpack Compose, themes are defined using Color objects and MaterialTheme. To support dark themes, you need two sets of color palettes: one for light mode and another for dark mode.

Step 1: Define Your Color Palettes

Create a dedicated Kotlin file (e.g., Theme.kt) to define your colors.

import androidx.compose.ui.graphics.Color

// Light theme colors
val LightPrimary = Color(0xFF6200EE)
val LightOnPrimary = Color(0xFFFFFFFF)
val LightBackground = Color(0xFFFFFFFF)
val LightOnBackground = Color(0xFF000000)

// Dark theme colors
val DarkPrimary = Color(0xFFBB86FC)
val DarkOnPrimary = Color(0xFF000000)
val DarkBackground = Color(0xFF121212)
val DarkOnBackground = Color(0xFFFFFFFF)

Step 2: Create Color Schemes

Use ColorScheme to organize your light and dark palettes:

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

val LightColorScheme = lightColorScheme(
    primary = LightPrimary,
    onPrimary = LightOnPrimary,
    background = LightBackground,
    onBackground = LightOnBackground
)

val DarkColorScheme = darkColorScheme(
    primary = DarkPrimary,
    onPrimary = DarkOnPrimary,
    background = DarkBackground,
    onBackground = DarkOnBackground
)

Setting Up the Theme

Step 3: Create a Custom Theme Function

Compose applications use a central Theme function to manage colors, typography, and shapes. Modify this function to support both light and dark themes based on user preferences.

import androidx.compose.runtime.Composable
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography

@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme

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

Step 4: Detect System Theme

Jetpack Compose provides isSystemInDarkTheme(), a handy utility to detect the system’s dark mode setting. Use it as the default parameter in your theme function to automatically adapt to the user’s system preference.

Applying the Theme

Wrap your app’s content with the custom theme function:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
                // Your app’s UI goes here
                MainScreen()
            }
        }
    }
}

Best Practices for a Dark Theme

Creating a dark theme involves more than just inverting colors. Follow these best practices to ensure a great user experience:

1. Maintain Contrast and Legibility

2. Use Neutral Backgrounds

  • Avoid pure black (#000000) as it can strain the eyes. Instead, use shades of dark gray (e.g., #121212) for better readability.

3. Emphasize Key Elements

  • Use accent colors sparingly to highlight important components.

4. Adapt Images and Icons

  • Provide dark-themed assets, such as icons and illustrations, to ensure they remain visible.

  • Use Painter to tint icons dynamically based on the theme.

5. Consider System Animations

  • Smooth transitions between light and dark themes improve the user experience. Use updateTransition for animated theme changes.

6. Test Across Devices

  • Test your app’s dark theme on various devices and screen sizes to ensure consistency.

Advanced Customizations

Dynamic Colors with Material You

If your app targets Android 12 (API level 31) or higher, leverage dynamic colors with Material You. Dynamic colors adapt your app’s theme to the user’s wallpaper.

import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme

@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = if (dynamicColor) {
        if (darkTheme) dynamicDarkColorScheme(LocalContext.current)
        else dynamicLightColorScheme(LocalContext.current)
    } else {
        if (darkTheme) DarkColorScheme else LightColorScheme
    }

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

Per-Component Theming

Compose allows you to customize themes at a component level. For example, apply a dark mode color scheme only to a specific button:

Button(
    onClick = { /* TODO */ },
    colors = ButtonDefaults.buttonColors(
        backgroundColor = DarkPrimary,
        contentColor = DarkOnPrimary
    )
) {
    Text("Dark Theme Button")
}

Conclusion

Implementing a dark theme in Jetpack Compose is a rewarding process that enhances your app’s usability and appeal. By following these best practices and leveraging the flexibility of Compose, you can create a polished and user-friendly dark theme tailored to your audience. Whether you’re building a small app or a large-scale application, Compose’s declarative paradigm makes it easier than ever to deliver a modern, responsive, and accessible UI.

Happy coding!