Open In App

Android Jetpack Compose – Theme Picker Palette

Last Updated : 21 Jun, 2023
Improve
Improve
Like Article
Like
Save
Share
Report

Jetpack Compose is a new UI toolkit from Google used to create native Android UI. It speeds up and simplifies UI development using less code, Kotlin APIs, and powerful tools.

Prerequisites:

  1. Familiar with Kotlin and OOP Concepts as well 
  2. basic understanding of Jetpack Compose
  3. Android Studio Canary Version (Programme was developed in the latest version => Android Studio Giraffe | 2022.3.1 Canary 11)

A sample video is given below to get an idea about what we will do in this article.

Step-by-Step Implementation

Step 1: Create a new Android studio project

To create a new project in Android Studio using Jetpack Compose please refer to:- How to Create a New Project in Android Studio Canary Version with Jetpack Compose.

Step 2: Let’s first review the build.gradle(module level)

Now go to the module-level build.gradle file & check on dependencies. If any of the dependencies are missing add them from the below snippet. Remember to double-check this file that everything is included. If something is missing just add those blocks from the below snippets.

@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
plugins {
    alias(libs.plugins.com.android.application)
    alias(libs.plugins.org.jetbrains.kotlin.android)
}

android {
    namespace = "com.example.jetpackthemepicker"
    compileSdk = 33

    defaultConfig {
        applicationId = "com.example.jetpackthemepicker"
        minSdk = 24
        targetSdk = 33
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary = true
        }
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
    buildFeatures {
        compose = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.3.2"
    }
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
}

dependencies {

    constraints {
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10") {
            because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
        }
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10") {
            because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
        }
    }
    implementation(libs.core.ktx)
    implementation(libs.lifecycle.runtime.ktx)
    implementation(libs.activity.compose)
    implementation(platform(libs.compose.bom))
    implementation(libs.ui)
    implementation(libs.ui.graphics)
    implementation(libs.ui.tooling.preview)
    implementation(libs.material3)
    implementation("androidx.core:core-ktx:1.10.0")
    implementation("com.google.android.material:material:1.8.0")
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.test.ext.junit)
    androidTestImplementation(libs.espresso.core)
    androidTestImplementation(platform(libs.compose.bom))
    androidTestImplementation(libs.ui.test.junit4)
    debugImplementation(libs.ui.tooling)
    debugImplementation(libs.ui.test.manifest)
}

Step 3: Now let’s review the build.gradle(project level)

Remember to double-check this file that everything is included. If something is missing just add those blocks from the below snippets.

// Top-level build file where you can add configuration 
// options common to all sub-projects/modules.
plugins {
    alias(libs.plugins.com.android.application) apply false
    alias(libs.plugins.org.jetbrains.kotlin.android) apply false
}

Step 4: Now let’s review the libs.versions.toml(Version Catalog) and settings.gradle.kts

These files are special files that were introduced in the latest update of the Android version i.e. Android Studio Giraffe | 2022.3.1 Canary 11. A description of what this file holds onto is given below :

  • “libs.versions.toml” is a version catalog file that contains a list of all the dependencies and their corresponding version numbers used in a project. This file is typically used in a Gradle-based project and helps manage the versioning of external libraries.
  • “settings.gradle.kts” is a Gradle settings file that specifies the configuration settings for a Gradle-based project. This file typically contains information about the project structure, such as the location of the source code, the build output directory, and any included modules or plugins. The “.kts” extension indicates that the file is written in Kotlin Script, a scripting language that can be used with Gradle to configure and build projects.

libs.versions.toml

[versions]
com-android-application = "8.1.0-alpha11"
org-jetbrains-kotlin-android = "1.7.20"
core-ktx = "1.9.0"
junit = "4.13.2"
androidx-test-ext-junit = "1.1.5"
espresso-core = "3.5.1"
lifecycle-runtime-ktx = "2.3.1"
activity-compose = "1.5.1"
compose-bom = "2022.10.00"

[libraries]
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
ui = { group = "androidx.compose.ui", name = "ui" }
ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
material3 = { group = "androidx.compose.material3", name = "material3" }

