Material Components / Themes

Broken Material Theme

When is a Material theme not a Material theme?

That may sound like the first line of a joke, but it is actually a real problem I faced recently. It caused me much noggin scratching until I chanced upon the cause. And you’ll have to wait until the end of the article for the punch line!

encountered the issue when putting together the code for the recent Currency Converter series. I have created a separate project which highlights the issue as the Currency Converter project is relatively large.

There are two separate branches in the sample code. The broken branch is, as its name suggests, the broken version. It compiles perfectly but crashes during the inflation of the main layout. For anyone wanting to play along at home, you can check out the broken branch and try and work out where the problem lies before reading further.

Investigating the problem

For those that haven’t read the previous articles, the project in question had little in the way of layouts, but I wanted to add a button to the main layout so that I could test triggering an immediate data refresh. All was working fine until I added a Button to the layout whereupon I started getting runtime crashes.

Investigating the runtime exception is the obvious place to start:

com.stylingandroid.brokenmaterialtheme E/AndroidRuntime: Caused by: java.lang.IllegalArgumentException: The style on this component requires your app theme to be Theme.AppCompat (or a descendant).
        at com.google.android.material.internal.ThemeEnforcement.checkTheme(ThemeEnforcement.java:243)
        at com.google.android.material.internal.ThemeEnforcement.checkAppCompatTheme(ThemeEnforcement.java:213)
        at com.google.android.material.internal.ThemeEnforcement.checkCompatibleTheme(ThemeEnforcement.java:148)
        at com.google.android.material.internal.ThemeEnforcement.obtainStyledAttributes(ThemeEnforcement.java:76)
        at com.google.android.material.button.MaterialButton.(MaterialButton.java:229)
        at com.google.android.material.button.MaterialButton.(MaterialButton.java:220)
        	... 27 more

I haven’t reproduced the entire stack trace here, just the most relevant part. The main error is:

java.lang.IllegalArgumentException: The style on this component requires your app theme to be Theme.AppCompat (or a descendant).

So this seems fairly straightforward – I forgot to use a Material or AppCompat theme as the base for my app there. So let’s take a look at that:


    
    

Wait. Wat?

This theme does have a MaterialComponents theme as its parent.

Thus began my noggin scratching.

Digging Deeper

I confirmed that my dark theme also used a MaterialComponents theme as its parent. It did.

I confirmed that the MaterialComponents theme actually used an AppCompat theme as its parent. It did.

I verified that the theme being used to perform inflation was, in fact, the app theme. It was. This actually involved some rather hack, convoluted code.

At this point I started analysing how MDC works internally. The fact that my standard Button was being transformed to a MaterialButton showed that MDC inflation was happening. The stack trace from earlier shows that it was in the MaterialButton constructor that the theme validation was taking place.

An internal MDC class named ThemeEnforcement was performing and failing the theme validation. The specific test that was failing is checkAppCompatTheme(). That method is being called from checkCompatibleTheme() The really odd thing is that checkAppCompatTheme() is the final line in checkCompatibleTheme(). There are other checks which all pass prior to this one. One of these is checkMaterialTheme(). I stepped through the code using a debugger and verified that these checks were all performed and passed.

So, the Material theme check passed whereas the AppCompt theme check failed. And the MaterialComponents theme that I was using for my app theme extends an AppCompat theme.

Noggin scratching continued

Deeper Down the rabbit hole

A deep dive in to understanding precisely what checkAppCompatTheme() does was required. In fact it’s really quite simple – it checks the theme attribute colorPrimary has been set. Nothing more than that.

So I went back to my theme (as posted earlier in the article) and confirmed that colorPrimary was being set in the theme. It was.

My noggin was becoming raw from all the scratching.

Eureka!

My “Eureka!” moment came from something I spotted out of the corner of my eye. I was checking my colors.xml file, and wasn’t seeing any obvious issues:



    #000000
    #FFFFFF

    #t6565575
    #156912
    #4EE147

However, I spotted something odd in Android Studio:

There’s no wiggly red line showing an error in the code itself, but the colour swatch in the left gutter is missing for colorPrimary. All of the other colour definitions have colour swatches, except colorPrimary. Looking closer at this, it is clear that #t6565575 is not a valid hex colour value. But it’s difficult to spot. Moreover, this was compiling correctly.

I’m not quite sure how this got changed from the correct value of #1E9618. I must have pasted something without realising that this window was active at some point. The odd thing is why AAPT didn’t complain about it. If I just insert a random t in the correct value (#t1E9618), then AAPT fails:

/app/src/main/res/values/colors.xml:6:4: Invalid  for given resource value.

But, for some reason, the specific value of #t6565575 compiles. I checked the resource definitions that were added to the APK and found that saGreen was defined with an empty value. This now explains why the AppCompat theme check was failing – because there was not valid value for colorPrimary. But it’s still something of a mystery why this value did not produce an error from AAPT.

Conclusion

A perfect combination of factors produced this issue. First there was an invalid colour value which somehow did not cause AAPT to barf. This resulted in a null value for saGreen. This happened to be used for colorPrimary. Any other usage of it would not have caused the ThemeEnforcement checks to pass.

While the chances of anyone else hitting this exact same problem are small, I though that sharing the steps I went through to try and understand what was happening might help others trying to track down similarly obscure issues.

To give the punch line to the question at the beginning:

Q: When is a Material theme not a Material theme?
A: When it has a null colorPrimary.

What a rubbish joke that is!

The fixed source code for this article is available here.

© 2021, Mark Allison. All rights reserved.

Copyright © 2021 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.