Jetpack Compose: Master Custom Theming Strategies for Android Apps

Jetpack Compose, Android’s modern UI toolkit, allows developers to create beautiful and dynamic user interfaces with declarative code. Theming plays a crucial role in creating a consistent and branded look across your app. Jetpack Compose offers powerful mechanisms for customizing the theme, ensuring your app aligns perfectly with your brand identity.

Understanding Theming in Jetpack Compose

Theming in Jetpack Compose is primarily managed through the MaterialTheme composable. By customizing the colors, typography, and shapes properties of the MaterialTheme, you can define a unique and consistent visual style for your app.

Why Use Custom Theming?

  • Brand Consistency: Ensures your app’s UI aligns with your brand’s visual identity.
  • User Experience: Creates a consistent and cohesive experience for users.
  • Maintainability: Centralizes styling information, making it easier to update and maintain the app’s look and feel.

How to Implement Custom Theming Strategies in Jetpack Compose

To implement custom theming strategies, follow these steps:

Step 1: Define Custom Theme Attributes

Start by defining custom theme attributes such as colors, typography, and shapes. Jetpack Compose uses Kotlin’s data classes to represent these attributes.

Custom Colors

Create a custom color palette by defining a data class for your app’s colors:


import androidx.compose.ui.graphics.Color

data class AppColors(
    val primary: Color,
    val secondary: Color,
    val background: Color,
    val surface: Color,
    val error: Color,
    val onPrimary: Color,
    val onSecondary: Color,
    val onBackground: Color,
    val onSurface: Color,
    val onError: Color
)

val LightColorPalette = AppColors(
    primary = Color(0xFF6200EE),
    secondary = Color(0xFF03DAC5),
    background = Color.White,
    surface = Color.White,
    error = Color(0xFFB00020),
    onPrimary = Color.White,
    onSecondary = Color.Black,
    onBackground = Color.Black,
    onSurface = Color.Black,
    onError = Color.White
)

val DarkColorPalette = AppColors(
    primary = Color(0xFFBB86FC),
    secondary = Color(0xFF03DAC6),
    background = Color(0xFF121212),
    surface = Color(0xFF121212),
    error = Color(0xFFCF6679),
    onPrimary = Color.Black,
    onSecondary = Color.Black,
    onBackground = Color.White,
    onSurface = Color.White,
    onError = Color.Black
)
Custom Typography

Define your app’s custom typography using the Typography class:


import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.compose.material.Typography

val AppTypography = Typography(
    body1 = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    ),
    h1 = TextStyle(
        fontFamily = FontFamily.Serif,
        fontWeight = FontWeight.Bold,
        fontSize = 32.sp
    ),
    button = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    )
)
Custom Shapes

Define custom shapes using the Shapes class:


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

val AppShapes = Shapes(
    small = RoundedCornerShape(4.dp),
    medium = RoundedCornerShape(8.dp),
    large = RoundedCornerShape(12.dp)
)

Step 2: Create a Custom Theme Composable

Create a composable function that wraps the MaterialTheme to apply your custom attributes.


import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.material.MaterialTheme

private val LocalAppColors = staticCompositionLocalOf { LightColorPalette }

@Composable
fun AppTheme(
    darkTheme: Boolean = false,
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    CompositionLocalProvider(LocalAppColors provides colors) {
        MaterialTheme(
            colors = androidx.compose.material.lightColors(
                primary = colors.primary,
                secondary = colors.secondary,
                background = colors.background,
                surface = colors.surface,
                error = colors.error,
                onPrimary = colors.onPrimary,
                onSecondary = colors.onSecondary,
                onBackground = colors.onBackground,
                onSurface = colors.onSurface,
                onError = colors.onError
            ),
            typography = AppTypography,
            shapes = AppShapes,
            content = content
        )
    }
}

object AppTheme {
    val colors: AppColors
        @Composable
        get() = LocalAppColors.current
}

Key components of this code:

  • Local Composition: Provides your custom colors through CompositionLocalProvider.
  • Dark Theme Support: Applies different color palettes based on the darkTheme parameter.
  • Custom Accessor: The AppTheme object is provided for convenient access of the defined properties

Step 3: Using Custom Theme in Your App

Wrap your composable content with your custom AppTheme and use the custom attributes within your composables.


import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun MyComposable() {
    AppTheme {
        Text(
            text = "Hello Theming!",
            style = AppTheme.typography.h1,
            color = AppTheme.colors.primary
        )
    }
}

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

Step 4: Dynamic Theming

To support dynamic theme changes (e.g., switching between light and dark mode), observe the system’s UI mode and update the theme accordingly.


import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import com.google.accompanist.systemuicontroller.rememberSystemUiController

@Composable
fun SystemBarColor(darkTheme: Boolean) {
    val systemUiController = rememberSystemUiController()
    val barColor = if (darkTheme) DarkColorPalette.background else LightColorPalette.background

    SideEffect {
        systemUiController.setSystemBarsColor(
            color = barColor
        )
    }
}

@Composable
fun MainContent() {
    val darkTheme = isSystemInDarkTheme()

    // Set status bar color
    SystemBarColor(darkTheme = darkTheme)

    AppTheme(darkTheme = darkTheme) {
        Column {
            //Your UI components
        }
    }
}

Here, isSystemInDarkTheme() checks the current system UI mode, and the AppTheme is updated based on this. This gives your application an automatic dark/light mode theme depending on the OS-wide settings.

Conclusion

Custom theming is essential for creating visually appealing and consistent Android apps with Jetpack Compose. By defining custom color palettes, typography, and shapes, and then applying them through a custom theme composable, you can ensure your app aligns with your brand and provides a cohesive user experience. Embracing dynamic theming also allows your app to adapt to system-level UI settings, further enhancing user comfort and engagement.