Theming & Dark Mode

Theming is what makes every screen of TrailLog feel like it belongs to the same app — consistent colors, consistent text sizes, consistent default look for every button and card, all defined in one place instead of repeated (and inevitably drifting out of sync) across dozens of layout files. This lesson covers how themes.xml and colors.xml work together, and how to support both light and dark system appearance correctly.

themes.xml: The Central Definition of Your App's Look

res/values/themes.xml is where an app's overall style lives — its primary and accent colors, default text appearance, and the default look Material components fall back to when a layout doesn't override them explicitly. Material Components covered why every Material widget requires a Material-derived theme to even render correctly; this lesson is about what actually goes inside that theme.

res/values/themes.xml

<style name="Theme.TrailLog" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <item name="colorPrimary">@color/trail_green</item>
    <item name="colorOnPrimary">@color/trail_on_primary</item>
    <item name="android:windowBackground">@color/trail_background</item>
</style>

Notice every value here is a @color/ reference, never a hardcoded hex value — that's deliberate, and it's the next section's whole point.

Colors Belong in colors.xml, Defined Once

Every color TrailLog uses should be declared exactly once in res/values/colors.xml and referenced everywhere else by name — @color/trail_green — rather than typing #4CAF50 directly into a dozen different layout files. The reason this matters is purely practical: the day the brand's green needs to shift slightly, editing one line in colors.xml updates every screen at once. Hardcoded hex values scattered across layouts mean hunting through the entire project, and near-certainly missing one.

res/values/colors.xml (light mode)

<resources>
    <color name="trail_green">#2E7D32</color>
    <color name="trail_on_primary">#FFFFFF</color>
    <color name="trail_background">#FAFAFA</color>
    <color name="trail_text_primary">#1B1B1B</color>
</resources>

Automatic Day/Night Support with values-night

Android's resource system already has a mechanism built for exactly this: create a second colors.xml under a res/values-night/ folder, using the same color names but dark-appropriate values. Android automatically picks whichever file matches the device's current system setting — no conditional code required anywhere in TrailLog itself.

res/values-night/colors.xml (dark mode)

<resources>
    <color name="trail_green">#66BB6A</color>
    <color name="trail_on_primary">#0A0A0A</color>
    <color name="trail_background">#121212</color>
    <color name="trail_text_primary">#ECECEC</color>
</resources>

Because every layout and theme references @color/trail_background rather than a raw value, this is the entire dark mode implementation for those colors — no Kotlin code needed, and no risk of one screen forgetting to support dark mode while others do.

Letting the User Override the System Setting

Following the system setting is the right default, but many apps also offer their own Light/Dark/ System Default toggle inside Settings. AppCompatDelegate.setDefaultNightMode() is the call that applies that override at runtime:

A settings screen toggle for TrailLog's theme

fun applyThemeChoice(choice: ThemeChoice) {
    val mode = when (choice) {
        ThemeChoice.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
        ThemeChoice.DARK -> AppCompatDelegate.MODE_NIGHT_YES
        ThemeChoice.SYSTEM_DEFAULT -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
    }
    AppCompatDelegate.setDefaultNightMode(mode)
}

enum class ThemeChoice { LIGHT, DARK, SYSTEM_DEFAULT }

Calling this immediately re-themes every open Activity in the app. The one piece it doesn't handle is remembering the user's choice the next time TrailLog launches — for that you'd persist the selected ThemeChoice using Jetpack DataStore, which is exactly what the next lesson, DataStore Preferences, is about.

Previewing Dark Mode Without Running the App

Android Studio's layout preview and the emulator both let you flip between light and dark rendering without changing any code — a small sun/moon toggle above the preview pane switches the configuration, and the emulator's own quick settings include a dark theme switch. Checking both variants for every new screen you add is a cheap habit that catches low-contrast text or an invisible icon well before a user ever reports it.

Note: All XML and Kotlin shown here is correct, real Android code, but this website's own project doesn't run a live Android Studio build or emulator, so no preview toggle actually exists on this page.
Next up: The theme choice above needs somewhere durable to live between app launches. DataStore Preferences covers exactly that.