[plugins]
com-android-application = { id = "com.android.application", version.ref = "com-android-application" }
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version = "1.7.20" }

[bundles]

settings.gradle.kts

pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

rootProject.name = "JetpackThemePicker"
include(":app")

Step 5: Adding more colors to colors.xml and adding themes.xml

Add some colors for the theme picker and use them in different customization of themes. Goto Android->res->values->colors.xml and add the following stub of code:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimaryDark">#EB495C</color>
    <color name="colorPrimary">#E16876</color>
    <color name="colorPrimaryVariant">#E4A8AF</color>

    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#FF6200EE</color>
    <color name="purple_700">#FF3700B3</color>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>
    <color name="blue">#0984e3</color>
    <color name="fruit">#6c5ce7</color>
    <color name="pink">#EF5366</color>
    <color name="orange">#e17055</color>
    <color name="pink40">#64EF5366</color>
    <color name="pink50">#80EF5366</color>
</resources>

Now goto Android->res->values->themes.xml and add the following stub of code:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">

    <style name="Theme.JetpackThemePicker" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryVariant">@color/colorPrimaryDark</item>
        <item name="colorOnPrimary">@color/white</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor" >?attr/colorPrimaryVariant</item>
        <!-- Customize your theme here. -->
    </style>

    <style name="Theme.JetpackThemePicker.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>

    <style name="Theme.JetpackThemePicker.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />

    <style name="Theme.JetpackThemePicker.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

Step 6: Now let’s define the UI flow and navigation flow for the screens

This is the MainActivity class, which extends ComponentActivity.

  • The onCreate() method is overridden to set the content of the activity using the Jetpack Compose UI.
  • Inside the setContent block, two variables are defined using the remember function, which is used for state management in Compose. 
    • The first variable, systemUiController, is an instance of the SystemUiController class, which is used to control the system UI (status bar and navigation bar) of the device. The second variable, appTheme, is a mutable state variable of type AppThemeState.
  • A BaseView composable is used to set the layout of the activity. 
    • It takes the appTheme and systemUiController variables as parameters and sets the content of the activity to MainScreen() composable.
  • The MainScreen() composable is defined as a Column composable with a TopAppBar and a LazyColumn
    • The TopAppBar has a title set to “Jetpack theme picker”. 
    • The LazyColumn is used to display a list of components returned by the getComponents() function. 
    • We are using this setup mainly because if in the future you need to add more components to the app you can easily add it to the getComponent() without changing much of the code.
    • In the getComponent() function the Component has two parameters named as componentName and className
  • The ButtonComponent() composable is defined to create a button for each item in the list. It takes two parameters, buttonText, and className
    • The buttonText parameter is the text to be displayed on the button, and className is the activity class that should be launched when the button is clicked.
  • The getComponents() function returns a list of Component objects, which contain the name and class of each activity to be displayed in the list. In this case, it returns only one Component object for a ThemeActivity.

Kotlin




// Replace the package name with yours
package com.example.jetpackthemepicker 
  
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.jetpackthemepicker.model.Component
import com.example.jetpackthemepicker.ui.theme.AppThemeState
import com.example.jetpackthemepicker.ui.theme.BaseView
import com.example.jetpackthemepicker.ui.theme.SystemUiController
import com.example.jetpackthemepicker.ui.theme.ThemeActivity
  
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val systemUiController = remember { SystemUiController(window) }
            val appTheme = remember { mutableStateOf(AppThemeState()) }
            BaseView(appTheme.value, systemUiController) {
                MainScreen()
            }
        }
    }
  
    @OptIn(ExperimentalMaterial3Api::class)
    @Preview
    @Composable
    fun MainScreen() {
        Column(Modifier.background(MaterialTheme.colorScheme.background)) {
            TopAppBar(
                title = { Text(text = "Jetpack theme picker", color = Color.Black) },
            )
            LazyColumn {
                items(getComponents()) {
                    ButtonComponent(it.componentName, it.className)
                }
            }
        }
    }
  
    @Composable
    fun ButtonComponent(buttonText: String, className: Class<*>) {
        val context = LocalContext.current
        OutlinedButton(
            onClick = {
                context.startActivity(Intent(applicationContext, className))
            },
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth(),
            shape = RoundedCornerShape(8.dp),
            colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary.copy(0.04f)),
            border = BorderStroke(2.dp, MaterialTheme.colorScheme.primary.copy(0.5f)),
        ) {
            Text(
                modifier = Modifier
                    .padding(8.dp),
                text = buttonText,
                fontSize = 16.sp,
                textAlign = TextAlign.Center,
                color = MaterialTheme.colorScheme.primary
            )
        }
    }
  
    private fun getComponents() = listOf(
        Component("Theme", ThemeActivity::class.java),
    )
  
}


