DayNight — Adding a dark theme to your app

Chris Banes
Android Developers
Published in
6 min readFeb 25, 2016

--

Illustration by Virginia Poltrack

This post has been updated multiple time since first publishing. The content is correct as-of April 26th 2019.

The DayNight functionality in AppCompat allows your app to easily switch between a dark ⚫ and light ⚪ theme. This has many benefits for your users, from saving power on OLED displays, to increasing usability for people with reduced-vision, and more.

How do I use it?

You need to change your theme to extend from one of the DayNight variants, and then call one method to enable the feature. Here’s an example theme declaration:

<style name="MyTheme" parent="Theme.AppCompat.DayNight">    <!-- Blah blah --></style>

If you’re using Material Design Components (and I recommend you to do so), then you can also use the Theme.MaterialComponents.DayNight theme from their v1.1.0 release. The rest of this post remains the same.

You then need to enable the feature in your app using one of the APIs provided.

setDefaultNightMode

The first API we provide to do that is AppCompatDelegate.setDefaultNightMode(), which takes one of the follow values:

  • MODE_NIGHT_NO. Always use the day (light) theme.
  • MODE_NIGHT_YES. Always use the night (dark) theme.
  • MODE_NIGHT_FOLLOW_SYSTEM (default). This setting follows the system’s setting, which on Android Q and above is a system setting (more on this below).
  • MODE_NIGHT_AUTO_BATTERY. Changes to dark when the device has its ‘Battery Saver’ feature enabled, light otherwise.
    New in v1.1.0-alpha03.
  • MODE_NIGHT_AUTO_TIME & MODE_NIGHT_AUTO. Changes between day/night based on the time of day.
    Deprecated in v1.1.0-alpha03.

The method is static so you can call it at any time. The value you set is not persisted across process starts though, therefore you need to set it every time your app process is started. I recommend setting it in your application class (if you have one) like so:

public class MyApplication extends Application {

public void onCreate() {
super.onCreate();
AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_YES);
}
}

From AppCompat v1.1.0-alpha05 onwards, setDefaultNightMode() will automatically apply any DayNight changes to any ‘started’ Activities. This means that you no longer have to manually recreate any Activities when you call the API.

setLocalNightMode

You can also override the default value in each component with a call to its AppCompatDelegate’s setLocalNightMode(). This is handy when you know that only some components should use the DayNight functionality, or for development so that you don’t have to sit and wait for night to fall to test your layout.

Using this method in every Activity is now an anti-pattern, and you should move to using setDefaultNightMode() instead. See the section below for the technical details to why.

Activity recreations

Both of the methods mentioned above will recreate your Activity if a Configuration change is required, so that the new theme can be applied. This is a good opportunity to test whether your Activity + Fragments save their instance state correctly.

How can I check what configuration I’m currently in?

You can do this by checking your resource configuration:

int currentNightMode = getResources().getConfiguration().uiMode
& Configuration.UI_MODE_NIGHT_MASK
switch (currentNightMode) {
case Configuration.UI_MODE_NIGHT_NO:
// Night mode is not active, we're in day time
case Configuration.UI_MODE_NIGHT_YES:
// Night mode is active, we're at night!
case Configuration.UI_MODE_NIGHT_UNDEFINED:
// We don't know what mode we're in, assume notnight
}

WebViews

There is currently one big caveat to using this feature: WebViews. Since they can not use theme attributes, and you rarely have control over any web content’s styling, there is a high probability that your WebViews will be too contrasting against your dynamic themed app. So make sure you test your app in both modes to ensure that it’s not annoying to the user.

System night mode

Android Q onwards has a system night mode which can be enabled in the Settings app. Android Pie also has a system night mode setting, but it is only surfaced in the device’s ‘developer options’. For ease, I recommend treating Android Pie the same as previous versions of Android.

In-app setting

