AppCompat v23.2 — DayNight

AppCompat v23.2 — DayNight

Please note, this post has been updated to reflect changes in AppCompat since first 23.2.0.

As you may have seen in the Support Lib 23.2.0 blog post, AppCompat now has a new theme family: Theme.AppCompat.DayNight.

What these themes do is switch between Theme.AppCompat (dark) and Theme.AppCompat.Light (light) based on the time of day. This has many benefits for your users, especially if you’re a content app (it seems to have become a standard feature in Reddit clients). One thing to note is that this feature only actually has an effect when running on an API v14 and later device, on devices before that it will default to the light theme.

How do I use it?

It’s simple to use, just change your theme to extend from one of the DayNight variants, and then there’s one call to enable it. So here’s your new theme declaration:

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

Then you need to enable the feature in your app. You do that by calling AppCompatDelegate.setDefaultNightMode() which takes one of four values:

  • MODE_NIGHT_NO. Always use the day (light) theme.
  • MODE_NIGHT_YES. Always use the night (dark) theme.
  • MODE_NIGHT_AUTO. Changes between day/night based on the time of day.
  • MODE_NIGHT_FOLLOW_SYSTEM (default). This setting follows the system’s setting, which is essentially MODE_NIGHT_NO at the time of writing (more on this later).

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

public class MyApplication extends Application {

    static {
        AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO);
    }

    // ...
}

setLocalNightMode()

You can override the default value in each component by 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.

Please note, this call does not take care of any recreation so if change the night mode after any inflation, it will not take anyeffect. In this instance you probably want to look at calling recreate().

As of 24.1.0, AppCompat will now automatically call recreate() on your behalf.

How can I check what mode my app is using?

Easy, check your Resources 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
}

We may add some formal APIs around this in a future release.

My app looks weird

Aka I can’t see any text (dark-on-dark). Aka my icons are the wrong color.

Since this functionality changes the theme of your app, you need to make sure that your layouts/styles/drawables work in both light and dark environments.

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.

WebViews

There is one big caveat to 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.

Location location location

To be able to calculate sensible times to switch between day or night, we need to know your location. This might be sounding alarms bells in your head, but don’t fear. If your app already has location permissions granted, AppCompat will try and grab the last known location from LocationManager and use those to calculate sunrise and sunset. It will not however request any permissions on your behalf.

If you do not have those permissions (or there simply isn’t a last known location), then we use some finger-in-the-air fixed values. These are currently 6am for sunrise and 10pm for sunset, but may change in the future as we tweak the values.

As you’re already targeting SDK 23 (I hope), you’ll be using run-time permissions. While we have not specified this flow, the imagined scenario is that your app will have a setting somewhere allowing the user to opt into enable DayNight functionality. That is the time to prompt for any location permissions if you want the higher precision of calculated sunrise/sunset times.

Why didn’t you just make AUTO the default?

There are a few reasons which are hopefully clearer after hearing the imagined scenario for using this functionality:

  1. Change your theme to extend from Theme.AppCompat.DayNight.
  2. Add a setting in your app allowing the user to opt-in. At this point store the setting (probably in a SharedPreference). Call setDefaultNightMode() with the chosen value.

Then the next time your app is launched, read the stored value and call setDefaultNightMode() again with the chosen value.

So the main reason is that we do not want your user to update your app, and suddenly find that it changes color randomly (to them) based on the time of day. Also remember that the default is MODE_NIGHT_FOLLOW_SYSTEM, so if we add a user-visible setting to the platform in the future, AppCompat will automatically use it.

Can I use my own resources for day/night switching?

Yes, you can. AppCompat in simple terms just enables the use of the night mode resource qualifiers at any time. These have actually been available in the platform since API 8, but were only used in a very specific scenario: in car mode and docked.

So 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 your resources for day/night time. Just use the -night qualifier on your resource folder: drawable-night, values-night, etc.

Night night

So there we go. We don’t expect this feature to be applicable or useful to every app, but it can be powerful when used appropriately.