Android Themes & styles, a real architecture

le 23/05/2017 par Pierre Degand
Tags: Software Engineering

In modern Android development, there is a huge rise of advanced architectures like MVP, MVVM or Clean Architecture, crazy libraries like RxJava or Dagger and even new languages like Kotlin. But on most projects, theme and styles are still written in an oldschool way with no consideration on how to architecture them. But these XML are part of your code base and you should show them the same love you show to your Java Code.

A common problem

On a new project, Android Studio generates for you a single style.xml file with an embryo of AppTheme. The more your project is growing, the bigger this AppTheme will be as this is a common place to put your theme attributes.

But, at one point of your project, you may need to add some device or API level specific values to your theme. For example, your app has a minSdkLevel of 16 but you want to add the windowTransluscentStatus attribute to your theme. This attribute was introduced with the API 19 therefore you can’t put it directly in your AppTheme because you will have an error.

api19attribute

In Android, you can put some resources under specific folders to target specific API level. For our example, you can create a res/values-v19/ folder and put a new style.xml file inside and this file will be used with a higher priority than the one in res/values/ when running on a device on API 19.

Great, back to the theme. You have now 2 easy solutions for the new API 19 attributes. First you could

  • Copy all your AppTheme from the res/values/styles.xml

  • Paste it on the -v19 version

  • Add the new attribute only on the -v19 file.

This will work because the -v19 theme will be only used on API 19 and more and you new attribute will work. But please, don’t do this. This will be a hell to maintain both files because as soon as you’ll want to add a new value to the base theme, you will have to remember to copy it as well on the -v19 theme.

Another solution is:

  • Create a BaseAppTheme in the res/values/styles.xml file with all the non-specific attributes

  • In the same file, change the AppTheme to make it inherit from the BaseAppTheme

  • In the -v19 version, you just have to make your AppTheme also inherit from the BaseAppTheme

  • Add the new specific attribute.

With this, if you want to add a new attribute for all API level, you just have to put it inside the BaseAppTheme and you don’t have to worry about copying it to the other file. Great!

values/styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Base application theme. -->
    <style name="BaseAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:windowBackground">@color/windowBackground</item>
    </style>
    <style name="AppTheme" parent="BaseAppTheme"/>
</resources>

values-v19/styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="AppTheme" parent="BaseAppTheme">
        <item name="android:windowTranslucentStatus">true</item>
    </style>
</resources>

More API level !

Now, you learn about a new cool API 21 specific new attribute and you want to use it ! You create a new res/values-v21/ folder, you add a new styles.xml file, you write a new AppTheme which extends the BaseAppTheme and you put your new attribute !

values-v21/styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="AppTheme" parent="BaseAppTheme">
        <item name="android:windowSharedElementEnterTransition">@android:animator/fade_in</item>
    </style>
</resources>

You run the app on a Lollipop+ and … the status bar is no longer transluscent ! Of course ! The system is using the AppTheme from the v21 folder and it’s bypassing the ones from v19 and the base folder. The attributes from the v19 AppTheme is not in the final theme when running on v21.

Introducing the theme inheritance chain

To fix this problem, let’s rewrite all our themes.

In the res/values/styles.xml, you create a Base.V0.AppTheme with all the generic attributes. This theme inherits from the standard AppCompat theme. In the same file, you also add the AppTheme and make it inherit from the Base.V0.AppTheme.

values/styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="AppTheme" parent="Base.V0.AppTheme"/>

    <style name="Base.V0.AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Generic, non-specific attributes -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:windowBackground">@color/windowBackground</item>
    </style>
</resources>

In the res/values-v19/styles.xml, you create a Base.V19.AppTheme and put the api 19 specific values. The parent of this theme is Base.V0.AppTheme. In this file, you add the AppTheme and make it inherit from the Base.V19.AppTheme

values-v19/styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="AppTheme" parent="Base.V19.AppTheme"/>
    
    <style name="Base.V19.AppTheme" parent="Base.V0.AppTheme">
        <!-- API 19 specific attributes -->
        <item name="android:windowTranslucentStatus">true</item>
    </style>
</resources>

In the res/values-v21/styles.xml, you finally create a Base.V21.AppTheme and put all the api 21 specific values. The parent of this is, you name it, Base.V19.AppTheme. And finally you add the AppTheme and make it inherit from the Base.V21.AppTheme.

values-v21/styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="AppTheme" parent="Base.V21.AppTheme"/>
    
    <style name="Base.V21.AppTheme" parent="Base.V19.AppTheme">
        <!-- API 21 specific attributes -->
        <item name="android:windowSharedElementEnterTransition">@android:animator/fade_in</item>
    </style>
</resources>

With this architecture of themes, for every API level, the AppTheme will have all the attributes from all API levels and it’s very easy to extend and add new api level specific attributes later.

This can also be used for styles because this is not only useful for API levels specific attributes but also for overriding specific values on tablet for exemple. Here is a full example for a view that should have a different width on phone and tablet and that should have an elevation on API 21+:

values/styles.xml for default and phone attributes.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Style.MainContent" parent="Base.Style.MainContent" />

    <style name="Base.Style.MainContent" parent="Base.SW.Style.MainContent" />

    <style name="Base.SW.Style.MainContent" parent="Base.SW0.Style.MainContent" />

    <style name="Base.SW0.Style.MainContent" parent="Base.ApiLevel.Style.MainContent">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_margin">8dp</item>
    </style>

    <style name="Base.ApiLevel.Style.MainContent" parent="Base.V0.Style.MainContent" />

    <style name="Base.V0.Style.MainContent" parent="">
        <item name="android:background">#FFFFFF</item>
    </style>
</resources>

values-v21/styles.xml for API21+ attributes.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Base.ApiLevel.Style.MainContent" parent="Base.V21.Style.MainContent" />

    <style name="Base.V21.Style.MainContent" parent="Base.V0.Style.MainContent">
        <item name="android:elevation">4dp</item>
    </style>
</resources>

values-sw600dp/styles.xml for tablet attributes.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Base.SW.Style.MainContent" parent="Base.SW600.Style.MainContent" />

    <style name="Base.SW600.Style.MainContent" parent="Base.SW0.Style.MainContent">
        <item name="android:layout_width">480dp</item>
        <item name="android:layout_margin">0dp</item>
        <item name="android:layout_gravity">center_horizontal</item>
    </style>
</resources>

Every attribute is only wrote once, it is easy to maintain, to add new attributes to this style and to extend it for more API levels or using other qualifiers.

I showed you an alternative way of writing and composing themes and styles and I hope you will find it useful in your Android projects. Styles and themes are a really good way of factorizing code and I highly suggest you use them more and if you are not used to, you can read more about it on this blog post.