Skip to Content

Proper Material Theming in a Multi-Module Setup

Basic Setup

Recently an opportunity opened up for us to fully utilize a Material Theming & Styling setup in one of our new projects. We took some time to dig into this topic and made a clean setup in order to prepare for a long-term running project. This article will describe the results and our learnings.

Let’s start with the basic setup: as with all of our projects we use our A3AndroidTemplate as a starting point. From there we extend the gradle module setup to prepare for the future: next to the main module app, we’ll use some shared modules like base (contains e.g. BaseActivity or BaseFragment), networking (holds Retrofit dependencies) or model (contains shared data classes) and feature modules. The advantage of this kind of multi-module setup was already explained exhaustively by other authors at other blogs (see here or here, so here we’ll not go into this again.

Some notes to the setup which will get relevant later:

  • app holds the AndroidManifest.xml, uses the com.android.application gradle plugin, generates the apk and only includes other modules, but is not included anywhere itself
  • base holds dependencies to the required shared modules like model or ui-components
  • feature modules always include base and possibly along with other shared modules as needed (e.g. networking)
  • ui-components holds the theming & styling configuration

Module Setup

Understanding What Needs To Be Done Where

Let’s begin with the first question that usually arises: Where shall the theming be placed?

It sounds like a simple question, but it’s not a simple answer, because:

  • The Theme has to be referenced from AndroidManifest in the app module
  • The app module itself is not included in other modules, therefore the preview of the layout editor in AndroidStudio has no access to the Theme — which basically means, that we would have to work blind.

The solution is to map the module dependency into the theme itself — meaning that there is an AppTheme.Base in the ui-components module, which defines the whole theming, and an AppTheme in the app module, which inherits from AppTheme.Base. Issue solved!

Theme VS Style

Before we can actually start the implementation, one last thing is missing: understanding the difference between “style” and “theme”. It might be a common concept, but it doesn’t hurt to recall it once again.

Chris Banes describes it as follows:

So what exactly is the difference? Well they are both declared in exactly the same way (which you already know), the difference comes in how they’re used.

Themes are meant to be the global source of styling for your app. The new functionality doesn’t change that, it just allows you to tweak it per view.

Styles are meant to be applied at a view level. Internally, when you set style on a View, the LayoutInflater will read the style and apply it to the AttributeSet before any explicit attributes (this allows you to override style values on a view).

Values in an attribute set can reference values from the View’s theme.

TL;DR: Themes are global, styles are local.

You can read more about it in his article Theme vs Style

Android Material Components Library

So now that everything is set, we can finally start the theming & styling. We use the new Android Material Components library for this, as it is now the recommended way of Google. The library offers a great variety of customization — far more than the we used to have. It took some time to look through all the possibilities and new named attributes. E.g. there are now color attributes like colorOnPrimary, colorOnSecondary, colorTextPrimary or colorTextSecondary. These attributes are theme colors and are therefore meant to be set once and used globally — no more @color/colorYellow, they are applied by the global theme! Same applies for the TextAppearance attributes.

In general, the library offers 3 theming areas:

Summary: The final setup

The final result looks as follows:

  • The AppTheme.Base Style is located in the ui-components module, we extend it with AppTheme in app
  • All styles are grouped in the ui-components module

Since having an extensive customized styling, the default styles.xml would simply explode, we started to group the different styles into separate files. At the moment of writing, this includes:

  • buttons.xml — defines common button styles (see appendix 1)
  • dialogs.xml — defines common dialog styles
  • text.xml — defines inheritances from TextAppearance
  • themes.xml — defines the global theme

Appendix 1: Button Styling Example

button.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <style name="Widget.App.Button.Shaped" parent="@style/Widget.MaterialComponents.Button">
        <item name="android:elevation">8dp</item>
        <item name="backgroundTint">@color/button_round</item>
        <item name="android:textColor">?colorOnPrimary</item>
        <item name="shapeAppearance">@style/Appearance.App.Button.Shaped</item>
        <item name="enforceTextAppearance">@style/TextAppearance.App.Body1</item>
        <item name="android:padding">16dp</item>
    </style>
    
    <style name="Widget.App.Button.Shaped.Inverted" parent="Widget.App.Button.Shaped">
        <item name="backgroundTint">@color/button_shaped_inverted</item>
        <item name="android:textColor">?colorPrimary</item>
        <item name="rippleColor">@color/button_shaped_inverted_ripple</item>
        <item name="enforceTextAppearance">@style/TextAppearance.App.Body1</item>
    </style>
    
    <style name="Widget.App.Button.Shaped.Alarm" parent="Widget.App.Button.Shaped">
        <item name="backgroundTint">@color/button_shaped_alarm</item>
        <item name="android:textColor">?colorOnPrimary</item>
        <item name="enforceTextAppearance">@style/TextAppearance.App.Body1</item>
    </style>
    
    <style name="Appearance.App.Button.Shaped" parent="">
        <item name="cornerSizeTopLeft">@dimen/cornerShapedBig</item>
        <item name="cornerSizeTopRight">@dimen/cornerShapedSmall</item>
        <item name="cornerSizeBottomRight">@dimen/cornerShapedBig</item>
        <item name="cornerSizeBottomLeft">@dimen/cornerShapedSmall</item>
        <item name="enforceTextAppearance">@style/TextAppearance.App.Body1</item>
    </style>
    
    <style name="Widget.App.Button.Flat" parent="Widget.MaterialComponents.Button.TextButton">
        <item name="enforceTextAppearance">@style/TextAppearance.App.Body1</item>
    </style>
    
    <style name="Widget.App.Button" parent="TextAppearance.MaterialComponents.Button">
        <item name="fontFamily">?fontFamilyPrimary</item>
        <item name="android:textColor">?android:textColorPrimary</item>
        <item name="android:textStyle">bold</item>
        <item name="enforceTextAppearance">@style/TextAppearance.App.Body1</item>
    </style>
</resources>

button_round.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="?colorPrimaryVariant" android:state_enabled="false" />
    <item android:color="?colorPrimary" />
</selector>

button_shaped_inverted_ripple.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="?colorPrimaryVariant" android:state_enabled="false" />
    <item android:color="?colorSurface" />
</selector>

button_shaped_inverted.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="?colorPrimaryVariant" android:state_enabled="false" />
    <item android:color="?colorSurface" />
</selector>

button_shaped_alarm.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="?colorAlarm" android:state_enabled="false" />
    <item android:color="?colorAlarm" />
</selector>

Appendix 2: Further readings