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@JvmOverloadsannotations

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,TextInputEditTextThe three-argument constructor for theandroid.support.design.R.attr.editTextStyleStyle 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@JvmOverloadsIt’s worth it. Maybe that’s what caused the mistake.