Theming in Jetpack Compose

Jetpack Compose is Android’s modern UI toolkit, designed to simplify and accelerate UI development. One of its standout features is its powerful theming capabilities. Theming allows you to define and apply a consistent visual style throughout your app, making it easier to maintain a cohesive look and feel. This blog post delves into theming in Jetpack Compose, explaining how to define custom themes, apply colors, typography, shapes, and more.

What is Theming in Jetpack Compose?

Theming in Jetpack Compose is a way to encapsulate visual design elements—like colors, typography, and shapes—into a single, reusable entity. By creating a custom theme, you ensure consistency and make it easier to update the look and feel of your application. It also promotes better code organization and maintainability.

Why Use Theming?

  • Consistency: Ensures all UI components adhere to a unified visual style.
  • Maintainability: Simplifies updating the app’s look and feel across the entire codebase.
  • Reusability: Themes can be reused across different parts of the app, or even across multiple apps.
  • Customization: Allows easy switching between light and dark themes, or other custom variants.

How to Implement Theming in Jetpack Compose

Theming in Compose involves defining your own Theme composable that sets the overall look and feel for the components it wraps.

Step 1: Set Up Dependencies

Ensure that you have the necessary dependencies added in your build.gradle file:

dependencies {
    implementation("androidx.compose.ui:ui:1.6.1")
    implementation("androidx.compose.material:material:1.6.1")
    implementation("androidx.compose.ui:ui-tooling-preview:1.6.1")
    debugImplementation("androidx.compose.ui:ui-tooling:1.6.1")
}

Step 2: Define Colors

Start by defining a set of colors that your theme will use. You can create a Color.kt file:


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)

These color values can then be used in both light and dark theme color schemes.

Step 3: Create Color Schemes for Light and Dark Themes

Next, create color schemes for light and dark themes using the lightColorScheme and darkColorScheme APIs from androidx.compose.material3:


import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color

private val DarkColorScheme = darkColorScheme(
    primary = Purple80,
    secondary = PurpleGrey80,
    tertiary = Pink80
)

private val LightColorScheme = lightColorScheme(
    primary = Purple40,
    secondary = PurpleGrey40,
    tertiary = Pink40

    /* Other default colors to override
    background = Color(0xFFFFFBFE),
    surface = Color(0xFFFFFBFE),
    onPrimary = Color.White,
    onSecondary = Color.Black,
    onTertiary = Color.Black,
    onBackground = Color(0xFF1C1B1F),
    onSurface = Color(0xFF1C1B1F),
    */
)

Step 4: Define Typography

Define a custom typography using the Typography class. Create a Typography.kt file:


import androidx.compose.material3.Typography
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

// Set of Material typography styles to start with
val Typography = Typography(
    bodyLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    )
    /* Other default text styles to override
    titleLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 22.sp
    ),
    labelSmall = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Medium,
        fontSize = 11.sp
    )
    */
)

You can customize different text styles according to your app’s design specifications.

Step 5: Define Shapes

Define shapes for components like buttons, cards, etc., using the Shapes class. Create a Shape.kt file:


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

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

Define different shapes for small, medium, and large components, as needed.

Step 6: Create a Custom Theme Composable

Create a custom Theme composable to encapsulate the color schemes, typography, and shapes. This will typically be in a file named Theme.kt:


import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView

@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }

        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    
    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}

The MyAppTheme composable encapsulates the theme settings and uses the MaterialTheme to apply these settings.

Step 7: Apply the Theme in Your App

Wrap your composable content with the MyAppTheme composable:


import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.example.myapp.ui.theme.MyAppTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
                // Your app content goes here
                Greeting("Android")
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: androidx.compose.ui.Modifier = androidx.compose.ui.Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    MyAppTheme {
        Greeting("Android")
    }
}

Now, all the composables within MyAppTheme will use the specified colors, typography, and shapes.

Using Theme Attributes in Composables

You can access the current theme’s attributes in your composables using MaterialTheme.

Accessing Colors


import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color

@Composable
fun ThemedText(text: String) {
    Text(
        text = text,
        color = MaterialTheme.colorScheme.primary
    )
}

This uses the primary color defined in the current theme.

Accessing Typography


import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.TextStyle

@Composable
fun ThemedTitle(text: String) {
    Text(
        text = text,
        style = MaterialTheme.typography.headlineLarge
    )
}

This applies the headlineLarge style defined in your custom typography.

Accessing Shapes


import androidx.compose.foundation.shape.CornerBasedShape
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable

@Composable
fun ThemedButton(text: String, onClick: () -> Unit) {
    Button(
        onClick = onClick,
        shape = MaterialTheme.shapes.medium
    ) {
        Text(text = text)
    }
}

This applies the medium shape to the button.

Conclusion

Theming in Jetpack Compose allows you to create a consistent and visually appealing user interface across your Android application. By defining custom colors, typography, and shapes, you can easily maintain and update the look and feel of your app. Jetpack Compose’s theming capabilities help streamline UI development and improve code organization.