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!
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();
// ...
}