Step 7: Creating a model data class of the Component

Create a new package as a model and create a Kotlin dataclass file in it named Component. This Component data class will have two properties: componentName and className.

  • The componentName property is a String that stores the name of the component.
  • The className property is a Class<*> type that stores the class object of the component. Class<*> is a special Kotlin type that represents a class, regardless of its type parameters. It is a way of referring to the class without actually creating an instance of it.

Kotlin




package com.example.jetpackthemepicker.model
  
data class Component(
    var componentName: String,
    var className: Class<*>
)


Step 8: Create a class that can be used as a state variable for managing the theme of the app (AppThemeState)

Goto Android -> Java -> com -> example -> ui -> theme and create a data class file named as AppThemeState. This class will have two properties:

  1. darkTheme: a boolean variable that indicates whether the app is currently using a dark theme. The default value is false.
  2. palette: a ColorPalette enum variable that represents the color scheme of the app. The default value is ColorPalette.PINK.

The data keyword used before the class definition automatically generates some methods such as equals(), hashCode(), toString(), and copy() for the class based on the properties defined in it.

This class can be used as a state variable for managing the theme of the app in a Jetpack Compose UI. When the darkTheme or palette properties of this class change, the UI that is using this state variable will recompose itself with the new values.

Kotlin




package com.example.jetpackthemepicker.ui.theme
  
/**
 * used as a state variable for managing the theme of the app
 * */
data class AppThemeState(
    var darkTheme: Boolean = false,
    var pallet: ColorPalette = ColorPalette.PINK
)


Step 9: Define colors, shapes, and typography for the theme

We will define some colors, shapes, and typography for the theme palette to choose from. You can also tinker with the values according to your preferences. Goto Android=>app=>java=>com=>example=>packagename(Here jetpackthemepicker)=>ui=>Color.kt and paste this stub of code

Color.kt

Kotlin




package com.example.jetpackthemepicker.ui.theme
  
import androidx.compose.ui.graphics.Color
  
val red200 = Color(0XFFEF9A9A)
val red500 = Color(0XFFF44336)
val red700 = Color(0XFFD32F2F)
val pink200 = Color(0XFFF27584)
val pink500 = Color(0XFFEF5366)
  
val pink700 = Color(0XFFD74A5B)
val purple200 = Color(0XFFCE93D8)
val purple500 = Color(0XFF9C27B0)
val purple700 = Color(0XFF7B1FA2)
  
val indigo200 = Color(0XFF9FA8DA)
val indigo500 = Color(0XFF3F51B5)
val indigo700 = Color(0XFF303f9f)
val blue200 = Color(0XFF90CAF9)
val blue500 = Color(0xFF2195F2)
val blue700 = Color(0xFF1976D2)
  
val teal200 = Color(0XFF80DEEA)
  
val green200 = Color(0XFFA5D6A7)
val green500 = Color(0XFF4CAF50)
val green700 = Color(0XFF388E3C)
  
val yellow200 = Color(0XFFFFF59D)
val yellow500 = Color(0XFFFFEB3B)
val yellow700 = Color(0XFFFBC02D)
  
val orange200 = Color(0XFFFFCC80)
val orange500 = Color(0XFFFF9800)
val orange700 = Color(0XFFF57C00)
  
