Search code examples
androidandroid-themematerial-components

Occasional InflateException due to ThemeEnforcement errors using MaterialComponents. How to solve?


After spending an entire day trying to figure this out, I am almost positive it is a bug in the Material Components library. Here are relevant parts of my configuration(removed irrelevant elements):

styles.xml:

<style name="MaterialAppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryVariant">@color/colorPrimary900</item>
    <item name="colorSecondary">@color/colorSecondary</item>
    <item name="colorSecondaryVariant">@color/colorSecondary900</item>
    <item name="android:statusBarColor">@android:color/transparent</item>
</style>

AndroidManifest:

<application
    android:name=".AndroidApplication"
    android:theme="@style/MaterialAppTheme">

MainActivity:

<activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:launchMode="singleTop"
        android:theme="@style/MaterialAppTheme">

Dependency versions:

materialComponents: '1.1.0-alpha02',
constraintLayout  : '2.0.0-alpha3',
appCompat         : '1.1.0-alpha01'

I have no problems adding a MaterialButton to my login page. The problems start in my MainActivity, in my first list fragment. When trying to display recycler view items with MaterialCardView as the root of each item, I am getting ThemeEnforcement errors. Same goes when I try adding MaterialTextButton. Here is what my MaterialCardView xml declaration looks like:

<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
                                               xmlns:card_view="http://schemas.android.com/apk/res-auto"
                                               xmlns:tools="http://schemas.android.com/tools"
                                               android:id="@+id/tripCard"
                                               style="@style/Widget.MaterialComponents.CardView"
                                               android:layout_width="match_parent"
                                               android:layout_height="wrap_content"
                                               android:layout_margin="8dp"
                                               card_view:cardBackgroundColor="@color/colorSurface"
                                               card_view:cardCornerRadius="@dimen/widget_corner_radius"
                                               card_view:cardElevation="@dimen/card_elevation">

When breakpointing in the MaterialComponents library, I notice it will fail to validate the theme at some point. Following is some of the library's code showing where it is failing (in ThemeEnforcement.java):

    if (enforceMaterialTheme) {
  TypedValue isMaterialTheme = new TypedValue();
  boolean resolvedValue =
      context.getTheme().resolveAttribute(R.attr.isMaterialTheme, isMaterialTheme, true);

  if (!resolvedValue
      || (isMaterialTheme.type == TypedValue.TYPE_INT_BOOLEAN && isMaterialTheme.data == 0)) {
    // If we were unable to resolve isMaterialTheme boolean attribute, or isMaterialTheme is
    // false, check for Material Theme color attributes
    checkMaterialTheme(context);
  }
}

public static void checkMaterialTheme(Context context) {
    checkTheme(context, MATERIAL_CHECK_ATTRS, MATERIAL_THEME_NAME);
}

private static final int[] MATERIAL_CHECK_ATTRS = {R.attr.colorPrimaryVariant};

From what I can tell, the Theme Enforcer tries to resolve the isMaterialTheme attribute (which it can sometimes do albeit not in this case) and then it goes ahead and checks the presence of material theme color attributes (checks for colorPrimaryVariant) and fails once again (I have defined it in my app theme, too.)

I will go ahead and file a bug, if anyone has the solution or any suggestions it would be very much appreciated!


Solution

  • To follow up on comments: it is important that you use a LayoutInflater that's been retrieved from a Context that is using the correct theme. An application context will not satisfy this requirement.

    The good news is that you don't have to worry about injecting a context into your adapter. It's not immediately obvious, but the adapter provides everything you need in order to get the right context.

    Take a look at the onCreateViewHolder() skeleton:

    @NonNull 
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        // ...
    }
    

    All View instances have a getContext() method, and the parent parameter is guaranteed to be non-null... so you can retrieve a context from the parent and use that to get your LayoutInflater:

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View itemView = inflater.inflate(R.layout.my_item_view, parent, false);
        return new MyViewHolder(itemView);
    }
    

    Similarly, inside a ViewHolder, you can use the same technique to get a context object. Every ViewHolder instance has an itemView field that's guaranteed to be non-null, so you can retrieve the context from that.

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Context context = holder.itemView.getContext();
        // ...
    }