Dark Mode in Compose

RecipeBoxTheme already accepts a darkTheme boolean and picks between RecipeBoxLightColors and RecipeBoxDarkColors — the previous two lessons glossed over where that boolean actually comes from. This lesson covers detecting the system's dark mode setting properly, letting RecipeBox users override it from a Settings screen, and previewing both themes side by side without ever running the app.

Detecting the System Setting: isSystemInDarkTheme()

isSystemInDarkTheme() is a composable function that reads the device's current Configuration and returns whether dark mode is active — and because it's observed as Compose state internally, the whole UI recomposes automatically if the user flips the system setting while RecipeBox is open, no restart required.

Theme/Theme.kt — the default parameter from lesson one

@Composable
fun RecipeBoxTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = if (darkTheme) RecipeBoxDarkColors else RecipeBoxLightColors
    MaterialTheme(
        colorScheme = colorScheme,
        typography = RecipeBoxTypography,
        content = content
    )
}

Because darkTheme has a default value, most call sites simply write RecipeBoxTheme { RecipeApp() } and get correct system-following behavior for free — the parameter only needs to be passed explicitly when something wants to override it.

Letting the User Override the Default

Following the system is the right default, but a Settings screen "Dark Mode" switch that overrides it is a common, welcome feature. That choice needs somewhere to live — RecipeBox holds it as state at the app root, above RecipeBoxTheme, so the override can reach the theme decision:

MainActivity.kt — app root holding the override

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // null = follow the system, true/false = explicit override
            var darkModeOverride by remember { mutableStateOf<Boolean?>(null) }

            RecipeBoxTheme(darkTheme = darkModeOverride ?: isSystemInDarkTheme()) {
                RecipeApp(
                    darkModeOverride = darkModeOverride,
                    onDarkModeOverrideChange = { darkModeOverride = it }
                )
            }
        }
    }
}

SettingsScreen.kt — the Dark Mode switch

@Composable
fun SettingsScreen(
    darkModeOverride: Boolean?,
    onDarkModeOverrideChange: (Boolean?) -> Unit
) {
    val systemDark = isSystemInDarkTheme()
    Row(verticalAlignment = Alignment.CenterVertically) {
        Text("Dark Mode", style = MaterialTheme.typography.bodyLarge)
        Spacer(modifier = Modifier.weight(1f))
        Switch(
            checked = darkModeOverride ?: systemDark,
            onCheckedChange = { isChecked -> onDarkModeOverrideChange(isChecked) }
        )
    }
}

Toggling the switch stores an explicit true/false, which darkModeOverride ?: isSystemInDarkTheme() then prefers over the system value. A "Reset to System Default" action elsewhere in Settings would simply set the override back to null.

Previewing Both Themes with Multiple @Preview

Compose supports stacking multiple @Preview annotations on the same composable, each with its own configuration — this is the fastest way to catch a color that looks fine in light mode but disappears in dark mode, without touching an emulator:

RecipeListItemPreview.kt

@Preview(name = "Light", showBackground = true)
@Preview(
    name = "Dark",
    showBackground = true,
    uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Composable
fun RecipeListItemPreview() {
    RecipeBoxTheme(darkTheme = isSystemInDarkTheme()) {
        RecipeListItem(title = "Tomato Basil Soup", category = "Soup")
    }
}

Android Studio renders both previews side by side in the Preview pane. The uiMode = Configuration.UI_MODE_NIGHT_YES flag is what forces that specific preview into dark mode regardless of the IDE's own theme, so isSystemInDarkTheme() reports correctly for each one independently.

Note: This site doesn't run a live Android Studio instance, so no rendered preview appears on this page — but the annotation combination above is exactly what you'd paste into a real project to see both variants.
Next up: Android 12+ devices can also generate a color scheme from the user's wallpaper — Dynamic Color (Material You) covers wiring that in alongside RecipeBox's static theme as a fallback.