val brown200 = Color(0XFFBCAAA4)
val brown500 = Color(0XFF795548)
val brown700 = Color(0XFF5D4037)
val grey200 = Color(0XFFEEEEEE)
val grey500 = Color(0XFF9E9E9E)
val grey700 = Color(0XFF616161)


Goto Android=>app=>java=>com=>example=>packagename(Here jetpackthemepicker)=>ui=>Shape.kt and paste this stub of code

Shape.kt

Kotlin




package com.example.jetpackthemepicker.ui.theme
  
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(4.dp),
    large = RoundedCornerShape(0.dp)
)


Goto Android=>app=>java=>com=>example=>packagename(Here jetpackthemepicker)=>ui=>Type.kt and paste this stub of code

Type.kt

Kotlin




package com.example.jetpackthemepicker.ui.theme
  
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(
    bodyMedium = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    )
)


Step 10: Let us create a class named “SystemUI.kt” that provides methods to change the color of the status and navigation bars

This is a SystemUiController class that provides methods to change the color of the status and navigation bars on an Android device. The SystemUiController class takes a Window object as its constructor parameter, which is used to set the status and navigation bar colors. The class has three methods:

  • setStatusBarColor takes a Color object as input and sets the color of the status bar. It also takes two optional Boolean parameters: darkIcons, which specifies whether the status bar icons should be dark or light, and transformColorForLightContent, which is a lambda that transforms the color if dark icons were requested but are not available.
  • setNavigationBarColor takes a Color object as input and sets the color of the navigation bar. It also takes two optional Boolean parameters: darkIcons, which specifies whether the navigation bar icons should be dark or light, and transformColorForLightContent, which is a lambda that transforms the color if dark icons were requested but are not available.
  • setSystemBarsColor takes a Color object as input and calls setStatusBarColor and setNavigationBarColor with the same color and optional parameters.

We also define a LocalSystemUiController object, which can be used as a Composable function to provide the SystemUiController to the composition. Additionally, we define two private Color values and a lambda that can be used to transform the color for light content.

Kotlin




package com.example.jetpackthemepicker.ui.theme
  
import android.os.Build
import android.view.View
import android.view.Window
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.toArgb
  
/**
 * A helper class for setting the navigation and status bar colors for a [Window], gracefully
 * degrading behavior based upon API level.
 */
class SystemUiController(private val window: Window) {
  
    /**
     * Set the status bar color.
     *
     * @param color The **desired** [Color] to set. This may require modification if running on an
     * API level that only supports white status bar icons.
     * @param darkIcons Whether dark status bar icons would be preferable. Only available on
     * API 23+.
     * @param transformColorForLightContent A lambda which will be invoked to transform [color] if
     * dark icons were requested but are not available. Defaults to applying a black scrim.
     */
    fun setStatusBarColor(
        color: Color,
        darkIcons: Boolean = color.luminance() > 0.5f,
        transformColorForLightContent: (Color) -> Color = BlackScrimmed
    ) {
        val statusBarColor = when {
            darkIcons && Build.VERSION.SDK_INT < 23 -> transformColorForLightContent(color)
            else -> color
        }
        window.statusBarColor = statusBarColor.toArgb()
        if (Build.VERSION.SDK_INT >= 23) {
            @Suppress("DEPRECATION")
            if (darkIcons) {
                window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or
                        View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
            } else {
                window.decorView.systemUiVisibility = window.decorView.systemUiVisibility and
                        View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
            }
        }
    }
  
