← Back to all posts

12 Xpense

Theming Done Right

Xpense Dev Retrospective


There Used to Be a Toggle

v1.x had an in-app light/dark switch, a setting stored and checked manually, with the app deciding which colors to apply based on that stored value. It worked, but it meant Xpense had its own opinion about theme that could disagree with the rest of the phone. v2.0.0 removed that toggle entirely. The app now just follows whatever the system is set to, like almost every other well-behaved app does.


One Line in BaseActivity Does All the Work

BaseActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    enableEdgeToEdge()
    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
    super.onCreate(savedInstanceState)
}

That's the entirety of the theme decision. No stored preference, no settings screen entry, no branching logic anywhere else in the app. Every one of the nine Activities gets this for free just by extending BaseActivity, since it's set once, here, before super.onCreate even runs.


Where the Actual Colors Live

Android resolves color resources based on which resource folder is active, and that's the entire mechanism, no code has to check "is dark mode on" anywhere in Xpense:

FolderActive when
res/values/colors.xmlSystem is in light mode
res/values-night/colors.xmlSystem is in dark mode

Same color name, @color/background, @color/card, @color/text_primary, defined twice, once per folder. Every layout and style just references the name. Android decides which file to pull the actual hex value from.

background (light)
#F5F5F5
background (dark)
#0F1115
card (light)
#E8EEF9
card (dark)
#1F232C
accent_blue
#2979FF (same in both)
danger
#FF5252 (same in both)

Why some colors stay identical across both files: accent_blue and danger don't change between light and dark. A brand accent and a warning red need to stay recognizable and legible in both modes, they're deliberately defined the same in both files rather than left to accidentally diverge.


Theme.Xpense: One Style, Two Definitions

The style itself is also defined twice, once in res/values/themes.xml and once in res/values-night/themes.xml, both named Theme.Xpense, both extending Android's own DayNight base theme:

res/values/themes.xml (light)
<style name="Theme.Xpense" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <item name="colorSurface">@color/surface_light</item>
    <item name="android:windowBackground">@color/background_light</item>
    <!-- ...dialog and popup styles pointing at the light variants -->
</style>

The parent, Theme.MaterialComponents.DayNight.NoActionBar, is what makes any of this automatic in the first place. DayNight is Android's own built-in mechanism for having a theme resolve differently based on system setting. Xpense's theme just builds on top of that instead of reinventing it.

Even dialogs get their own dark variant. Date pickers, time pickers, and alert dialogs each have a light and dark style, DatePickerThemeLight/DatePickerThemeDark, wired into the two Theme.Xpense definitions. Without that, a native Android dialog could pop up in the wrong theme even while the rest of the app is correctly following the system setting, which is a genuinely easy detail to miss.


Why This Approach Beats a Manual Toggle

Removing the toggle wasn't about doing less work, it's that the manual version had a second problem: it could silently disagree with the phone's actual system setting. Someone with their phone set to dark mode, opening Xpense, could still end up looking at whatever the app's own remembered toggle said, not what they'd actually set system-wide. Following the system removes the disagreement entirely, and it removes a whole settings screen entry and its storage key along with it.


Closing

Theming works because Android's own resource system does the actual work. Next up, the part of any Android project that only matters once and is completely unforgiving if it goes wrong: signing and releasing an APK.

← Previous 11 - The Bug That Almost Wiped Real User Data Next → 13 - Signing, Keystore, and Releasing an APK