Android Jetpack Compose – Implement Dark Mode
Jetpack Compose is a new UI toolkit from Google that is used to create native Android UI. It speeds up and simplifies UI development by using less code, Kotlin APIs, and powerful tools.
Prerequisites:
- Familiar with Kotlin and OOP Concepts as well
- Basic understanding of Jetpack Compose
- Android Studio Canary Version
A sample video is given below to get an idea about what we are going to do in this article.
Fortunately, Android 10 and later enable automatically “dark-theming” your app by forcing it to utilize certain darker hues. You may enable this system feature for your app by adding the
android:forceDarkAllowed="true"
to the theme of your choice. When this option is enabled, it will automatically evaluate your light theme and apply a dark version to it.
Now there are two problems with the above approach :
- What if the user wants dark mode for a specific app and not system-wide dark mode?
- How to implement seamless dark mode below android 10 using jetpack compose?
So now we will write a template that will help us enable dark mode on lower versions of android and also if the user wants to enable it for a specific app.
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)
Remember to double-check this file that everything is included. If something is missing just add those blocks from the below snippets.
plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' } android { namespace 'com.example.testing' compileSdk 33 defaultConfig { applicationId "com.example.testing" minSdk 21 targetSdk 33 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary true } } buildTypes { release { minifyEnabled 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 compose_version } packagingOptions { resources { excludes += '/META-INF/{AL2.0,LGPL2.1}' } } } dependencies { implementation 'androidx.core:core-ktx:1.9.0' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' implementation 'androidx.activity:activity-compose:1.6.1' implementation 'androidx.appcompat:appcompat:1.5.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.4' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0-RC" implementation'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0' }
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.
buildscript { ext { kotlin_version = '1.0.1-2' compose_version = '1.1.0-rc01' } // Requirements repositories { google() mavenCentral() } dependencies { classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" // dependency } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id 'com.android.application' version '7.3.0-alpha01' apply false id 'com.android.library' version '7.3.0-alpha01' apply false id 'org.jetbrains.kotlin.android' version '1.6.0' apply false }
Step 4: Now rename MainActivity.kt to DarkModeActivity.kt
We can put the same code to MainActivity.kt as well, but it’s a good idea to create or rename the file to reflect its role. Once you change this we also need to modify the AndroidManifest.xml activity tag to the renamed file since the default is MainActivity. You can refer to the below snippet of AndroidManifest.xml.
XML
<? xml version = "1.0" encoding = "utf-8" ?> < manifest xmlns:android = "http://schemas.android.com/apk/res/android" 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.Testing" tools:targetApi = "32" > < activity android:name = ".DarkModeActivity" android:exported = "true" android:label = "@string/app_name" android:theme = "@style/Theme.AppCompat" > < intent-filter > < action android:name = "android.intent.action.MAIN" /> < category android:name = "android.intent.category.LAUNCHER" /> </ intent-filter > </ activity > </ application > </ manifest > |
Step 5: Importing necessary modules
It’s good practice to import only the necessary modules rather than importing all the modules and using only a few.
Kotlin
import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.Card import androidx.compose.material.DrawerValue import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.ModalDrawer import androidx.compose.material.Surface import androidx.compose.material.Switch import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.Typography import androidx.compose.material.darkColors import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu import androidx.compose.material.lightColors import androidx.compose.material.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign.Companion.Justify import androidx.compose.ui.text.style.TextIndent import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.launch |
Step 6: Implement AppCompatActivity() to class DarkModeActivity
Kotlin
class DarkModeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) // This sets the @Composable function as the root view of the activity. // This is meant to replace the .xml file that we would typically // set using the setContent(R.id.xml_file) method. setContent { // Reacting to state changes is the core behavior of Compose // @remember helps to calculate the value passed to it only // during the first composition. It then // returns the same value for every subsequent composition. // @mutableStateOf as an observable value where updates to // this variable will redraw all // the composable functions. "only the composable // that depend on this will be redraw while the // rest remain unchanged making it more efficient". val enableDarkMode = remember { mutableStateOf( false ) } CustomTheme(enableDarkMode) { ThemedDrawerAppComponent(enableDarkMode) } } } } |
Step 7: Create a composable function for making custom themes
Before we go, there are a few things you should be aware of:
Composable annotations: The @Composable annotation is used to indicate a Composable function. Composable functions can only be invoked from other composable functions. Consider composable functions to be comparable to Lego blocks in that each composable function is constructed up of smaller composable functions.
- lightColors: lightColors is the standard implementation of dark mode. Material Design’s ColorPalette standard
- darkColors: darkColors is the standard implementation of dark mode. Material Design’s ColorPalette standard
Kotlin
@Composable fun CustomTheme(enableDarkMode: MutableState<Boolean>, children: @Composable () () -> Unit) { // In this case, I'm just showing an example of // how you can override any of the values that // are a part of the Palette even though // We are just using the default values itself. val lightColors = lightColors( primary = Color( 0xFF6200EE ), primaryVariant = Color( 0xFF3700B3 ), onPrimary = Color( 0xFFFFFFFF ), secondary = Color( 0xFF03DAC5 ), secondaryVariant = Color( 0xFF0000FF ), onSecondary = Color( 0xFF000000 ), background = Color( 0xFFFFFFFF ), onBackground = Color( 0xFF000000 ), surface = Color( 0xFFFFFFFF ), onSurface = Color( 0xFF000000 ), error = Color( 0xFFB00020 ), onError = Color( 0xFFFFFFFF ) ) // darkColors is a default implementation val darkColors = darkColors() val colors = if (enableDarkMode.value) darkColors else lightColors // Data class holding typography definitions as defined by the // Material typography specification // https://material.io/design/typography/the-type-system.html#type-scale val typography = Typography( body1 = TextStyle( fontFamily = FontFamily.Serif, fontWeight = FontWeight.Normal, fontSize = 20 .sp, textIndent = TextIndent(firstLine = 16 .sp), textAlign = Justify ) ) // A MaterialTheme comprises of colors, typography // and the child composables that are going // to make use of this styling. MaterialTheme(colors = colors, content = children, typography = typography) } |
Step 8: Create a composable function for making app drawers
ModalDrawer: ModalDrawer is a pre-defined composable used to provide access to destinations in the app.
Kotlin
@Composable fun ThemedDrawerAppComponent(enableDarkMode: MutableState<Boolean>) { val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val currentScreen = remember { mutableStateOf(ThemedDrawerAppScreen.Screen1) } val scope = rememberCoroutineScope() // It's a common pattern used across multiple // apps where you see a drawer on the left // of the screen. ModalDrawer( // Drawer state indicates whether // the drawer is open or closed. drawerState = drawerState, gesturesEnabled = drawerState.isOpen, drawerContent = { // drawerContent accepts a composable to // represent the view/layout that will be displayed // when the drawer is open. ThemedDrawerContentComponent( currentScreen = currentScreen, closeDrawer = { scope.launch { drawerState.close() } } ) }, content = { // bodyContent takes a composable to represent // the view/layout to display on the // screen. We select the appropriate screen based // on the value stored in currentScreen. ThemedBodyContentComponent( currentScreen = currentScreen.value, enableDarkMode = enableDarkMode, openDrawer = { scope.launch { drawerState.open() } } ) } ) } |
Step 9: Create a composable function for making ThemedDrawerContentComponent
Kotlin
@Composable fun ThemedDrawerContentComponent( currentScreen: MutableState<ThemedDrawerAppScreen>, closeDrawer: () -> Unit ) { Column(modifier = Modifier.fillMaxHeight()) { // Column with clickable modifier wraps the child composable // and enables it to react to a // click through the onClick callback similar to // the onClick listener that we are accustomed // to on Android. // Here, we just update the currentScreen variable to // hold the appropriate value based on // the row that is clicked i.e if the // first row is clicked, we set the value of // currentScreen to DrawerAppScreen.Screen1, // when second row is clicked we set it to // DrawerAppScreen.Screen2 and so on and so forth. Column( modifier = Modifier.clickable(onClick = { currentScreen.value = ThemedDrawerAppScreen.Screen1 // We also close the drawer when an option // from the drawer is selected. closeDrawer() }), content = { Text(text = ThemedDrawerAppScreen.Screen1.name, modifier = Modifier.padding( 16 .dp)) } ) Column( modifier = Modifier.clickable( onClick = { currentScreen.value = ThemedDrawerAppScreen.Screen2 closeDrawer() } ), content = { Text(text = ThemedDrawerAppScreen.Screen2.name, modifier = Modifier.padding( 16 .dp)) } ) Column( modifier = Modifier.clickable { currentScreen.value = ThemedDrawerAppScreen.Screen3 closeDrawer() }, content = { Text(text = ThemedDrawerAppScreen.Screen3.name, modifier = Modifier.padding( 16 .dp)) } ) } } |
Step 10: Create a composable function for passing the dark mode all over the app
Kotlin
@Composable fun ThemedBodyContentComponent( currentScreen: ThemedDrawerAppScreen, enableDarkMode: MutableState<Boolean>, openDrawer: () -> Unit ) { val onCheckChanged = { _: Boolean -> enableDarkMode.value = !enableDarkMode.value } when (currentScreen) { ThemedDrawerAppScreen.Screen1 -> ThemedScreen1Component( enableDarkMode.value, openDrawer, onCheckChanged ) ThemedDrawerAppScreen.Screen2 -> ThemedScreen2Component( enableDarkMode.value, openDrawer, onCheckChanged ) ThemedDrawerAppScreen.Screen3 -> ThemedScreen3Component( enableDarkMode.value, openDrawer, onCheckChanged ) } } |
Step 11: Create a composable function for making ThemedScreen1,2,3 Components
Before we go, there are a few things you should be aware of:
- Row: The row may be assembled, and it arranges its children in a horizontal line. It is comparable to a LinearLayout with a horizontal orientation.
- Column: The column is composable and places its children in a vertical sequence. It is comparable to a LinearLayout in that it is vertically oriented. Additionally, we add a modifier to the column.
- Modifier: Modifiers serve as examples of the decorator pattern and are used to alter the composable to which they are applied. In this case, we use the Modifier to set the Column up to take up the whole available width and height using the Modifier.fillMaxSize() modifier.
- TopAppBar: TopAppBar is a pre-defined composable that’s placed at the top of the screen.
- Card: Card composable is a predefined composable that represents the card surface as outlined in the Material Design standard. We also apply a modifier and configure it to have rounded corners.
- Surface: It’s typically used to modify the backdrop color, and add elevation, clip, or form to its children’s composable.
Kotlin
@Composable fun ThemedScreen1Component( enableDarkMode: Boolean, openDrawer: () -> Unit, onCheckChanged: (Boolean) -> Unit ) { Column(modifier = Modifier.fillMaxSize()) { // slots for a title, navigation icon, and actions. // Also known as the action bar. TopAppBar( // The Text composable is pre-defined by the // Compose UI library; you can use this // composable to render text on the screen title = { Text( "Screen 1" ) }, navigationIcon = { IconButton(onClick = openDrawer) { Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu" ) } } ) Card( modifier = Modifier.fillMaxWidth(), backgroundColor = MaterialTheme.colors.surface ) { Row(modifier = Modifier.padding( 16 .dp)) { // A pre-defined composable that's capable // of rendering a switch. It honors the Material // Design specification. Switch(checked = enableDarkMode, onCheckedChange = onCheckChanged) Text( text = "Enable Dark Mode" , style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface), modifier = Modifier.padding(start = 8 .dp) ) } } Surface(modifier = Modifier.weight(1f), color = MaterialTheme.colors.surface) { Text( text = "Beginner for Beginner : Beginner learning from Beginner " , style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface), modifier = Modifier.padding( 16 .dp) ) } } } @Composable fun ThemedScreen2Component( enableDarkMode: Boolean, openDrawer: () -> Unit, onCheckChanged: (Boolean) -> Unit ) { Column(modifier = Modifier.fillMaxSize()) { TopAppBar( // The Text composable is pre-defined by the // Compose UI library; you can use this // composable to render text on the screen title = { Text( "Screen 2" ) }, navigationIcon = { IconButton(onClick = openDrawer) { Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu" ) } } ) Card( modifier = Modifier.fillMaxWidth(), backgroundColor = MaterialTheme.colors.surface ) { Row(modifier = Modifier.padding( 16 .dp)) { // A pre-defined composable that's capable of rendering a switch. // It honors the Material // Design specification. Switch(checked = enableDarkMode, onCheckedChange = onCheckChanged) Text( text = "Enable Dark Mode" , style = MaterialTheme.typography.body1, modifier = Modifier.padding(start = 8 .dp) ) } } Surface(modifier = Modifier.weight(1f)) { Text( text = "GFG : w3wiki was founded by Sandeep Jain" , style = MaterialTheme.typography.body1, modifier = Modifier.padding( 16 .dp) ) } } } @Composable fun ThemedScreen3Component( enableDarkMode: Boolean, openDrawer: () -> Unit, onCheckChanged: (Boolean) -> Unit ) { Column(modifier = Modifier.fillMaxSize()) { // It has slots for a title, // navigation icon, and actions. // Also known as the action bar. TopAppBar( // The Text composable is pre-defined by // the Compose UI library; you can use this // composable to render text on the screen title = { Text( "Screen 3" ) }, navigationIcon = { IconButton(onClick = openDrawer) { Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu" ) } } ) Card( modifier = Modifier.fillMaxWidth(), backgroundColor = MaterialTheme.colors.surface ) { Row(modifier = Modifier.padding( 16 .dp)) { // A pre-defined composable that's capable of // rendering a switch. It honors the Material // Design specification. Switch(checked = enableDarkMode, onCheckedChange = onCheckChanged) Text( text = "Enable Dark Mode" , style = MaterialTheme.typography.body1, modifier = Modifier.padding(start = 8 .dp) ) } } Surface(modifier = Modifier.weight(1f)) { Text( text = "Address: A-143, 9th Floor, Sovereign Corporate Tower Sector-136, Noida, Uttar Pradesh - 201305 " , style = MaterialTheme.typography.body1, modifier = Modifier.padding( 16 .dp) ) } } } |
Step 12: Creating an enum class for ModelDrawer screens
Enum is a data type in Kotlin that has a fixed set of constants. We utilize ENUM when a preset set of values reflects a specific type of data. When a variable can only accept one of a small number of potential values, we use enums.
Kotlin
/** * Creating an enum class for ModelDrawer screens */ enum class ThemedDrawerAppScreen { Screen1, Screen2, Screen3 } |
Step 13: If you want to preview your dark theme and light theme look then continue else you can skip it
Significance of @preview and composable annotations :
- Instead of needing to download the app to an Android device or emulator, Android Studio allows you to preview your composable functions within the IDE itself. This is an excellent feature since it allows you to preview every one of your own components—or composable functions—right inside the IDE
- The composable function cannot accept any parameters, which is the fundamental constraint. You may just include your component within another composable function that doesn’t take any arguments and calls your composable function with the necessary parameters
- Also, don’t forget to annotate it with @Preview & @Composable annotations
Kotlin
/** * Significance of @preview and composable annotations : */ @Preview @Composable fun CustomThemeLightPreview() { CustomTheme(enableDarkMode = remember { mutableStateOf( false ) }) { Card { Text( "Preview Text" , modifier = Modifier.padding( 32 .dp)) } } } @Preview @Composable fun CustomThemeDarkPreview() { CustomTheme(enableDarkMode = remember { mutableStateOf( true ) }) { Card { Text( "Preview Text" , modifier = Modifier.padding( 32 .dp)) } } } |
Step 14: Complete code snippet
Kotlin
// Code For enabling dark mode across all Android versions // with a dedicated dark mode toggle(specific to the app) // Please replace the name of // package with your project name package com.example.testing import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.Card import androidx.compose.material.DrawerValue import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.ModalDrawer import androidx.compose.material.Surface import androidx.compose.material.Switch import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.Typography import androidx.compose.material.darkColors import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu import androidx.compose.material.lightColors import androidx.compose.material.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign.Companion.Justify import androidx.compose.ui.text.style.TextIndent import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.launch class DarkModeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) // This sets the @Composable function as the // root view of the activity. // This is meant to replace the .xml file that // we would typically set using the setContent(R.id.xml_file) method. setContent { // Reacting to state changes is the core behavior of Compose // @remember helps to calculate the value passed to it only // during the first composition. It then // returns the same value for every subsequent composition. // @mutableStateOf as an observable value where updates to // this variable will redraw all // the composable functions. "only the composable that // depend on this will be redraw while the // rest remain unchanged making it more efficient". val enableDarkMode = remember { mutableStateOf( false ) } CustomTheme(enableDarkMode) { ThemedDrawerAppComponent(enableDarkMode) } } } } @Composable fun CustomTheme(enableDarkMode: MutableState<Boolean>, children: @Composable () () -> Unit) { // In this case, I'm just showing an example // of how you can override any of the values that // are a part of the Palette even though // we are just using the default values itself. val lightColors = lightColors( primary = Color( 0xFF6200EE ), primaryVariant = Color( 0xFF3700B3 ), onPrimary = Color( 0xFFFFFFFF ), secondary = Color( 0xFF03DAC5 ), secondaryVariant = Color( 0xFF0000FF ), onSecondary = Color( 0xFF000000 ), background = Color( 0xFFFFFFFF ), onBackground = Color( 0xFF000000 ), surface = Color( 0xFFFFFFFF ), onSurface = Color( 0xFF000000 ), error = Color( 0xFFB00020 ), onError = Color( 0xFFFFFFFF ) ) // darkColors is a default implementation val darkColors = darkColors() val colors = if (enableDarkMode.value) darkColors else lightColors // Data class holding typography // definitions as defined by the // Material typography specification // https://material.io/design/typography/the-type-system.html#type-scale val typography = Typography( body1 = TextStyle( fontFamily = FontFamily.Serif, fontWeight = FontWeight.Normal, fontSize = 20 .sp, textIndent = TextIndent(firstLine = 16 .sp), textAlign = Justify ) ) // A MaterialTheme comprises of colors, // typography and the child composables that are going // to make use of this styling. MaterialTheme(colors = colors, content = children, typography = typography) } @Composable fun ThemedDrawerAppComponent(enableDarkMode: MutableState<Boolean>) { val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val currentScreen = remember { mutableStateOf(ThemedDrawerAppScreen.Screen1) } val scope = rememberCoroutineScope() // It's a common pattern used across multiple // apps where you see a drawer on the left // of the screen. ModalDrawer( // Drawer state indicates whether // the drawer is open or closed. drawerState = drawerState, gesturesEnabled = drawerState.isOpen, drawerContent = { // drawerContent accepts a composable to represent // the view/layout that will be displayed // when the drawer is open. ThemedDrawerContentComponent( currentScreen = currentScreen, closeDrawer = { scope.launch { drawerState.close() } } ) }, content = { // bodyContent takes a composable to // represent the view/layout to display on the // screen. We select the appropriate screen // based on the value stored in currentScreen. ThemedBodyContentComponent( currentScreen = currentScreen.value, enableDarkMode = enableDarkMode, openDrawer = { scope.launch { drawerState.open() } } ) } ) } @Composable fun ThemedDrawerContentComponent( currentScreen: MutableState<ThemedDrawerAppScreen>, closeDrawer: () -> Unit ) { Column(modifier = Modifier.fillMaxHeight()) { // Column with clickable modifier wraps the child // composable and enables it to react to a // click through the onClick callback similar to // the onClick listener that we are accustomed // to on Android. // Here, we just update the currentScreen variable // to hold the appropriate value based on // the row that is clicked i.e if the first // row is clicked, we set the value of // currentScreen to DrawerAppScreen.Screen1, // when second row is clicked we set it to // DrawerAppScreen.Screen2 and so on and so forth. Column( modifier = Modifier.clickable(onClick = { currentScreen.value = ThemedDrawerAppScreen.Screen1 // We also close the drawer when an // option from the drawer is selected. closeDrawer() }), content = { Text(text = ThemedDrawerAppScreen.Screen1.name, modifier = Modifier.padding( 16 .dp)) } ) Column( modifier = Modifier.clickable( onClick = { currentScreen.value = ThemedDrawerAppScreen.Screen2 closeDrawer() } ), content = { Text(text = ThemedDrawerAppScreen.Screen2.name, modifier = Modifier.padding( 16 .dp)) } ) Column( modifier = Modifier.clickable { currentScreen.value = ThemedDrawerAppScreen.Screen3 closeDrawer() }, content = { Text(text = ThemedDrawerAppScreen.Screen3.name, modifier = Modifier.padding( 16 .dp)) } ) } } /** * Passed the corresponding screen composable based on the current screen that's active. */ @Composable fun ThemedBodyContentComponent( currentScreen: ThemedDrawerAppScreen, enableDarkMode: MutableState<Boolean>, openDrawer: () -> Unit ) { val onCheckChanged = { _: Boolean -> enableDarkMode.value = !enableDarkMode.value } when (currentScreen) { ThemedDrawerAppScreen.Screen1 -> ThemedScreen1Component( enableDarkMode.value, openDrawer, onCheckChanged ) ThemedDrawerAppScreen.Screen2 -> ThemedScreen2Component( enableDarkMode.value, openDrawer, onCheckChanged ) ThemedDrawerAppScreen.Screen3 -> ThemedScreen3Component( enableDarkMode.value, openDrawer, onCheckChanged ) } } @Composable fun ThemedScreen1Component( enableDarkMode: Boolean, openDrawer: () -> Unit, onCheckChanged: (Boolean) -> Unit ) { Column(modifier = Modifier.fillMaxSize()) { // slots for a title, navigation icon, and actions. // Also known as the action bar. TopAppBar( // The Text composable is pre-defined by the // Compose UI library; you can use this // composable to render text on the screen title = { Text( "Screen 1" ) }, navigationIcon = { IconButton(onClick = openDrawer) { Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu" ) } } ) Card( modifier = Modifier.fillMaxWidth(), backgroundColor = MaterialTheme.colors.surface ) { Row(modifier = Modifier.padding( 16 .dp)) { // A pre-defined composable that's capable of // rendering a switch. It honors the Material // Design specification. Switch(checked = enableDarkMode, onCheckedChange = onCheckChanged) Text( text = "Enable Dark Mode" , style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface), modifier = Modifier.padding(start = 8 .dp) ) } } Surface(modifier = Modifier.weight(1f), color = MaterialTheme.colors.surface) { Text( text = "Beginner for Beginner : Beginner learning from Beginner " , style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface), modifier = Modifier.padding( 16 .dp) ) } } } @Composable fun ThemedScreen2Component( enableDarkMode: Boolean, openDrawer: () -> Unit, onCheckChanged: (Boolean) -> Unit ) { Column(modifier = Modifier.fillMaxSize()) { TopAppBar( // The Text composable is pre-defined by the // Compose UI library; you can use this // composable to render text on the screen title = { Text( "Screen 2" ) }, navigationIcon = { IconButton(onClick = openDrawer) { Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu" ) } } ) Card( modifier = Modifier.fillMaxWidth(), backgroundColor = MaterialTheme.colors.surface ) { Row(modifier = Modifier.padding( 16 .dp)) { // A pre-defined composable that's capable of // rendering a switch. It honors the Material // Design specification. Switch(checked = enableDarkMode, onCheckedChange = onCheckChanged) Text( text = "Enable Dark Mode" , style = MaterialTheme.typography.body1, modifier = Modifier.padding(start = 8 .dp) ) } } Surface(modifier = Modifier.weight(1f)) { Text( text = "GFG : w3wiki was founded by Sandeep Jain" , style = MaterialTheme.typography.body1, modifier = Modifier.padding( 16 .dp) ) } } } @Composable fun ThemedScreen3Component( enableDarkMode: Boolean, openDrawer: () -> Unit, onCheckChanged: (Boolean) -> Unit ) { Column(modifier = Modifier.fillMaxSize()) { // It has slots for a title, navigation icon, and actions. // Also known as the action bar. TopAppBar( // The Text composable is pre-defined by the // Compose UI library; you can use this // composable to render text on the screen title = { Text( "Screen 3" ) }, navigationIcon = { IconButton(onClick = openDrawer) { Icon(imageVector = Icons.Filled.Menu, contentDescription = "Menu" ) } } ) Card( modifier = Modifier.fillMaxWidth(), backgroundColor = MaterialTheme.colors.surface ) { Row(modifier = Modifier.padding( 16 .dp)) { // A pre-defined composable that's capable of // rendering a switch. It honors the Material // Design specification. Switch(checked = enableDarkMode, onCheckedChange = onCheckChanged) Text( text = "Enable Dark Mode" , style = MaterialTheme.typography.body1, modifier = Modifier.padding(start = 8 .dp) ) } } Surface(modifier = Modifier.weight(1f)) { Text( text = "Address: A-143, 9th Floor, Sovereign Corporate Tower Sector-136, Noida, Uttar Pradesh - 201305 " , style = MaterialTheme.typography.body1, modifier = Modifier.padding( 16 .dp) ) } } } /** * Creating an enum class for ModelDrawer screens */ enum class ThemedDrawerAppScreen { Screen1, Screen2, Screen3 } /** * Significance of @preview and composable annotations : */ @Preview @Composable fun CustomThemeLightPreview() { CustomTheme(enableDarkMode = remember { mutableStateOf( false ) }) { Card { Text( "Preview Text" , modifier = Modifier.padding( 32 .dp)) } } } @Preview @Composable fun CustomThemeDarkPreview() { CustomTheme(enableDarkMode = remember { mutableStateOf( true ) }) { Card { Text( "Preview Text" , modifier = Modifier.padding( 32 .dp)) } } } |
If any difficulties are faced check your Gradle files as well android manifest. If the error persists you can refer to this zip file.
Output:
As we can see with the help of jetpack compose the dark mode persists over all three screens as well as it’s app-specific rather than system-wide.