Switching on the dark: a practical guide to DayNight mode in Android
Dark interfaces have become a baseline expectation for any polished Android app. Material Design codified the practice several releases ago, but the most consistent path remains the DayNight framework baked into AppCompat and Material Components. With a few resource tweaks, your application can respond to the system setting, a user toggle, or the time of day without doubling the maintenance cost.
Australia's mobile market skews toward OLED-equipped handsets, particularly Samsung Galaxy and Google Pixel lines sold through Telstra and Optus. True black pixels are switched off entirely on OLED, so a properly designed dark surface can shave measurable battery life during long Parramatta-to-city commutes or regional NSW train trips. Apps that ignore the setting feel dated the moment a user flips the system toggle, which most Sydneysiders do at least weekly according to recent ACMA research.
DayNight is a parent theme that resolves to either a light or a dark variant depending on Configuration. It reads the device's uiMode flag, checks your resource qualifiers, and selects the right colors and styles automatically. The heavy lifting happens through the values-night folder, a set of theme overrides, and attribute references that pick the right palette on the fly, with no manual refresh logic required for the system default.
This walkthrough covers the conceptual background, the resource configuration, programmatic switching, and common traps. By the end you should be able to add a credible dark mode to an existing project and verify it on a Pixel emulator while sitting in a Brisbane cafe wondering whether the espresso is worth four dollars.
Understanding DayNight and the theme resolution chain
DayNight is a contract the support library fulfils on your behalf. The base declaration, Theme.AppCompat.DayNight or Theme.MaterialComponents.DayNight, references two child themes. When the activity inflates its layout, the system inspects the night qualifier, locates the matching child theme, and supplies the corresponding color attributes to every view that reads them through ?attr/colorPrimary, ?attr/colorOnPrimary, ?attr/colorSurface, and the rest of the Material attribute set.
Your XML layouts stay untouched because widgets do not hardcode hex values. A MaterialButton that asks for colorPrimary receives a calibrated lavender by day and a desaturated variant by night, without any change to the layout file. The same trick applies to drawables that tint against theme attributes and to vector assets that resolve their fill through ?android:attr/textColorPrimary.
The night qualifier is not tied to the wall clock. Android only flips it when the system uiMode changes, which happens when the user toggles the global setting, when an OEM schedule triggers it, or when your code calls AppCompatDelegate.setDefaultNightMode. A naive implementation that reads the time directly will drift away from the system contract. There is also a subtle WebView interaction: the pageForceDark and forceDark APIs are deprecated, so the supported path is to publish a real values-night set of resources and let the embedded page's CSS respond to the prefers-color-scheme media query.
Wiring up a project for DayNight compatibility
Begin by confirming that build.gradle pulls in Material Components 1.5.0 or higher, which exposes the DayNight parent themes with full dynamic-color support. Your styles.xml should reference Theme.MaterialComponents.DayNight.NoActionBar or the equivalent, depending on whether you want a toolbar.
Inside res/values, declare your color resources as normal but avoid referencing platform names directly. Instead, expose a semantic palette such as brandPrimary, surface, onSurface, and divider. These map cleanly to the Material attribute set, so a CardView using ?attr/colorSurface inherits the correct shade in either mode without further intervention.
In res/values-night, override only the entries that need to change. Most palettes flip the surface from near-white to a desaturated near-black, reduce brandPrimary brightness slightly to keep contrast within WCAG ratios, and shift text to a soft off-white. Keep a single accent and let the surrounding tones do the work, the same restraint you see in local bank apps and the NSW Transport app. It is also worth checking that every custom drawable references theme attributes rather than hardcoded color references: a selector that hardcodes #FFFFFF will look right by day and broken by night, while one using ?attr/colorControlNormal adapts automatically. The same audit applies to notification icons, which should be supplied as monochrome assets so the system can tint them in either mode.
Switching themes at runtime
System default is the right starting point, but many users want a manual override. The cleanest way to expose that is to add a settings screen, persist the choice in SharedPreferences, and apply it through AppCompatDelegate.setDefaultNightMode at app launch. The accepted constants are MODE_NIGHT_NO, MODE_NIGHT_YES, MODE_NIGHT_FOLLOW_SYSTEM, and MODE_NIGHT_AUTO_BATTERY, the last of which was introduced specifically to reduce battery drain on devices where the user has not set a preference.
The change is not instant for activities already on the stack. You need to call delegate.applyDayNight or recreate the affected activity for the new resources to be inflated. A common pattern is to broadcast a configuration change and let each visible activity handle its own recreation, which keeps the lifecycle predictable and avoids the visual stutter a full process restart would introduce. The recreation also gives the system a chance to re-evaluate WebView content, so dark mode propagates into embedded pages without extra glue code. For developers who have wrestled with the same problem on other platforms, the recreate pattern guide is a useful parallel, even though the underlying window APIs differ.
Persistence deserves care. Storing the raw integer constant works, but storing a string and resolving it at read time is friendlier when the constants evolve between library versions, the same defensive habit you see in production code from Australian fintechs like Up and Airwallex. For users on Android 12 and above, the system also exposes dynamic color theming through Monet, which derives your brand palette from the user's wallpaper and lets DayNight decide which of two tonal palettes to expose, the configuration most flagship users in Melbourne and Adelaide already see.
Polishing the result and common pitfalls
Testing is where most teams slip. A Pixel 6 emulator with both light and dark wallpapers is the bare minimum; ideally you also run on a Samsung device, a Nothing Phone, and at least one older API 28 handset to confirm the AppCompat fallback still resolves correctly. The real test, though, is a long bus ride on the 333 from Bondi Junction with the screen at half brightness, where a poorly contrasted dark surface will fatigue the eye within minutes.
Watch for hardcoded alpha values, particularly white overlays used to dim images. A 30 percent white layer over a dark image inverts poorly against a true black surface and produces a muddy grey instead of the subtle darkening the designer intended. The fix is usually to switch the overlay color to a near-black with similar alpha, which behaves consistently in both modes and respects the OLED advantage.
Another trap lives in custom views. If you extend View directly and read getResources().getColor(R.color.something) in onDraw, you bypass the theme system entirely. The supported path is to resolve the color through a TypedArray obtained from the view's context.obtainStyledAttributes, which honours the night qualifier. Finally, document the palette: a short markdown note explaining which colors map to which Material attributes makes future maintenance far easier, especially when a contractor from a Sydney studio picks up the codebase. No worries if the first pass is rough; a dark theme is the kind of polish that rewards iteration over weeks rather than hours.
Theme attributes at a glance
A consistent vocabulary separates a dark theme that feels intentional from one that feels patched together. The Material attribute set exists for exactly this reason, and getting familiar with the names pays off the first time you audit an existing layout. Instead of hunting for hardcoded hex values, you can search for attribute references and follow the chain back to a small set of color resources.
The mapping below summarises the most important Material attributes and the role each one plays in a DayNight implementation. Treat it as a checklist whenever you audit an existing screen or design a new one.
| Attribute | Light default | Dark default | Typical use |
|---|---|---|---|
| colorPrimary | brand accent | brand accent, lower chroma | AppBar, FAB, switches |
| colorOnPrimary | white | white | Iconography on primary surfaces |
| colorPrimaryVariant | deeper accent | deeper accent | Status bar tint |
| colorSecondary | supporting accent | supporting accent | Chips, toggles |
| colorSurface | near-white | near-black | Card and sheet backgrounds |
| colorOnSurface | near-black | off-white | Body text and icons on surfaces |
| colorError | red 500 | red 200 | Validation messages, destructive actions |
| android:colorBackground | windowBackground light | windowBackground dark | Activity root background |
| textColorPrimary | dark grey | off-white | Primary text |
| textColorSecondary | medium grey | light grey | Captions, hints |
That covers the foundation. The rest is iteration: toggling between modes, watching contrast on a sunny Perth veranda, and refining the values-night entries until the experience feels as considered in dim light as it does at midday on a desktop browser.