Do not always trust @JvmOverloads
You can lose your custom view style when subclassing a
TextView
,Button
or other components. Here’s a cool way to do this in Kotlin.
It’s common when writing in Kotlin, to combine many constructors into one by @JvmOverloads
annotation, especially when subclassing Android views. Usually, it’s ok, but sometimes you can run into some unexpected issues.
Let’s take a look at the definition:
Instructs the Kotlin compiler to generate overloads for this function that substitute default parameter values.
If a method has N parameters and M of which have default values, M overloads are generated: the first one takes N-1 parameters (all but the last one that takes a default value), the second takes N-2 parameters, and so on.
Sounds good, so we usually combine all those constructors:
class CustomLinearLayout : LinearLayout {constructor(context: Context?) : super(context)constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)}
into one:
class CustomLinearLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr)
Step 0: The problem
Let’s take a look at TextInputEditText
from the Design library.
In our custom class
class CustomTextInputEditText : TextInputEditText {constructor(context: Context) : super(context)constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)}
we can replace all those constructors with only one:
class CustomTextInputEditText @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : TextInputEditText(context, attrs, defStyleAttr)
This is also the code that will be generated by Android Studio.
So let’s have an Activity that has two TextInputEditText
components, first of them with all three constructors, and the second one with @JvmOverloads
annotation.
class CustomTextInputEditText1 : TextInputEditText { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)}class CustomTextInputEditText2 @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : TextInputEditText(context, attrs, defStyleAttr)
Will they look and behave the same at the end?
As you can see, the second one with @JvmOverloads
is not working at all.
What happened? Why do we have some styling issues?
Step 1: Understanding the @JvmOverloads annotation
Let’s go back to the @JvmOverloads
definition for a second. We know that two overloads will be generated (in our case N=3 and M=2) by the Kotlin compiler. So we will end up having three constructors similar to:
@JvmOverloads
public CustomTextInputEditText(@NotNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}@JvmOverloads
public CustomTextInputEditText(@NotNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}@JvmOverloads
public CustomTextInputEditText(@NotNull Context context) {
this(context, null, 0);
}
So always in our custom class, we will call the three-param constructor from TextInputEditText.
Step 2: Understanding View’s constructors
Now let’s focus on View’s constructors for a while.
When a View is inflated from an XML file, its’ second constructor is called. This constructor then calls a three-param constructor.
public View(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
So why does this three-param constructor even exist if it usually takes 0 as the third parameter? The answer is in the docs.
This constructor of View allows subclasses to use their own base style when they are inflating.
Classes subclassing the View class can pass their own style to modify all of the base view attributes. Easy.
Now we are onto something.
Step 3: Understanding what went wrong
At this point, from Step 1, we know that because of @JvmOverloads
annotation we always call a three-param constructor, and from Step 2, that classes subclassing the View class can use this three-param constructor to pass their own style.
Let’s go back to our TextInputEditText
and take a look at its constructors, especially the second one:
public TextInputEditText(Context context, AttributeSet attrs) {
this(context, attrs, attr.editTextStyle);
}
This is exactly what is happening there — it passes a android.support.design.R.attr.editTextStyle
style, which we are loosing, when calling the three-param constructor by ourselves.
Step 4: Fixing
From the implementation of TextInputEditText we know that it passes android.support.design.R.attr.editTextStyle
as a third parameter, so we can do a fix by setting it as our default value of the defStyleAttr
param:
class CustomTextInputEditText @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.support.design.R.attr.editTextStyle
) : TextInputEditText(context, attrs, defStyleAttr)
From now on, everything will work as planned unless… the constructor implementation of TextInputEditText
in the Design library will change, for example by passing some other style there.
Another example. The subclassed component we are using, that worked perfectly fine, suddenly looks different than in other parts of our app, because it’s new version started passing a style, and we are subclassing it in one place only.
How to stay safe?
Just do not use @JvmOverloads
when it can be potentially risky. Implement all constructors and write some explanation comment so it could be understood in future.
Some final notes
I’ve picked TextInputEditText
as an example but the same situation will occur with Button
, EditText
, RadioButton
, Switch
, and many other components.
You can find an example implementation showcasing those issues, at my Github repo.
A key takeaway from this article — if you are getting some unexpected styling issues with a subclassed view, it’s worth to check if you are using @JvmOverloads. Maybe there’s the bug.
Please let me know if you have any thoughts on this topic.
Thanks for reading!