Kotlin Android Extensions: Say goodbye to findViewById (KAD 04)
Antonio Leiva Gordillo
Kotlin&Android Google Developer Expert ┃ Formador Android certificado por JetBrains @DevExpert.io┃Speaker Internacional┃Ayudo a empresas y desarrolladores a dominar Kotlin y las mejores técnicas de calidad de software??
If you’ve been developing Android Apps for some time, you’re probably already tired of working with findViewById in your day-to-day life to recover views. Or maybe you gave up and started using the famous Butterknife library. If that’s your case, then you’ll love Kotlin Android Extensions.
Kotlin Android Extensions: What’s this?
Kotlin Android Extensions are another Kotlin plugin that is included in the regular one, and that will allow recovering views from Activities, Fragments, and Views in an amazing seamless way.
The plugin will generate some extra code that will allow you to access views in the layout XML, just as if they were properties with the name of the id you used in the layout definition.
It also builds a local view cache. So the first time a property is used, it will do a regular findViewById. But next times, the view will be recovered from the cache, so the access will be faster.
How to use them
Let’s see how easy it is. I’ll do this first example with an activity:
Integrating Kotlin Android Extensions in our code
Though the plugin comes integrated into the regular one (you don’t need to install a new one), if you want to use it you have to add an extra apply in the Android module:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
And that’s all you need. You’re now ready to start working with it.
Recovering views from the XML
From this moment, recovering a view is as easy as using the view id you defined in the XML directly into your activity.
Imagine you have an XML like this one:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/welcomeMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Hello World!"/>
</FrameLayout>
As you can see, the TextView has welcomeMessage id.
Just go to your MainActivity and write it:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
welcomeMessage.text = "Hello Kotlin!"
}
To be able to use it, you need a special import (the one I write below), but the IDE is able to auto-import it. Couldn’t be easier!
import kotlinx.android.synthetic.main.activity_main.*
As I mentioned above, the generated code will include a view cache, so if you ask the view again this won’t require another findViewById
Let’s see what’s behind the scenes.
The magic behind Kotlin Android Extensions
When you start working with Kotlin, it’s really interesting to understand the bytecode that is being generated when you use one feature or another. This will help you understand the hidden costs behind your decisions.
There’s a powerful action below Tools –> Kotlin, called Show Kotlin Bytecode. If you click here, you’ll see the bytecode that will be generated when the class file you have opened is compiled.
The bytecode is not really helpful for most humans, but there’s another option here: Decompile.
This will show a Java representation of the bytecode that is generated by Kotlin. So you can understand more or less the Java equivalent code to the Kotlin code you wrote.
I’m going to use this on my activity, and see the code generated by the Kotlin Android Extensions.
The interesting part is this one:
private HashMap _$_findViewCache;
...
public View _$_findCachedViewById(int var1) {
if(this._$_findViewCache == null) {
this._$_findViewCache = new HashMap();
}
View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
if(var2 == null) {
var2 = this.findViewById(var1);
this._$_findViewCache.put(Integer.valueOf(var1), var2);
}
return var2;
}
public void _$_clearFindViewByIdCache() {
if(this._$_findViewCache != null) {
this._$_findViewCache.clear();
}
}
Here it is the view cache we were talking about.
When asked for a view, it will try to find it in the cache. If it’s not there, it will find it and add it to the cache. Pretty simple indeed.
Besides, it adds a function to clear the cache: clearFindViewByIdCache. You can use it for instance if you have to rebuild the view, as the old views won’t be valid anymore.
Then this line:
welcomeMessage.text = "Hello Kotlin!"
is converted into this:
((TextView)this._$_findCachedViewById(id.welcomeMessage)).setText((CharSequence)"Hello Kotlin!");
So the properties are not real, the plugin is not generating a property per view. It will just replace the code during compilation to access the view cache, cast it to the proper type and call the method.
Kotlin Android Extensions on fragments
This plugin can also be used on fragments.
The problem with fragments is that the view can be recreated but the fragment instance will be kept alive. What happens then? This means that the views inside the cache would be no longer valid.
Let’s see the code it generates if we move it to a fragment. I’m creating this simple fragment, that uses the same XML I wrote above:
class Fragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment, container, false)
}
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
welcomeMessage.text = "Hello Kotlin!"
}
}
In onViewCreated, I again change the text of the TextView. What about the generated bytecode?
Everything is the same as in the activity, with this slight difference:
// $FF: synthetic method
public void onDestroyView() {
super.onDestroyView();
this._$_clearFindViewByIdCache();
}
When the view is destroyed, this method will call clearFindViewByIdCache, so we are safe!
Kotlin Android extensions on a Custom View
It will work very similarly on a custom view. Let’s say we have a view like this:
<merge xmlns:android="https://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/itemImage"
android:layout_width="match_parent"
android:layout_height="200dp"/>
<TextView
android:id="@+id/itemTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</merge>
I’m creating a very simple custom view and generate the constructors with the new intent that uses @JvmOverloads annotation:
class CustomView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
init {
LayoutInflater.from(context).inflate(R.layout.view_custom, this, true)
itemTitle.text = "Hello Kotlin!"
}
}
In the example above, I’m changing the text to itemTitle. The generated code should be trying to find the view from the cache. It doesn’t make sense to copy all the same code again, but you can see this in the line that changes the text:
((TextView)this._$_findCachedViewById(id.itemTitle)).setText((CharSequence)"Hello Kotlin!");
Great! We are only calling findViewById the first time in custom views too.
Recovering views from another view
The last alternative Kotlin Android Extensions provide is to use the properties directly from another view.
I’m using a layout very similar to the one in the previous section. Imagine that this is being inflated in an adapter for instance.
You can also access the subviews directly, just using this plugin:
val itemView = ...
itemView.itemImage.setImageResource(R.mipmap.ic_launcher)
itemView.itemTitle.text = "My Text"
Though the plugin will also help you fill the import, this one is a little different:
import kotlinx.android.synthetic.main.view_item.view.*
There are a couple of things you need to know about this:
- In compilation time, you’ll be able to reference any view from any other view. This means you could be referencing to a view that is not a direct child of that one. But this will fail in execution time when it tries to recover a view that doesn’t exist.
- In this case, the views are not cached as it did for Activities and Fragments.
Why is this? As opposed to the previous cases, here the plugin doesn’t have a place to generate the required code for the cache.
If you again review the code that is generated by the plugin when calling a property from a view, you’ll see this:
((TextView)itemView.findViewById(id.itemTitle)).setText((CharSequence)"My Text");
As you can see, there’s no call to a cache. Be careful if your view is complex and you are using this in an Adapter. It might impact the performance.
Or you have another alternative: Kotlin 1.1.4
Kotlin Android Extensions in 1.1.4
Since this new version of Kotlin, the Android Extensions have incorporated some new interesting features: caches in any class (which interestingly includes ViewHolder), and a new annotation called @Parcelize. There’s also a way to customize the generated cache.
We’ll see them in a minute, but you need to know that these features are not final, so you need to enable them adding this to you build.gradle:
androidExtensions {
experimental = true
}
Using it on a ViewHolder (or any custom class)
You can now build a cache on any class in a simple way. The only required thing is that your class implements the interface LayoutContainer. This interface will provide the view that the plugin will use to find the subviews. Imagine we have a ViewHolder that is holding a view with the layout described in the previous examples. You just need to do:
class ViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView),
LayoutContainer {
fun bind(title: String) {
itemTitle.text = "Hello Kotlin!"
}
}
The containerView is the one that we are overriding from the LayoutContainer interface. But that’s all you need.
From that moment, you can access the views directly, no need of prepending itemView to get access to the subviews.
Again, if you check the code generation, you’ll see that it’s taking the view from the cache:
((TextView)this._$_findCachedViewById(id.itemTitle)).setText((CharSequence)"Hello Kotlin!");
I’ve used it here on a ViewHolder, but you can see this is generic enough to be used in any class.
Kotlin Android Extension to implement Parcelable
With the new @Parcelize annotation, you can make any class implement Parcelable in a very simple way.
You just need to add the annotation, and the plugin will do all the hard work:
@Parcelize
class Model(val title: String, val amount: Int) : Parcelable
Then, as you may know, you can add the object to any intent:
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra(DetailActivity.EXTRA, model)
startActivity(intent)
And recover the object from the intent at any point (in this case in the target activity):
val model: Model = intent.getParcelableExtra(EXTRA)
Customize the cache build
A new feature included in this experimental set is a new annotation called @ContainerOptions. This one will allow you to customize the way the cache is built, or even prevent a class from creating it.
By default, it will use a Hashmap, as we saw before. But this can be changed to use a SparseArray from the Android framework, which may be more efficient in certain situations. Or, if for some reason, you don’t want a cache for a class, you also have that option.
This is how it’s used:
@ContainerOptions(CacheImplementation.SPARSE_ARRAY)
class MainActivity : AppCompatActivity() {
...
}
Currently, the existing options are these:
public enum class CacheImplementation {
SPARSE_ARRAY,
HASH_MAP,
NO_CACHE;
...
}
Conclusion
You’ve seen how easy is to deal with Android views in Kotlin. With a simple plugin, we can forget about all that awful code related to view recovery after inflation. This plugin will create the required properties for us casted to the right type without any issues.
Besides, Kotlin 1.1.4 has added some interesting features that will be really helpful in some cases that were not previously covered by the plugin.
If you like what you’ve seen, I encourage you to sign up for my free training, where I’ll tell you everything you need to learn about how to create your own Android Apps in Kotlin from scratch.
Experienced Software Engineer || Tech Enthusiast || Exploring Complex Systems Design and Backend Engineering
5 年It's time to learn Jetpack Compose soon and say goodbye to xml-s :-)