Jetpack Compose has revolutionized how we build UIs in Android, offering a more declarative and intuitive approach. As apps grow more complex, maintaining a consistent look and feel across screens becomes crucial. This is where theming in Jetpack Compose shines, allowing you to define colors, typography, and shapes centrally and apply them across your entire app with ease.
In this article, we’ll explore best practices for defining colors in Jetpack Compose, covering everything from the standard Material color schemes to creating custom palettes and integrating dynamic colors. By the end, you’ll be equipped to create flexible and scalable themes that make your UI not only look great but also consistent and easy to maintain.
MaterialTheme in Jetpack Compose
Compose is built around the Material Design principles. The MaterialTheme is a composable that provides a central place for defining your app’s color scheme, typography, and shapes.
Here’s a basic structure of a theme in Compose:
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
darkColorPalette
} else {
lightColorPalette
}
MaterialTheme(
colors = colors,
typography = AppTypography,
shapes = AppShapes,
content = content
)
}
With this setup, your theme adapts dynamically based on whether the user prefers light or dark mode. Defining your theme this way ensures that all your UI components are consistently styled.
Defining Your Color Palette
In Jetpack Compose, the ColorPalette is where you define the essential colors for your app’s theme, such as primary, secondary, etc. A best practice when defining these colors is to centralize them in the dedicated Color.kt file. Organizing your color definitions this way keeps your codebase clean and makes it easier to manage and update your color palette as your app evolves.
Here’s an example of defining light and dark color palettes:
// Color.kt
private val lightColorPalette = lightColors(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
...
)
private val darkColorPalette = darkColors(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
...
)
In this setup, each color scheme is defined based on whether the app is in light or dark mode, ensuring that the appropriate colors are applied consistently across your UI. By placing these definitions in the Color.kt file, you not only improve the organization of your project but also make future updates more manageable.
Customizing Your Theme with Extended Colors
In some cases, the standard Material color scheme might not be enough for your needs. You may want to add additional colors specific to your brand or design system, like a background variant or other colors that aren’t covered by the default ColorScheme.
Jetpack Compose makes it easy to extend the theme by using a custom implementation.
@Immutable
data class ColorFamily(
val backgroundVariant: Color,
)
@Immutable
data class ExtendedColorScheme(
val extra: ColorFamily = extendedLight.extra,
)
val extendedLight = ExtendedColorScheme(
extra = ColorFamily(
backgroundVariant = Color(0xFFEEEEEE), // Example light variant
),
)
val extendedDark = ExtendedColorScheme(
extra = ColorFamily(
backgroundVariant = Color(0xFF333333), // Example dark variant
),
)
val LocalExColorScheme = staticCompositionLocalOf { ExtendedColorScheme() }
In this example, we create a ColorFamily data class to hold our additional colors. We then build an ExtendedColorScheme that includes this ColorFamily. The staticCompositionLocalOf function is used to make this extended color scheme available throughout the app.
Next, modify your theme composable to include the extended colors:
@Composable
fun MainTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
val extendedColorScheme = if (darkTheme) extendedDark else extendedLight
CompositionLocalProvider(
LocalExColorScheme provides extendedColorScheme
) {
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
}
You can now access your custom colors anywhere in your composables using LocalExColorScheme.current:
@Composable
fun CustomBackgroundBox() {
Box(
modifier = Modifier
.fillMaxSize()
.background(LocalExColorScheme.current.extra.backgroundVariant)
) {
// Your content here
}
}
This approach brings several significant benefits, particularly in terms of scalability, consistency, and flexibility. As your app evolves and your design system grows, the ability to incorporate additional custom colors becomes essential. By leveraging this method, you can easily scale your theme to include any extra colors required without disrupting the existing structure.
Moreover, the use of an extended color scheme ensures that these custom colors are consistently applied across your app. With centralized access through CompositionLocal, you eliminate discrepancies in color usage, resulting in a more cohesive and professional look throughout the entire user interface.
Flexibility is another key advantage of this approach. By integrating CompositionLocal, your theming remains adaptable to various contexts, such as different user settings, UI modes, or device configurations. Whether you need to respond dynamically to changes in theme preferences, screen modes, or specific conditions, this strategy allows for seamless adjustments.
Job Offers
Integrating Dynamic Colors
Starting from Android 12 (API level 31), Jetpack Compose supports dynamic colors. This feature allows your app’s theme to adapt based on the user’s wallpaper and system settings, providing a more personalized experience.
Dynamic colors are automatically generated and applied, making it easy for your app to blend with the overall device theme.
Here’s how you can integrate dynamic colors into your theme:
@Composable
fun MainTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true, // Enable dynamic colors
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
}
CompositionLocalProvider(
LocalExColorScheme provides if (darkTheme) extendedDark else extendedLight
) {
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
}
This integration revolves around the dynamicColor parameter, which determines whether your theme should adapt to the system’s dynamic color settings. When the parameter is enabled, and the device runs on Android 12 or higher (API level 31), Compose uses dynamicDarkColorScheme(context) or dynamicLightColorScheme(context) to automatically generate a color scheme. These functions dynamically adjust the colors based on the user’s wallpaper, creating a theme that harmonizes with both light and dark modes.
To ensure backward compatibility, the implementation first checks the device’s SDK version before applying these dynamic schemes, allowing your app to support older devices gracefully.
Why Use Dynamic Colors?
Dynamic colors in Jetpack Compose offer several advantages that enhance both the visual appeal and usability of your app. They contribute to an enriched user experience by providing seamless integration with the device’s UI, leading to a more immersive and cohesive design. By adopting dynamic colors, your app naturally aligns with the overall system theme, ensuring consistency across other apps and the broader device interface. This alignment contributes to a unified look and feel, which is particularly valuable when aiming for a polished and professional presentation. Additionally, dynamic colors simplify the theming process; instead of manually defining every color, you can leverage system-generated palettes while still retaining the flexibility to override specific elements when needed.
This feature is especially useful for apps that want to align with the latest Android design trends and offer a personalized experience based on user preferences.
A Helpful Tool for Designing Your Theme
Creating a visually cohesive and polished theme can be challenging, especially when balancing colors, typography, and brand identity. A great resource to assist with this is Material Theme Builder, a powerful online tool.
This tool allows you to:
- Experiment with different color schemes and see how they adapt across light and dark modes.
- Fine-tune typography and shape settings.
- Export the generated theme directly for use in your Jetpack Compose project.
The Material Theme Builder simplifies the process of defining your color palette and typography, ensuring that your theme aligns with Material Design guidelines.
By leveraging this tool, you can quickly iterate and fine-tune your theme before integrating it into your Compose project, saving time and ensuring consistency across your app.
Conclusion
Theming in Jetpack Compose offers a powerful and flexible way to manage your app’s visual identity. By extending your color scheme, leveraging CompositionLocal, integrating dynamic colors, and utilizing resources like the Material Theme Builder, you can create a cohesive and scalable design system that’s both easy to maintain and visually appealing.
If you want to see these principles in action, feel free to explore this project where I applied the same techniques discussed in this article: Social Cleaning Control.
With these best practices in place, your Jetpack Compose apps will not only look great but also provide a consistent and enjoyable user experience.
Feel free to share your comments, or if you prefer, you can reach out to me on LinkedIn.
Have a great day!
This article is previously published on proandroiddev.com