    /**
     * Set the navigation bar color.
     *
     * @param color The **desired** [Color] to set. This may require modification if running on an
     * API level that only supports white navigation bar icons. Additionally this will be ignored
     * and [Color.Transparent] will be used on API 29+ where gesture navigation is preferred or the
     * system UI automatically applies background protection in other navigation modes.
     * @param darkIcons Whether dark navigation bar icons would be preferable. Only available on
     * API 26+.
     * @param transformColorForLightContent A lambda which will be invoked to transform [color] if
     * dark icons were requested but are not available. Defaults to applying a black scrim.
     */
    fun setNavigationBarColor(
        color: Color,
        darkIcons: Boolean = color.luminance() > 0.5f,
        transformColorForLightContent: (Color) -> Color = BlackScrimmed
    ) {
        val navBarColor = when {
            Build.VERSION.SDK_INT >= 29 -> Color.Transparent // For gesture nav
            darkIcons && Build.VERSION.SDK_INT < 26 -> transformColorForLightContent(color)
            else -> color
        }
        window.navigationBarColor = navBarColor.toArgb()
        if (Build.VERSION.SDK_INT >= 26) {
            @Suppress("DEPRECATION")
            if (darkIcons) {
                window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or
                        View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
            } else {
                window.decorView.systemUiVisibility = window.decorView.systemUiVisibility and
                        View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
            }
        }
    }
  
    /**
     * Set the status and navigation bars to [color].
     *
     * @see setStatusBarColor
     * @see setNavigationBarColor
     */
    fun setSystemBarsColor(
        color: Color,
        darkIcons: Boolean = color.luminance() > 0.5f,
        transformColorForLightContent: (Color) -> Color = BlackScrimmed
    ) {
        setStatusBarColor(color, darkIcons, transformColorForLightContent)
        setNavigationBarColor(color, darkIcons, transformColorForLightContent)
    }
}
  
/**
 * An [androidx.compose.Local] holding the current [SystemUiController] or throws an error if none
 * is [provided][androidx.compose.Providers].
 */
val LocalSystemUiController = staticCompositionLocalOf<SystemUiController> {
    error("No SystemUiController provided")
}
  
private val BlackScrim = Color(0f, 0f, 0f, 0.2f) // 20% opaque black
private val BlackScrimmed: (Color) -> Color = { original ->
    BlackScrim.compositeOver(original)
}


Step 11: For a custom Compose theme let’s declare several predefined color palettes for light and dark modes in themes.kt

Let’s create a custom Compose theme with several predefined color palettes for light and dark modes. In this class, we will declare 10 predefined color palettes for both light and dark modes. The colors are defined using the Color class provided by Compose, and the palettes are created using the darkColorScheme() and lightColorScheme() functions.

The ColorPalette enum class is used to specify which color palette to use. The JetpackTheme() function is the main entry point of the theme and takes in two parameters: darkTheme and colorPalette. The darkTheme parameter specifies whether to use the dark or light mode of the theme based on the system’s current mode. The colorPalette parameter specifies which color palette to use for the theme.

The content parameter is a composable function that represents the content of the screen or component that the theme is applied to. To apply this theme to a Composable function or a screen, you need to wrap the content with the JetpackTheme() function and pass the desired darkTheme and colorPalette parameters.

Kotlin




package com.example.jetpackthemepicker.ui.theme
  
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
  
// dark palettes
private val DarkPinkColorPalette = darkColorScheme(
    primary = pink200,
    secondary = pink500,
    tertiary = teal200,
    background = Color.Black,
    surface = Color.Black,
    onPrimary = Color.Black,
    onTertiary = Color.White,
    onBackground = Color.White,
    onSurface = Color.White,
    error = Color.Red,
)
  
private val DarkGreenColorPalette = darkColorScheme(
    primary = green200,
    secondary = green700,
    tertiary = teal200,
    background = Color.Black,
    surface = Color.Black,
    onPrimary = Color.Black,
    onTertiary = Color.White,
    onBackground = Color.White,
    onSurface = Color.White,
    error = Color.Red,
)
  
private val DarkPurpleColorPalette = darkColorScheme(
    primary = purple200,
    secondary = purple700,
    tertiary = teal200,
    background = Color.Black,
    surface = Color.Black,
    onPrimary = Color.Black,
    onTertiary = Color.White,
    onBackground = Color.White,
    onSurface = Color.White,
    error = Color.Red,
)
  
private val DarkBlueColorPalette = darkColorScheme(
    primary = blue200,
    secondary = blue700,
    tertiary = teal200,
    background = Color.Black,
    surface = Color.Black,
    onPrimary = Color.Black,
    onTertiary = Color.White,
    onBackground = Color.White,
    onSurface = Color.White,
    error = Color.Red,
)
  