It is recommended to provide a way for the user to override the default theme in your app. The recommended options and strings are:

  • ‘Light’ (MODE_NIGHT_NO)
  • ‘Dark’ (MODE_NIGHT_YES)
  • ‘Set by Battery Saver’ (MODE_NIGHT_AUTO_BATTERY).
    Only show on Android Pie and below. This should be your app’s default when shown.
  • ‘Use system default’ (MODE_NIGHT_FOLLOW_SYSTEM).
    Only show on Android Q and above. This should be your app’s default when shown.

A common way to do to implement would be via a ListPreference, calling to setDefaultNightMode() when the value changes.

Updating your themes + styles

As well as calling AppCompat, you will likely need to do some work to update your themes, styles and layouts so that they work seamlessly across both dark and light themes.

The rule-of-thumb for these things is to always use theme attributes when you can. Here are the most important to know about:

  • ?android:attr/textColorPrimary. General purpose text color. Will be near-black on light theme, near-white on dark themes. Contains a disabled state.
  • ?attr/colorControlNormal. General-purpose icon color. Contains a disabled state.

Using Material Design Components also makes this a lot easier, as it’s attributes (such as ?attr/colorSurface and ?attr/colorOnSurface) provide you an easy generalized themed color to use. These attributes of course can be customized in your theme.

Using your own resources for dark/light

AppCompat in simple terms is just enabling the use of the night and notnight resource qualifiers. These have actually been available in the platform since API 8, but were previously only used in very specific scenarios.

Under the hood Theme.AppCompat.DayNight is implemented as so:

res/values/themes.xml

<style name="Theme.AppCompat.DayNight" 
parent="Theme.AppCompat.Light" />

res/values-night/themes.xml

<style name="Theme.AppCompat.DayNight" 
parent="Theme.AppCompat" />

This means that you can also provide alternative resources for your light and dark UIs. Just use the -night qualifier on your resource folders: drawable-night, values-night, etc.

Why should I move to using setDefaultNightMode?

In AppCompat v1.1.0, DayNight had a big rewrite in order to fix a number of bugs. The biggest bug was this:

  • WebView would reset the Activity configuration as soon as it was loaded into the process. This could lead to later inflated views using the wrong theme.

AppCompat needs to change the Activity’s resources configuration to enable “night mode”. The problems with WebView stemmed from using a now deprecated method: Resources.updateConfiguration() to achieve that. Unfortunately WebView doesn’t work very well with that method (hence the deprecation).

The rewrite focused on moving to a newer method to update the configuration: ContextThemeWrapper.applyOverrideConfiguration(). Unfortunately, that API is a lot more tricky to use since it can only be called before any call to getResources(). It turns out getResources() is called a lot, and gets called very early on in the Activity lifecycle. In fact the only place I found where it could be called safely is in attachBaseContext(), which is a lot earlier than onCreate().

OK, I’ve just told you a lot of technical stuff about the internals of DayNight, so what does that have to do with setDefaultNightMode()? Well because we can only call applyOverrideConfiguration() in attachBaseContext(), this gives us a very small window for setLocalNightMode() to work without having to recreate the Activity.

Previously you could do the following quite happily, without AppCompat needing to recreate the Activity:

public class MyActivity extends Activity {

public void onCreate(Bundle icicle) {
super.onCreate(icicle);
getDelegate().setLocalNightMode(
AppCompatDelegate.MODE_NIGHT_YES);
}
}

Due to what we’ve spoken about above, that will now trigger a recreation in the new version of DayNight. In fact, any call to setLocalNightMode will trigger a recreation (if the theme changes).

This is the crux of why you should now prefer setDefaultNightMode(), to minimize unnecessary recreations. Since it’s a static method, the value is always available, and isn’t beholden to the Activity lifecycle. The method models what most apps want to do anyway, which is provide an app-wide setting or preference.

That doesn't mean that setLocalNightMode is wrong to use, just use it for what it is meant for: one-off overrides in individual Activities.

--

--