π Theming
This document provides a detailed explanation of the theming system used in our applications. It covers the theme architecture, components, customization, and usage.
- β¨ Material Design 3: Based on Material Design 3 principles
- π¨ Colors: Custom color schemes with light and dark modes
- π Dark Mode: Full support for light and dark themes
- π Dynamic Color: Support for dynamic color based on system settings
- πͺ Elevations: Consistent elevation system for shadows
- πΌοΈ Images: Images and icons consistent with the theme
- πΆ Shapes: Customizable shape system for components
- π Sizes: Standardized sizes for components
- π Spacings: Consistent spacing system for layout
- π °οΈ Typography: Consistent typography system
π± Theme Architecture
Our theme architecture is designed with several key principles in mind:
- Consistency: Provide a unified look and feel across all applications while allowing for brand-specific customization
- Flexibility: Support different visual identities for different applications (Thunderbird, K-9 Mail) using the same underlying system
- Extensibility: Enable easy addition of new theme components or modification of existing ones
- Maintainability: Centralize theme definitions to simplify updates and changes
- Material Design Compatibility: Build on top of Material Design 3 while extending it with our specific needs
The theming system follows a hierarchical structure:
graph TD subgraph APP_THEMES["App-Specific Themes"] TB_THEME[ThunderbirdTheme2] K9_THEME[K9MailTheme2] end subgraph MAIN["Main Theme"] MAIN_THEME[MainTheme] THEME_CONFIG[ThemeConfig] end subgraph MATERIAL["Material Design 3"] MAT_THEME[MaterialTheme] end TB_THEME --> |uses| MAIN_THEME TB_THEME --> |defines| THEME_CONFIG K9_THEME --> |uses| MAIN_THEME K9_THEME --> |defines| THEME_CONFIG THEME_CONFIG --> |configures| MAIN_THEME MAIN_THEME --> |wraps| MAT_THEME classDef app_theme fill:#d9ffd9,stroke:#000000,color:#000000 classDef main_theme fill:#d9e9ff,stroke:#000000,color:#000000 classDef material fill:#ffe6cc,stroke:#000000,color:#000000 linkStyle default stroke:#999,stroke-width:2px class TB_THEME,K9_THEME app_theme class MAIN_THEME,THEME_CONFIG main_theme class MAT_THEME material
ποΈ Architecture Layers
The theme system consists of three main layers:
- App-Specific Themes Layer: The top layer contains theme implementations for specific applications (ThunderbirdTheme2, K9MailTheme2). Each app theme:
- Defines its own brand colors, logos, and other app-specific visual elements
- Creates a ThemeConfig with these customizations
- Uses the MainTheme as its foundation
- Main Theme Layer: The middle layer provides our extended theming system:
- MainTheme: A composable function that sets up the theme environment
- ThemeConfig: A data class that holds all theme components
- This layer extends Material Design with additional components like custom spacings, elevations, and app-specific colors
- Material Design Layer: The foundation layer is Material Design 3:
- Provides the base theming system (colors, typography, shapes)
- Ensures compatibility with standard Material components
- Our MainTheme wraps MaterialTheme and converts our theme components to Material 3 format when needed
π Data Flow
The theme data flows through the system as follows:
- App-specific themes (ThunderbirdTheme2, K9MailTheme2) define their visual identity through a ThemeConfig
- ThemeConfig is passed to MainTheme, which:
- Selects the appropriate color scheme based on dark/light mode
- Configures system bars (status bar, navigation bar)
- Provides all theme components through CompositionLocal providers
- Converts our theme components to Material 3 format and configures MaterialTheme
- Composables access theme properties through the MainTheme object
- Material components automatically use the Material 3 theme derived from our theme
π Benefits
This architecture provides several benefits:
- Separation of Concerns: Each layer has a specific responsibility
- Code Reuse: Common theme logic is shared between applications
- Customization: Each application can have its own visual identity
- Consistency: All applications share the same theming structure and components
- Extensibility: New theme components can be added without changing the overall architecture
- Compatibility: Works with both our custom components and standard Material components
π§© Theme Components
The theming system consists of several components that work together to provide a comprehensive and consistent visual experience across the application. Each component is responsible for a specific aspect of the UI design.
π§ ThemeConfig
The ThemeConfig
is the central configuration class that holds all theme components. It serves as a container for all theme-related settings and is passed to the MainTheme
composable.
data class ThemeConfig(
val colors: ThemeColorSchemeVariants,
val elevations: ThemeElevations,
val images: ThemeImageVariants,
val shapes: ThemeShapes,
val sizes: ThemeSizes,
val spacings: ThemeSpacings,
val typography: ThemeTypography,
)
The ThemeConfig
allows for:
- Centralized management of all theme components
- Easy switching between light and dark themes
- Simplified theme customization for different applications
- Consistent theme application throughout the app
π¨ ThemeColorScheme
The ThemeColorScheme
defines all colors used in the application. It extends Material Design 3βs color system with additional colors specific to our applications.
data class ThemeColorScheme(
// Material 3 colors
val primary: Color,
val onPrimary: Color,
val primaryContainer: Color,
val onPrimaryContainer: Color,
val secondary: Color,
val onSecondary: Color,
val secondaryContainer: Color,
val onSecondaryContainer: Color,
val tertiary: Color,
val onTertiary: Color,
val tertiaryContainer: Color,
val onTertiaryContainer: Color,
val error: Color,
val onError: Color,
val errorContainer: Color,
val onErrorContainer: Color,
val surfaceDim: Color,
val surface: Color,
val surfaceBright: Color,
val onSurface: Color,
val onSurfaceVariant: Color,
val surfaceContainerLowest: Color,
val surfaceContainerLow: Color,
val surfaceContainer: Color,
val surfaceContainerHigh: Color,
val surfaceContainerHighest: Color,
val inverseSurface: Color,
val inverseOnSurface: Color,
val inversePrimary: Color,
val outline: Color,
val outlineVariant: Color,
val scrim: Color,
// Extra colors
val info: Color,
val onInfo: Color,
val infoContainer: Color,
val onInfoContainer: Color,
val success: Color,
val onSuccess: Color,
val successContainer: Color,
val onSuccessContainer: Color,
val warning: Color,
val onWarning: Color,
val warningContainer: Color,
val onWarningContainer: Color,
)
The color scheme is organized into:
- Base colors: Primary, secondary, and tertiary colors that define the appβs brand identity
- Surface colors: Colors for backgrounds, cards, and other surfaces
- Content colors: Colors for text and icons that appear on various backgrounds (prefixed with βonβ)
- Container colors: Colors for containers like buttons, chips, and other interactive elements
- Utility colors: Colors for specific purposes like errors, outlines, and scrims
Colors are provided in variants for both light and dark themes through the ThemeColorSchemeVariants
class:
data class ThemeColorSchemeVariants(
val light: ThemeColorScheme,
val dark: ThemeColorScheme,
)
πͺ ThemeElevations
The ThemeElevations
component defines standard elevation values used throughout the application to create a consistent sense of depth and hierarchy.
data class ThemeElevations(
val level0: Dp,
val level1: Dp,
val level2: Dp,
val level3: Dp,
val level4: Dp,
val level5: Dp,
)
Typical usage includes:
- level0: For elements that are flush with their background (0dp)
- level1: For subtle elevation like dividers (1dp)
- level2: For cards, buttons in their resting state (3dp)
- level3: For floating action buttons, navigation drawers (6dp)
- level4: For dialogs, bottom sheets (8dp)
- level5: For modal surfaces that should appear prominently (12dp)
πΌοΈ ThemeImages
The ThemeImages
component stores references to app-specific images like logos, icons, and illustrations.
data class ThemeImages(
val logo: Int, // Resource ID
// ... other image resources
)
These images can have light and dark variants through the ThemeImageVariants
class:
data class ThemeImageVariants(
val light: ThemeImages,
val dark: ThemeImages,
)
πΆ ThemeShapes
The ThemeShapes
component defines the corner shapes used for UI elements throughout the application.
data class ThemeShapes(
val extraSmall: CornerBasedShape,
val small: CornerBasedShape,
val medium: CornerBasedShape,
val large: CornerBasedShape,
val extraLarge: CornerBasedShape,
)
These shapes are used for:
- extraSmall: Subtle rounding for elements like text fields (4dp)
- small: Light rounding for cards, buttons (8dp)
- medium: Moderate rounding for floating elements (12dp)
- large: Significant rounding for prominent elements (16dp)
- extraLarge: Very rounded corners for special elements (28dp)
Note: For no rounding (0% corner radius), use RectangleShape
. For completely rounded corners (50% corner radius) for circular elements, use CircleShape
.
The ThemeShapes
can be converted to Material 3 shapes using the toMaterial3Shapes()
method for compatibility with Material components.
π ThemeSizes
The ThemeSizes
component defines standard size values for UI elements to ensure consistent sizing throughout the application.
data class ThemeSizes(
val smaller: Dp,
val small: Dp,
val medium: Dp,
val large: Dp,
val larger: Dp,
val huge: Dp,
val huger: Dp,
val iconSmall: Dp,
val icon: Dp,
val iconLarge: Dp,
val iconAvatar: Dp,
val topBarHeight: Dp,
val bottomBarHeight: Dp,
val bottomBarHeightWithFab: Dp,
)
These sizes are used for:
- General sizes:
smaller
,small
,medium
,large
,larger
,huge
,huger
for component dimensions (width, height), button heights, and other UI element dimensions that need standardization - Icon sizes:
iconSmall
,icon
,iconLarge
for different icon sizes throughout the app - Avatar size:
iconAvatar
for user avatars and profile pictures - Layout sizes:
topBarHeight
,bottomBarHeight
,bottomBarHeightWithFab
for consistent app bar and navigation bar heights
π ThemeSpacings
The ThemeSpacings
component defines standard spacing values used for margins, padding, and gaps between elements.
data class ThemeSpacings(
val zero: Dp,
val quarter: Dp,
val half: Dp,
val default: Dp,
val oneHalf: Dp,
val double: Dp,
val triple: Dp,
val quadruple: Dp,
)
Consistent spacing helps create a rhythmic and harmonious layout:
- zero: No spacing (0dp)
- quarter: Quarter of the default spacing, for very tight layouts (4dp)
- half: Half of the default spacing, for tight layouts (8dp)
- default: The standard spacing unit for general use (16dp)
- oneHalf: One and a half times the default spacing (24dp)
- double: Twice the default spacing, for separating sections (32dp)
- triple: Three times the default spacing, for major layout divisions (48dp)
- quadruple: Four times the default spacing, for maximum separation (64dp)
π °οΈ ThemeTypography
The ThemeTypography
component defines text styles for different types of content throughout the application.
data class ThemeTypography(
// Display styles for large headlines
val displayLarge: TextStyle,
val displayMedium: TextStyle,
val displaySmall: TextStyle,
// Headline styles for section headers
val headlineLarge: TextStyle,
val headlineMedium: TextStyle,
val headlineSmall: TextStyle,
// Title styles for content titles
val titleLarge: TextStyle,
val titleMedium: TextStyle,
val titleSmall: TextStyle,
// Body styles for main content
val bodyLarge: TextStyle,
val bodyMedium: TextStyle,
val bodySmall: TextStyle,
// Label styles for buttons and small text
val labelLarge: TextStyle,
val labelMedium: TextStyle,
val labelSmall: TextStyle,
)
Each TextStyle
includes:
- Font family
- Font weight
- Font size
- Line height
- Letter spacing
- Other typographic attributes
The ThemeTypography
can be converted to Material 3 typography using the toMaterial3Typography()
method for compatibility with Material components.
βοΈ Component Interaction
These theme components work together to create a cohesive design system:
- ThemeConfig aggregates all components and provides them to the
MainTheme
- MainTheme makes components available through
CompositionLocal
providers - Composables access theme components through the
MainTheme
object - Components like
ThemeColorScheme
andThemeShapes
are converted to Material 3 equivalents for use with Material components
This structured approach ensures consistent design application throughout the app while providing flexibility for customization.
π MainTheme
The MainTheme
is the foundation of our theming system:
- Acts as a wrapper around Material Design 3βs
MaterialTheme
- Provides additional theme components beyond what Material Design offers
- Configurable through a
ThemeConfig
parameter - Supports dark mode and dynamic color
- Exposes theme components through the
MainTheme
object
π Theme Provider Implementation and Usage
π οΈ How the Theme Provider Works
The MainTheme
function uses Jetpack Composeβs CompositionLocalProvider
to make theme components available throughout the composition tree:
@Composable
fun MainTheme(
themeConfig: ThemeConfig,
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit,
) {
val themeColorScheme = selectThemeColorScheme(
themeConfig = themeConfig,
darkTheme = darkTheme,
dynamicColor = dynamicColor,
)
val themeImages = selectThemeImages(
themeConfig = themeConfig,
darkTheme = darkTheme,
)
SystemBar(
darkTheme = darkTheme,
colorScheme = themeColorScheme,
)
CompositionLocalProvider(
LocalThemeColorScheme provides themeColorScheme,
LocalThemeElevations provides themeConfig.elevations,
LocalThemeImages provides themeImages,
LocalThemeShapes provides themeConfig.shapes,
LocalThemeSizes provides themeConfig.sizes,
LocalThemeSpacings provides themeConfig.spacings,
LocalThemeTypography provides themeConfig.typography,
) {
MaterialTheme(
colorScheme = themeColorScheme.toMaterial3ColorScheme(),
shapes = themeConfig.shapes.toMaterial3Shapes(),
typography = themeConfig.typography.toMaterial3Typography(),
content = content,
)
}
}
Each theme component is provided through a CompositionLocal
that makes it available to all composables in the composition tree. These CompositionLocal
values are defined using staticCompositionLocalOf
in their respective files:
internal val LocalThemeColorScheme = staticCompositionLocalOf<ThemeColorScheme> {
error("No ThemeColorScheme provided")
}
internal val LocalThemeElevations = staticCompositionLocalOf<ThemeElevations> {
error("No ThemeElevations provided")
}
// ... other LocalTheme* definitions
The MainTheme
object provides properties to access these values from anywhere in the composition tree:
object MainTheme {
val colors: ThemeColorScheme
@Composable
@ReadOnlyComposable
get() = LocalThemeColorScheme.current
val elevations: ThemeElevations
@Composable
@ReadOnlyComposable
get() = LocalThemeElevations.current
// ... other properties
}
This theme provider mechanism ensures that theme components are available throughout the app without having to pass them as parameters to every composable.
π App-Specific Themes
The app-specific themes (ThunderbirdTheme2
and K9MailTheme2
) customize the MainTheme
for each application:
- Provide app-specific color schemes
- Include app-specific assets (like logos)
- Configure theme components through
ThemeConfig
- Use default values for common components (elevations, sizes, spacings, shapes, typography)
ThunderbirdTheme2
@Composable
fun ThunderbirdTheme2(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = false,
content: @Composable () -> Unit,
) {
val images = ThemeImages(
logo = R.drawable.core_ui_theme2_thunderbird_logo,
)
val themeConfig = ThemeConfig(
colors = ThemeColorSchemeVariants(
dark = darkThemeColorScheme,
light = lightThemeColorScheme,
),
elevations = defaultThemeElevations,
images = ThemeImageVariants(
light = images,
dark = images,
),
sizes = defaultThemeSizes,
spacings = defaultThemeSpacings,
shapes = defaultThemeShapes,
typography = defaultTypography,
)
MainTheme(
themeConfig = themeConfig,
darkTheme = darkTheme,
dynamicColor = dynamicColor,
content = content,
)
}
K9MailTheme2
@Composable
fun K9MailTheme2(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = false,
content: @Composable () -> Unit,
) {
val images = ThemeImages(
logo = R.drawable.core_ui_theme2_k9mail_logo,
)
val themeConfig = ThemeConfig(
colors = ThemeColorSchemeVariants(
dark = darkThemeColorScheme,
light = lightThemeColorScheme,
),
elevations = defaultThemeElevations,
images = ThemeImageVariants(
light = images,
dark = images,
),
sizes = defaultThemeSizes,
spacings = defaultThemeSpacings,
shapes = defaultThemeShapes,
typography = defaultTypography,
)
MainTheme(
themeConfig = themeConfig,
darkTheme = darkTheme,
dynamicColor = dynamicColor,
content = content,
)
}
π¨ Using Themes in the App
π§© Applying a Theme
To apply a theme to your UI, wrap your composables with the appropriate theme composable:
// For Thunderbird app
@Composable
fun ThunderbirdApp() {
ThunderbirdTheme2 {
// App content
}
}
// For K9Mail app
@Composable
fun K9MailApp() {
K9MailTheme2 {
// App content
}
}
π Accessing Theme Components
Inside themed content, you can access theme properties through the MainTheme
object:
@Composable
fun ThemedButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Button(
onClick = onClick,
modifier = modifier,
colors = ButtonDefaults.buttonColors(
containerColor = MainTheme.colors.primary,
contentColor = MainTheme.colors.onPrimary,
),
shape = MainTheme.shapes.medium,
) {
Text(
text = text,
style = MainTheme.typography.labelLarge,
)
}
}
π Dark Mode and Dynamic Color
The theming system supports both dark mode and dynamic color:
- Dark Mode: Automatically applies the appropriate color scheme based on the systemβs dark mode setting
- Dynamic Color: Optionally uses the deviceβs wallpaper colors for the theme (Android 12+)
@Composable
fun ThunderbirdTheme2(
darkTheme: Boolean = isSystemInDarkTheme(), // Default to system setting
dynamicColor: Boolean = false, // Disabled by default
content: @Composable () -> Unit,
) {
// ...
}
π§ Customizing Themes
To customize a theme, you can create a new theme composable that wraps MainTheme
with your custom ThemeConfig
:
@Composable
fun CustomTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = false,
content: @Composable () -> Unit,
) {
val images = ThemeImages(
logo = R.drawable.custom_logo,
)
val themeConfig = ThemeConfig(
colors = ThemeColorSchemeVariants(
dark = customDarkThemeColorScheme,
light = customLightThemeColorScheme,
),
elevations = customThemeElevations,
images = ThemeImageVariants(
light = images,
dark = images,
),
sizes = customThemeSizes,
spacings = customThemeSpacings,
shapes = customThemeShapes,
typography = customTypography,
)
MainTheme(
themeConfig = themeConfig,
darkTheme = darkTheme,
dynamicColor = dynamicColor,
content = content,
)
}
π§ͺ Testing with Themes
When writing tests for composables that use theme components, you need to wrap them in a theme:
@Test
fun testThemedButton() {
composeTestRule.setContent {
ThunderbirdTheme2 {
ThemedButton(
text = "Click Me",
onClick = {},
)
}
}
composeTestRule.onNodeWithText("Click Me").assertExists()
}