In the first part of this post, we have created a light theme and made initial preparation to support multiple themes. In this blog post, we will continue that effort, creating another theme and allowing dynamic switching of themes during runtime.
Ideally, if we treat theme as a configuration, we should be able to specify theme-specific resources under a ‘theme-qualifier’ resources directory, e.g. values-dark
for dark theme resources and values-light
for light theme resources. Unfortunately, this is not yet an option at the time of this post.
So how should we specify resources for multiple themes? If we look at how resources are organized in appcompat
, we will have a rough idea of how the Android team organize their theme specific resources. Materialistic also employs a similar approach.
Theming
values/styles.xml
values/colors.xml
Here we add a new dark theme called AppTheme.Dark
, and for style and color consistency, we extend from appcompat
’s theme Theme.AppCompat
(a dark theme). Unfortunately, since our two themes extend two different base themes, we cannot share any common attributes (the same way a class in Java cannot extend two or more classes).
The two themes should have appropriate (different if applicable) values for base Android and appcompat
theme attributes, e.g. android:textColorPrimary
for dark theme should be light, and for light theme should be dark. By convention, here we suffix alternative theme colors with Inverse
.
Tip
Try out your alternative theme by temporary switchingandroid:theme
for application
in AndroidManifest.xml
to see what extra colors/style you need to create. For certain cases a color may look okay in both dark and light theme.
Theme-specific resources
At this point, we should have a pretty decent dark theme for our app, except for some anomalies here and there, e.g. drawables used for action bar menu items. A dark action bar expects light-color menu items, and vice versa. In order to tell Android to use different drawables for different app themes, we create custom attributes1 that allow specifying reference to the correct drawable, and provide different drawable references as values for these custom attributes under different themes (the same way appcompat
library provides custom attributes such as colorPrimary
).
values/attrs.xml
values/styles.xml
menu/my_menu.xml
Similar implementation can be used to specify most custom attributes you need for theme specific resource values. One hiccup to this approach is that attribute resolving in drawable resources seems to be broken before API 21. For example, if you have a drawable which is a layer-list
of colors, their values must be fixed for API <21. See this commit from Google I/O 2014 app for a fix.
An alternative approach to avoid duplicating drawable resources for different themes is to use drawable tint
. This attribute is available from API 21. Dan Lew in his blog2 shows how to do this for all API levels. Personally I would prefer to keep my Java implementation free of view logic if possible, so I choose to have different drawable resources per theme.
Dynamic theme switching
Now that we have two polished themes ready to be used, we need to allow users to choose which one they prefer and switch theme dynamically during runtime. This can be done by having a SharedPreferences
, says pref_dark_theme
to store theme preference and use its value to decide which theme to apply. Application of theme should be done for all activies, before their views are created, so onCreate()
is our only option to put the logic.
BaseActivity.java
Here, since our app already has a default light theme, we only need to check if default preference has been overriden to override dark theme. The logic is put in the ‘base’ activity so it can be shared by all activities.
Note that this approach will only apply theme for activities that are not in the back stack3. For those that are already in current stack, they will still exhibit previous theme, as going back will only trigger onResume()
. Depends on product requirements, the implementation to handle these ‘stale’ screens can be as simple as clearing the back stack, or restarting every single activity in the back stack upon preference change. Here we simply clear back stack and restart current activity upon theme change.
SettingsFragment.java
So that’s it. Now we have an app with two polished themes for even the most picky users! Head over to hidroh/materialistic GitHub repository to checkout complete implementation!