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 thecom.android.application
gradle plugin, generates the apk and only includes other modules, but is not included anywhere itselfbase
holds dependencies to the required shared modules likemodel
orui-components
feature
modules always includebase
and possibly along with other shared modules as needed (e.g.networking
)ui-components
holds the theming & styling configuration
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 theui-components
module, we extend it withAppTheme
inapp
- 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 stylestext.xml
— defines inheritances from TextAppearancethemes.xml
— defines the global theme
Appendix 1: Button Styling Example
<?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>
<?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>
<?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>
<?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>
<?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>