private val DarkOrangeColorPalette = darkColorScheme(
    primary = orange200,
    secondary = orange700,
    tertiary = teal200,
    background = Color.Black,
    surface = Color.Black,
    onPrimary = Color.Black,
    onTertiary = Color.White,
    onBackground = Color.White,
    onSurface = Color.White,
    error = Color.Red,
)
  
private val DarkRedColorPalette = darkColorScheme(
    primary = red200,
    secondary = red500,
    tertiary = teal200,
    surface = Color.Black
)
  
private val DarkYellowColorPalette = darkColorScheme(
    primary = yellow200,
    secondary = yellow500,
    tertiary = teal200,
    surface = Color.Black
)
  
private val DarkBrownColorPalette = darkColorScheme(
    primary = brown200,
    secondary = brown500,
    tertiary = teal200,
    surface = Color.Black
)
  
private val DarkGreyColorPalette = darkColorScheme(
    primary = grey200,
    secondary = grey500,
    tertiary = teal200,
    surface = Color.Black
)
  
private val DarkIndigoColorPalette = darkColorScheme(
    primary = indigo200,
    secondary = indigo500,
    tertiary = teal200,
    surface = Color.Black
)
  
// Light pallets
private val LightPinkColorPalette = lightColorScheme(
    primary = pink500,
    secondary = pink700,
    tertiary = teal200,
    background = Color.White,
    surface = Color.White,
    onPrimary = Color.White,
    onTertiary = Color.Black,
    onBackground = Color.Black,
    onSurface = Color.Black
)
  
private val LightGreenColorPalette = lightColorScheme(
    primary = green500,
    secondary = green700,
    tertiary = teal200,
    background = Color.White,
    surface = Color.White,
    onPrimary = Color.White,
    onTertiary = Color.Black,
    onBackground = Color.Black,
    onSurface = Color.Black
)
  
private val LightPurpleColorPalette = lightColorScheme(
    primary = purple500,
    secondary = purple700,
    tertiary = teal200,
    background = Color.White,
    surface = Color.White,
    onPrimary = Color.White,
    onTertiary = Color.Black,
    onBackground = Color.Black,
    onSurface = Color.Black
)
  
private val LightBlueColorPalette = lightColorScheme(
    primary = blue500,
    secondary = blue700,
    tertiary = teal200,
    background = Color.White,
    surface = Color.White,
    onPrimary = Color.White,
    onTertiary = Color.Black,
    onBackground = Color.Black,
    onSurface = Color.Black
)
  
private val LightOrangeColorPalette = lightColorScheme(
    primary = orange500,
    secondary = orange700,
    tertiary = teal200,
    background = Color.White,
    surface = Color.White,
    onPrimary = Color.White,
    onTertiary = Color.Black,
    onBackground = Color.Black,
    onSurface = Color.Black
)
  
private val LightRedColorPalette = lightColorScheme(
    primary = red500,
    secondary = red700,
    tertiary = teal200,
    surface = Color.White
)
  
private val LightYellowColorPalette = lightColorScheme(
    primary = yellow500,
    secondary = yellow700,
    tertiary = teal200,
    surface = Color.White
)
  
private val LightBrownColorPalette = lightColorScheme(
    primary = brown500,
    secondary = brown700,
    tertiary = teal200,
    surface = Color.White
)
  
private val LightGreyColorPalette = lightColorScheme(
    primary = grey500,
    secondary = grey700,
    tertiary = teal200,
    surface = Color.White
)
  
private val LightIndigoColorPalette = lightColorScheme(
    primary = indigo500,
    secondary = indigo700,
    tertiary = teal200,
    surface = Color.White
)
  
enum class ColorPalette {
    PINK, PURPLE, GREEN, ORANGE, BLUE, RED, INDIGO, BROWN, GREY
}
  
