Last week I wrote about some strange breakpoint behavior. This time I want to share a cautionary tale about using
I was reviewing a PR last week, and came across something that stirred up a memory of something I had read a while back. This PR had defined a custom view class in Kotlin, and the author had used the
@JvmOverloads annotation on the constructor.
Here's an example class that demonstrates the code that was written:
Whoa - what happened? Why does the "JVM Overloads Button" look different?
The answer has to do with Android's default styling attributes, and is directly related to the use of the
Let's take a step back - what are all of these arguments in the Button's constructor to begin with? Android's official documentation on constructors helps explain:
The first argument is always required - the context the view should be created in. The 2nd argument (the attributes) are what are passed to your view when it is inflated from XML. The 3rd argument defines what the default styling is for your view (this traverses the theme hierarchy). The 4th argument (which oddly isn't supplied by Android Studio's code generation) specifies what the default style resource is.
What happens when you define
@JvmOverloads is that Android's view system will call the two-arg version of the constructor, but the 3rd argument will use the default value provided - which is
0. The key difference here is that this value is different than what is passed in the default 2-argument constructor for the Button class in Android:
com.android.internal.R.attr.buttonStyle. This is a private constant that we don't have access to - but it's definitely not 0.
Therefore, we end up with a problem when using this view in XML with the default styling - this default style is omitted and we're left with the difference in appearance you see above. You may not be using default styling in your app, but this can cause unexpected issues that may take a long time to track down.
But, looking at the other Buttons in this layout, clearly it's possible to define custom views that properly respect these defaults. So how do we do that?
Let's first look at the "full constructor" version:
This definitely looks a little messier, but at least it functions correctly.
We discussed this as a team, and came up with this solution that allows us to remove one of the constructors and eliminate some boilerplate:
In this version, we define
@JvmOverloads only on the two-argument constructor, and then explicitly define the 3rd and 4th ones. This allows us to combine the 1- and 2-arg constructors (because these are safe to combine), but still have working behavior for default themes. In this simple example, you can actually make this even more concise by removing the 3- and 4-arg constructors entirely. This may not be an option in your code if you have other things you need to initialize, but it does work here.
So, what are the important takeaways?
- Understand what the different view constructors are, and how they behave: https://blog.danlew.net/2016/07/19/a-deep-dive-into-android-view-constructors/
- Unless you really know what you are doing, don't use
@JvmOverloadson the 3- or 4-argument constructors for Android view classes.
- Educate your workforce - make sure everyone knows about this issue and calls it out in PR reviews, or take it one step further and write a lint rule to check for this automatically!
If you'd like to play with this some more yourself, check out the sample project repo:
Update on 2/21/2020:
I have filed an official Android Studio bug report asking them to improve the way “quick fix” works for generating view constructors (based on this tidbit). Please ⭐️ this issue if it’s something you’d like to see fixed as well! https://issuetracker.google.com/issues/149986188
I hope you learned something that helps you avoid problems in the future! How does your team handle defining custom views and their constructors? Let me know in the comments below! And, please follow me on Medium if you're interested in being notified of future tidbits.
Interested in joining the awesome team here at Intrepid? We're hiring!
This tidbit was delivered on February 14, 2020.