State of Compose 2023 results are in! Click here to learn more
Published on

🚀 Change themes programmatically

Authors

Happy Thursday! Alex here 👋

Following last week's tutorial about themes in Jetpack Compose, this week we are going through how to change themes via code.

Let's jump straight to it 🚀

Define light and dark themes

Here is a slim down version of the theme generated when you create a new Jetpack Compose project in Android Studio:

@Composable
fun MyAppTheme(
    isDarkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) {
    val colorScheme = if (isDarkTheme) darkColorScheme() else lightColorScheme()

    MaterialTheme(
        colorScheme = colorScheme,
        // shapes = ...,
        // typography = ...,
        content = content
    )
}

The above composable defines a new theme. It will update the look and feel of the children composable passed to content.

Note the isSystemInDarkTheme() function. As soon as the user changes their system wide theme preference (from light to dark and vise versa), this function will automatically give you a new Boolean representing their preference.

Depending on the value emitted, we are using either the dark or the light color scheme. This is the way Jetpack Compose knows when it is time to update the UI and use dark or light color theme.

Change themes programmatically

Instead of relying on the isSystemInDarkTheme() function to pick a light or dark theme, we can expect the user of the MyAppTheme to provide the isDarkTheme value instead.

var isDarkTheme by remember { mutableStateOf(true) }

MyAppTheme(isDarkTheme = isDarkTheme) {
    Scaffold {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier
                .fillMaxWidth()
                .padding(
                    horizontal = 16.dp,
                    vertical = 10.dp
                ),
            horizontalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            Text("☀️")
            Switch(
                checked = isDarkTheme,
                onCheckedChange = {
                    isDarkTheme = it
                }
            )
            Text("🌘")
        }
    }
}
Switch the theme from light to dark using a Switch

Bonus 🎁: Implement the On/Off/System user setting for themes

You can take this a step further and introduce a third state for the user. You can let them decide if they want the app to be light, dark or follow the system wide theme.

Here is how to implement this:

var appTheme by remember { mutableStateOf(AppTheme.System) }

MyAppTheme(
    isDarkTheme = when (appTheme) {
        AppTheme.Light -> false
        AppTheme.Dark -> true
        AppTheme.System -> isSystemInDarkTheme()
    }
) {
    Scaffold {
        Column {
            LabeledRadioButton(
                label = "☀️",
                selected = appTheme == AppTheme.Light,
                onClick = {
                    appTheme = AppTheme.Light
                }
            )
            LabeledRadioButton(
                label = "🌘",
                selected = appTheme == AppTheme.Dark,
                onClick = {
                    appTheme = AppTheme.Dark
                }
            )
            LabeledRadioButton(
                label = "🤖",
                selected = appTheme == AppTheme.System,
                onClick = {
                    appTheme = AppTheme.System
                }
            )
        }
    }
}

To simplify the code sample I created a composable that combines some Text and a Radio button:

@Composable
fun LabeledRadioButton(
    selected: Boolean,
    onClick: () -> Unit,
    label: String,
) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        RadioButton(
            selected = selected,
            onClick = onClick
        )
        Text(label)
    }
}
Switch light, dark and system setting

PS: You would need to persist the theme selected. You can either use SharedPreferences or Data Store to do this. I personally like DataStore more as it exposes Kotlin Flows instead of callbacks.

That's all for today.

Until the next one,

Alex