Moving to the Dark Side: Dark Theme Recap

Yaroslav Berezanskyi
ProAndroidDev
Published in
6 min readMay 30, 2019

--

Dark Theme is finally here! 🎉

In fact, it’s been there for a few years already (aka DayNight). However, the majority of the apps haven’t taken advantage of it yet. Only 39% of apps on my device have a dark theme. What a pity!

That’s why I decided to gather everything in one place and provide a complete technical walkthrough of Dark Theme implementation including the latest updates in Android Q. Also, the post addresses some caveats and implementation details which are not documented or mentioned anywhere at the time of writing.

Why is the Dark Theme important?

There are 3 major reasons:

  • Can significantly reduce power usage for devices with OLED display. That’s why some manufacturers enable Dark Theme as a part of the Battery Saver mode;
  • Brings much more comfortable and eye-friendly way to use your device in a low-light environment;
  • Improves visibility for users with low vision and those who are sensitive to bright light.

Dark Theme is opt-in

Technically, your app is not affected in any way (even by targeting Android Q) until you explicitly opt-in. However, users will increasingly expect your app to support it since the system level toggle is introduced in Android Q.

NOTE: The only exception here is the new Override force-dark option (introduced in Android Q) in Developer options which can force your app to be dark using the new Force Dark mode. However, it’s unlikely to be used by regular users. Moreover, there is a way to disable this behavior as well. I’ll address that at the end of the post.

There are 2 ways to support Dark Theme:

  1. Make your app’s theme extend AppCompat.DayNight (or similar from MaterialComponents). It includes some predefined values suitable for the dark environment for basic things like text or background color. You are to provide an alternative version for other things yourself using -night qualifier. It’s backward compatible up to API 14.
  2. Enable the new Force Dark feature by setting android:forceDarkAllowed=”true” in the app’s theme. It attempts to automatically convert your app to Dark Theme at rendering time. It’s available starting from Android Q (API 29). We’ll talk about it in details shortly.

Despite the implementation complexity, the first one is the only recommended approach since it’s backward compatible and gives you fine-grained control over your app. Force Dark may only be used as a temporary solution for the sake of rapid development iteration.

Levels of control. Backward compatibility

There are 3 levels of control for Dark Theme:

  • System setting;
  • Application setting;
  • Activity setting.

The important part to understand here is that a local setting always wins. For instance, if the system setting is set to Dark, but the application setting is Light, then the app follows Light. Pretty simple.

System Setting

It’s a global setting which is controlled by the user either explicitly or implicitly (by toggling the Battery Saver mode).

There are a limited number of ways to change it:

  • Dark Theme Toggle (Settings->Display->Dark Theme) — introduced in Android Q (API 29);
  • Night Mode developer option (Settings->System->Developer Options->Night mode) — available in Android P only (API 28).
  • Battery Saver Mode. Backward compatible up to Android Lollipop (API 21). Some EOMs might not support that.

NOTE: On Android 8.1 and 9 users can trigger the dark theme for the system UI by setting a dark wallpaper or choosing it explicitly in settings (Settings->Display->Device Theme). However, it affects the system UI only. Regular applications are not able to follow it.

This setting is applied on the system level including all the system UI and applications. Once the setting is changed, your application gets Application.onConfigurationChange callback and all activities are immediately recreated. However, it’s up to your application to either follow it or override with a local one (application or activity wide setting).

NOTE: There is an exception for Battery Saver mode on API 21–27. Application.onConfigurationChange is not called in this case, but activities are recreated as intended. Most likely, it was not possible to provide backward compatibility for these versions due to some internal limitations.

Application Setting

As a good citizen, you can let the user choose between themes inside your app (overriding the system setting).

It’s to be controlled using AppCompatDelegate.setDefaultNightMode API via your custom widget (usually, it’s ListPreference in your settings screen).

The recommended options are:

  • Light
  • Dark
  • Set by Battery Saver. It’s backward compatible up to API 21 (the recommended default option for API 21–27)
  • System default (the recommended default option for API 28 and above)

Furthermore, you can set Light as the default and hide the last 2 options for API below 21 since none of them are supported.

Each of the options maps directly to one of the AppCompat.DayNight modes:

  • LightMODE_NIGHT_NO
  • DarkMODE_NIGHT_YES
  • Set by Battery SaverMODE_NIGHT_AUTO_BATTERY
  • System defaultMODE_NIGHT_FOLLOW_SYSTEM

NOTE: MODE_NIGHT_AUTO_TIME is deprecated starting from v1.1.0.

Once the setting is changed, all started activities get recreated (or get Activity.onConfigurationChange callback, if you opted-in in the manifest to handle the configuration change manually).

NOTE: The application setting is not persisted across process death. Therefore, you need to set it every time your app process is started (Application.onCreate is the best place).

Activity Setting

It’s very similar to the application setting, but applies to a specific activity only using getDelegate().setLocalNightMode. Be aware that any call of it triggers an activity recreation (if the theme changes). As Chris Banes suggested here, you should prefer AppCompatDelegate.setDefaultNightMode over it since it minimizes unnecessary recreations.

NOTE: Be aware that setLocalNightMode(MODE_NIGHT_FOLLOW_SYSTEM) makes your activity follow the system setting, not the application one.

Check whether Dark Theme is applied programmatically

Sometimes you might want to customize your UI based on a theme at runtime. You can check whether Dark Theme is applied using the following code:

Note: Never use application context to check whether the dark theme is applied at that moment. After some testing, I found out that it might return wrong value in some edge cases. Always use activity instead. You can check the bug report here.

Theming

There are a lot of great posts/talks on this topic which I highly recommend you to check out. I’ll put links to some of them at the end. I’ll just mention a few rules of thumb:

  • reuse semantically named theme attributes whenever you can (in layouts, drawables, color lists, etc.) which can have different values when running under the dark or light theme. For instance, you can use ?attr/colorSurface, ?attr/colorOnBackground or ?attr/colorPrimary from the latest material palette to name a few;
  • provide alternative versions for other things using -night qualifier;
  • work with your designer to come up with desaturated colors for your dark theme palette.

Theming for web views, google maps or custom tabs are out of the scope of the post cause it’s a pretty narrow topic. Luckily, there is a great talk on that by Nick Butcher and Chris Banes.

Force Dark (aka Smart Dark)

This feature attempts to apply the dark theme for apps during rendering time without any extra work needed (available on Android Q). Apps can opt-in for Force Dark by setting android:forceDarkAllowed=”true” in the app’s theme (applicable to Light themes only).

Despite doing a pretty good job, sometimes It might produce some visual artifacts. If you find one, you can exclude a specific view/viewgroup from force-dark mode by setting android:forceDarkAllowed or View.setForceDarkAllowed() flags to false.

Override force-dark option from Developer options

I didn’t find any documentation which describes what this option does exactly, but I was expecting it to make android:forceDarkAllowed attribute always true. In fact, it’s not the case. It forces all apps to be dark even when Dark Theme is disabled by the system toggle. Another interesting fact is that you can block this option from being applied to your app by setting android:forceDarkAllowed to false explicitly. However, Android Q is still in Beta, so this behavior is subject to change.

Wrap-up

Yeah, I know. Dark theme implementation might be tedious work, but the result is definitely worth it. The loyalty of the users will increase and the app will look fluent across the system.
Finally, it’s our job as developers to make users happy!

Sample project

Feel free to check the playground project on GitHub.

Environment used for research and testing:

AppCompat library 1.1.0-rc01
Android Q Beta 4
Target SDK 29

Further Resources

Sample projects using Dark Theme on GitHub

--

--