Manage Dark Theme in Jetpack-Compose

Easily Manage your themes from your settings page

Akshay Sharma
4 min readOct 27, 2021
Photo by Jasper Graetsch on Unsplash

With the new Jetpack Compose it’s now a lot easier to maintain Themes, here we are going to see how we can change themes for the app from an in-app configuration.
We are aiming to create this type of configuration where the user can keep the theme as auto as well as manually change it accordingly.

Let’s start by creating a new Android Project with Jetpack compose then add the Datastore dependency where we will be saving the theme configuration.

implementation("androidx.datastore:datastore-preferences:1.0.0-beta01")

Now at the Main Activity add a root component block

setContent {
RootComponent(window)
}

where RootComponent can be defined as

@Composable
fun RootComponent(window: Window) {

val isDark = isSystemInDarkThemeCustom()
TheNewYorkTimesAppTheme(isDark) {
window.StatusBarConfig(isDark)
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
// all the screen will be visible here
.
.
.
}
}
}

Here we are using two custom methods to fetch the current theme and second to set up the Statusbar color according to the theme.
1. isSystemInDarkThemeCustom
2. StatusBarConfig

They can be defined as follows

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

val themePreferenceKey = intPreferencesKey("list_theme")
fun Context.isDarkThemeOn() = dataStore.data
.map { preferences ->
// No type safety.
preferences[themePreferenceKey] ?: 0
}
@Composable
fun isSystemInDarkThemeCustom(): Boolean {

val context = LocalContext.current
val prefs = runBlocking { context.dataStore.data.first() }
return when (context.isDarkThemeOn().collectAsState(initial = prefs[themePreferenceKey] ?: 0).value) {
2 -> true
1 -> false
else -> context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}
}

@Composable
fun Window.StatusBarConfig(darkTheme: Boolean) {
WindowInsetsControllerCompat(this, this.decorView).isAppearanceLightStatusBars =
!darkTheme
this.statusBarColor = MaterialTheme.colors.primary.toArgb()
}

In method isSystemInDarkThemeCustom we are retrieving a boolean after checking the datastore value, if the datastore value is empty then we are setting up a default value 0 which leads to checking and returning the current UI_MODE_NIGHT status.

Now lets proceed to create our settings screen where we can change these values.
First lets add the Flow reference from the datastore preference in our settings composable.

val theme = context.isDarkThemeOn().collectAsState(initial = 0)

Here isDarkThemeOn is providing a int value which we have to modify according to user input.

Settings Composable can be defined as follows.

Column(Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.width(600.dp)
.fillMaxWidth()
.align(Alignment.CenterHorizontally)
) {
Row(
modifier = Modifier
.padding(16.dp, 16.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,

) {

Column(modifier = Modifier.weight(1f, true)) {
Text(
"Theme",
style = MaterialTheme.typography.h6,
maxLines = 2,
textAlign = TextAlign.Start,
overflow = TextOverflow.Ellipsis,
)
Text(
stringResource(
id =
when (theme.value) {
0 -> R.string.default_theme
else -> R.string.custom_theme
}
),
style = MaterialTheme.typography.overline,
textAlign = TextAlign.Start,
)
}
Switch(
colors = SwitchDefaults.colors(uncheckedThumbColor = MaterialTheme.colors.secondaryVariant),
onCheckedChange = {
coroutineScope.launch {
context.dataStore.edit { settings ->
settings[themePreferenceKey] = if (it) 1 else 0
}
}
}
, checked = theme.value != 0
)
}
if (theme.value != 0) {

Row(
modifier = Modifier
.padding(16.dp, 16.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,

) {

Column(modifier = Modifier.weight(1f, true)) {
Text(
"Dark Mode",
style = MaterialTheme.typography.h6,
maxLines = 2,
textAlign = TextAlign.Start,
overflow = TextOverflow.Ellipsis,
)
Text(
stringResource(
id =
when (theme.value) {
1 -> R.string.light_mode_selected
2 -> R.string.dark_mode_selected
else -> R.string.default_theme
}
),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Start,
)
}
Switch(
colors = SwitchDefaults.colors(uncheckedThumbColor = MaterialTheme.colors.secondaryVariant),
onCheckedChange = {
coroutineScope.launch {
context.dataStore.edit { settings ->
Log.e("checked ", "boolean $it")
settings[themePreferenceKey] = if (it) 2 else 1
}
}
}
, checked = theme.value == 2
)
}
}

Here we have 2 Rows, initially first one to select whether the theme should be auto or custom, so the second row will appear only when the first switch is selected making it a custom choice
as onCheckedChange listener we are setting editing the datastore.

coroutineScope.launch {
context.dataStore.edit { settings ->
settings[themePreferenceKey] = if (it) 1 else 0
}
}

As it’s selected then 1 which will provide false in isSystemInDarkThemeCustom method and set the Light UI theme, else 0 will be selected which will return a boolean according to the system default theme.

Accordingly in the second row, we are saving values 1 and 2 which leads to setting Dark and Light UI theme respectively.

Congratulations you have added the dark theme functionality in your app.

You can get the full source code from here

Thanks.

--

--