@Composable
fun JetpackTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    colorPalette: ColorPalette = ColorPalette.GREEN,
    content: @Composable() () -> Unit
) {
  
    val colors = colorPalette.getMaterialColors(darkTheme)
  
    MaterialTheme(
        colorScheme = colors,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}
  
@Composable
fun BaseView(
    appThemeState: AppThemeState,
    systemUiController: SystemUiController?,
    content: @Composable () -> Unit
) {
  
    val color = appThemeState.pallet.getMaterialColor()
  
    systemUiController?.setStatusBarColor(color = color, darkIcons = appThemeState.darkTheme)
    JetpackTheme(darkTheme = appThemeState.darkTheme, colorPalette = appThemeState.pallet) {
        content()
    }
}
  
fun ColorPalette.getMaterialColors(darkTheme: Boolean): ColorScheme {
    return when (this) {
        ColorPalette.GREEN -> if (darkTheme) DarkGreenColorPalette else LightGreenColorPalette
        ColorPalette.PURPLE -> if (darkTheme) DarkPurpleColorPalette else LightPurpleColorPalette
        ColorPalette.ORANGE -> if (darkTheme) DarkOrangeColorPalette else LightOrangeColorPalette
        ColorPalette.BLUE -> if (darkTheme) DarkBlueColorPalette else LightBlueColorPalette
        ColorPalette.RED -> if (darkTheme) DarkRedColorPalette else LightRedColorPalette
        ColorPalette.PINK -> if (darkTheme) DarkPinkColorPalette else LightPinkColorPalette
        ColorPalette.INDIGO -> if (darkTheme) DarkIndigoColorPalette else LightIndigoColorPalette
        ColorPalette.BROWN -> if (darkTheme) DarkBrownColorPalette else LightBrownColorPalette
        ColorPalette.GREY -> if (darkTheme) DarkGreyColorPalette else LightGreyColorPalette
    }
}
  
fun ColorPalette.getMaterialColor(): Color {
    return when (this) {
        ColorPalette.GREEN -> green700
        ColorPalette.BLUE -> blue700
        ColorPalette.ORANGE -> orange700
        ColorPalette.PURPLE -> purple700
        ColorPalette.RED -> red700
        ColorPalette.PINK -> pink700
        ColorPalette.INDIGO -> indigo700
        ColorPalette.BROWN -> brown700
        ColorPalette.GREY -> grey700
    }
}


Step 12: Now let’s define a new Activity class ThemeActivity which lets the users change the theme of the screen

  • The ThemeActivity class extends the ComponentActivity class which is a base class for activities that use the modern Android view system, Jetpack Compose. It has a lateinit var property called appTheme which is a mutable state variable of the AppThemeState class. 
    • AppThemeState represents the current state of the theme, which includes a boolean flag to indicate whether the theme has been changed and a ColorPalette object to represent the selected color palette.
  • The onCreate function is called when the activity is created. It sets the content view of the activity using the setContent function which takes a composable function as an argument. 
    • In this case, the composable function is ThemeSample() which is defined later in the code. The remember function is used to create an instance of SystemUiController which is used to modify the system UI of the device, and an instance of MutableState for the appTheme variable.
  • The ThemeSample composable function is used to define the UI layout for ThemeActivity. It contains a Column layout, which has a TopAppBar at the top, and another Column layout below it. The second Column layout contains a Text view and a LazyVerticalGrid view.
  • The Column composable is used to create a vertical layout for the UI components in the screen. It has a Modifier that sets the background color of the column to MaterialTheme.colorScheme.background and makes it fill the maximum height of the screen. 
    • Inside the Column, there is a TopAppBar composable that displays a top app bar at the top of the screen. The title parameter is set to a Text composable that displays the text “Theme”. The colors parameter sets the colors of the app bar. The containerColor is set to MaterialTheme.colorScheme.surfaceTint which is a color that represents the surface of the app. Finally, there is a navigationIcon parameter that sets the icon to an arrow back icon.
  • The TopAppBar contains the title of the screen, a navigation icon, and a background color. The navigationIcon is an IconButton with an arrow back icon that takes the user back to the previous screen when clicked.
  • The Text view displays the label “Change theme of this screen: ” in the center and a medium headline font with a surface tint color.
  • The LazyVerticalGrid view is used to display the available color palettes in a grid layout. Each item in the grid is defined by the items parameter, which takes the size of the colorPallete array and an itemContent lambda function that defines the layout of each item.
  • Each item in the colorPallete array is rendered as a Box composable with a size of 100.dp, a rounded corner shape of 10.dp, and a background color defined by the getMaterialColor() extension function. 
    • When an item is clicked, its color palette is set as the new app theme. If the item’s color palette is currently selected as the app theme, a checkmark icon is displayed in the center of the box. If not, then the box is not modified.
  • Finally, the ThemeSample composable function is annotated with the @OptIn annotation to enable experimental APIs used in the function, namely ExperimentalFoundationApi and ExperimentalMaterial3Api. The function is also annotated with the @Preview annotation to enable previewing the UI layout in Android Studio.

Kotlin




package com.example.jetpackthemepicker.ui.theme
  
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
  
/**
 * ThemeActivity -> lets users change the theme of the screen
 *
 * */
  
class ThemeActivity : ComponentActivity() {
  
    lateinit var appTheme: MutableState<AppThemeState>
  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val systemUiController = remember { SystemUiController(window) }
            appTheme = remember { mutableStateOf(AppThemeState()) }
            BaseView(appTheme.value, systemUiController) {
                ThemeSample()
            }
        }
    }
  
    @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
    @Preview
    @Composable
    fun ThemeSample() {
        val spaceHeight = 24.dp
        Column(Modifier.background(MaterialTheme.colorScheme.background).fillMaxHeight()) {
            TopAppBar(
                title = { Text("Theme") },
                colors = TopAppBarDefaults.smallTopAppBarColors(
                    containerColor = MaterialTheme.colorScheme.surfaceTint
                ),
                navigationIcon = {
                    IconButton(onClick = { onBackPressed() }) {
                        Icon(Icons.Filled.ArrowBack, contentDescription = null)
                    }
                }
            )
            Column(Modifier.padding(20.dp)) {
                Text(text = "Change theme of this screen : ", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.surfaceTint)
                Spacer(Modifier.requiredHeight(spaceHeight))
                val colorPallet = ColorPalette.values()
                LazyVerticalGrid(columns = GridCells.Fixed(3)) {
                    items(colorPallet.size, itemContent = {
                        val isSelected = colorPallet.get(it) == appTheme.value.pallet
                        Column(horizontalAlignment = Alignment.CenterHorizontally) {
                            Box(
                                Modifier
                                    .padding(20.dp)
                                    .size(100.dp)
                                    .clip(RoundedCornerShape(10.dp))
                                    .background(
                                        colorPallet
                                            .get(it)
                                            .getMaterialColor()
                                    )
                                    .clickable {
                                        appTheme.value = AppThemeState(false, colorPallet.get(it))
                                    }
                                    .then(
                                        if (isSelected) {
                                            Modifier.border(
                                                BorderStroke(2.dp, MaterialTheme.colorScheme.onSurface),
                                                shape = RoundedCornerShape(10.dp)
                                            )
                                        } else Modifier
                                    )
                            ) {
                                if (isSelected) {
                                    Icon(
                                        Icons.Filled.Check,
                                        contentDescription = "Theme Selected",
                                        modifier = Modifier
                                            .align(Alignment.Center)
                                            .size(40.dp),
                                        tint = Color.White
                                    )
                                }
                            }
                        }
                    })
                }
            }
        }
    }
}


Step 13: Now let’s register this new ThemeActivity as an activity in AndroidManifest.xml

XML




<?xml version="1.0" encoding="utf-8"?>
    xmlns:tools="http://schemas.android.com/tools">
  
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.JetpackThemePicker"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.JetpackThemePicker">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
  
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <!--registering ThemeActivity as activity-->
        <activity
            android:name=".ui.theme.ThemeActivity">
        </activity>
  
    </application>
  
</manifest>


Output:



Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads