Translator: Still fantexixi
When programming with Kotlin, it is often possible to combine multiple constructors into a single constructor using the @jvMoverloads annotation, especially when customizing views by inheriting Android Views. Most of the time, this works fine, but sometimes it can cause some unexpected problems.
Let’s look at the definition of @jvmoverloads:
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.
What does that mean? Explain:
Tell the Kotlin compiler to generate overloaded functions for @jvMoverloads’ methods with default arguments. If a method has N arguments and M of them have default values, it will generate M overloads: the first one takes n-1 arguments (except the last one takes default values), the second one takes n-2 arguments, and so on.
Sounds good, so we often call these 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)
}
Copy the code
Merge into a constructor:
class CustomLinearLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr)
Copy the code
Step 0: Problems arise
Let’s start by looking at the TextInputEditText control in the Design library,
In our custom class, as follows:
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)
}
Copy the code
The above code can be replaced with a constructor:
class CustomTextInputEditText @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : TextInputEditText(context, attrs, defStyleAttr)
Copy the code
This is the code that Android Studio automatically generates for us
Next we write an Activity that has two CustomTextInputEditText controls, the first containing all three constructors and the second containing the @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)
Copy the code
Will they eventually behave the same?
[image-465510-1568899351475] [image-465510-1568899351475]
As you can see, the second control using the @jvMoverloads annotation is not working.
What happened? Why are there style issues?
Step 1: Understand@JvmOverloads
annotations
Let’s revisit the definition of JvmOverloads for a second. We know that the Kotlin compiler generates two overloads (N = 3 and M = 2 in our example). So we end up with three similar constructors:
@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);
}
Copy the code
So in our custom class, we always call the CustomTextInputEditText three-argument constructor.
Step 2: Understand the View constructor
Now let’s focus for a moment on the View constructor.
When the View is loaded from the XML file, its second constructor is called. This constructor then calls the three-argument constructor.
public View(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
Copy the code
If 0 is usually the third argument, why does the three-argument constructor exist at all? The answer is in the documentation.
The View constructor allows subclasses to use their own base styles when loading.
Classes that inherit from a View can pass in their own styles to modify all the basic View properties.
Easy. Now let’s see why this went wrong.
Step 3: Know what went wrong
At this point, from step 1, we know that thanks to the @jvMoverloads annotation, we always call a three-parameter constructor, and from step 2, classes that inherit from View can use this three-parameter constructor to pass in their own styles.
Let’s go back to the TextInputEditText constructor, especially the second one:
public TextInputEditText(Context context, AttributeSet attrs) {
this(context, attrs, attr.editTextStyle);
}
Copy the code
That’s where the problem lies,TextInputEditText
The three-argument constructor for theandroid.support.design.R.attr.editTextStyle
Style when we use@JvmOverloads
, the default parameter is passed0
, so the original style is lost.
Step 4: Fix the problem
From TextInputEditText implementation we know, the android support. The design. The state Richard armitage TTR event. EditTextStyle as its third parameter passing, So we can fix this by setting it to the default value of defStyleAttrparam:
class CustomTextInputEditText @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.support.design.R.attr.editTextStyle
) : TextInputEditText(context, attrs, defStyleAttr)
Copy the code
Now, everything is working fine unless…… The constructor implementation of TextInputEditText will change, for example by passing other styles through it.
As another example, the subclass component we’re using that was working fine suddenly looks a little different from the rest of our app, because the new version of it starts passing styles, and we only inherit it in this one place.
How to stay safe?
Don’t use @jvmoverloads when it’s risky to do so, implement all constructors instead, just write some comments as you implement all constructors so you can understand them in the future.
Some final notes
This article picks out TextInputEditText as an example, but the same will happen with buttons, EditText, RadioButton, Switch, and many other components.
You can find sample implementations that demonstrate these issues in my Github repository.
An important point of this article – if you are having problems with the display style of some components when you customize them using inherited Views, check if you are using them@JvmOverloads
It’s worth it. Maybe that’s what caused the mistake.