Android extensions

Getting rid of boilerplate with Kotlin Android Extensions

Posted on by Andras Kindler

findViewById - probably the most common phrase in every Android codebase. And if you think about it, it's a bit of an overkill. After defining a view with an id and a class in xml, one has to look it up, and cast it, before starting to use it in code - the definition of boiler plate code. No wonder developers are trying to get rid of it with serious effort.

The purpose of Kotlin Android Extensions is to completely eliminate the phrase above. Basically after applying the plugin, every view can be referenced by their id supplied in the xml layouts, without casting, as their respective types. That's it.

How to use

The Android Extensions are part of the Kotlin plugin for Android studio, no install necessary. The first step is to apply the plugin in the build.gradle script:

apply plugin: 'kotlin-android-extensions'

Then import every view in a layout with the following line:

import kotlinx.android.synthetic.main.<layout>.*

Where <layout> is the file name of the xml layout. So for example the import statement for activity_login.xml would be import kotlinx.android.synthetic.main.activity_login.*.

And now you can access each view by their ids supplied in the xml. For example after declaring the following Button in XML:

<Button
    android:id="@+id/loginButton"
    ...
    />

It can be accessed in code like this, without any preceding ceremony:

loginButton.text = "Login"

That's all. The Kotlin Android Extensions can be used anywhere where xml layouts are involved: activities, fragments, custom view subclasses, viewholders, etc.

Under the hood

Kotlin Android Extensions is a compiler plugin that offers a convenient way of accessing views defined in XML via a property-like syntax. But even though the views look like real class properties in the Kotlin code, they are synthetic, meaning accessing them means calling a hidden method in the background.

After importing a layout, the compiler adds a caching function and a HashMap holding the views to the class. This guarantees lazy access: when a view is used in code, the caching function looks it up in the HashMap, and tries to add it to the cache if not present (by extracting the view with findViewById()).

The decompiled bytecode contains a HashMap object for caching the views.

private HashMap _$_findViewCache;

Access is managed via the findCachedViewById() function.

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;
}

All of the above is hidden from the developer, everything happens automatically in the background. It is worth knowing that writing loginButton.text = "Login" in Kotlin translates to the following statement:

((Button)this._$_findCachedViewById(id.loginButton)).setText((CharSequence)"Login");

Also, a second function is added to the class, for clearing the HashMap, to rebuild the cache for whatever reason. Also, when using the Kotlin Android Extensions with fragments, this is called automatically in onDestroyView(), when the views are recreated but the fragment object stays intact, so the state of the cache becomes invalid. It can also be called manually with clearFindViewByIdCache():

public void _$_clearFindViewByIdCache() {
  if(this._$_findViewCache != null) {
    this._$_findViewCache.clear();
  }
}

Fine-tuning the cache

It is possible to with the @ContainerOptions annotation. The default setting uses a HashMap, which can be changed to a SparseArray (with the SPARSE_ARRAY parameter), or turned off entirely (NO_CACHE parameter).

An example:

@ContainerOptions(NO_CACHE)
class MainActivity : Activity() {
  ...
}

However, this is an experimental feature, the following flag has to be set to true in the build script:

androidExtensions {
    experimental = true
  }

Adapters and viewholders

While activities, fragments, and custom views work effortlessly, caching is not available outside those, so using the extensions with classes (adapters, viewholders) require a different approach. Basically what this means is after inflating a view, its subviews will be available in the same property-like way, but will not be cached. Taking a look at the decompiled bytecode reveals that every time a view is accessed, findViewById is called - the cache and the helper methods are not generated in these cases. This can make the extensions very inefficient with adapters.

The first solution is to wrap the itemviews into a custom view, so caching will work this way. This is a preferred solution anyways, so the LOC count can be kept to a minimum in the adapter subclass.

The second approach is experimental, meaning the flag above has to be set to true in the build script before using it. When using the extensions outside of an activity, fragment, or custom view, implementing the LayoutContainer interface will make caching available automatically. However, the container view will have to be passed as a constructor parameter.

Alternatives

The findViewById problem inspired many to come up with clever solutions.

Performance tradeoff

There's not much, really. Lazy access and caching guarantees efficiency, and the impact on the method count is subtle: every class is extended with two new functions.

Bottom line

We've been using the extensions in production for quite some time to our complete satisfaction, and we think currently this is the best solution for dealing with XML layouts. A more convenient way of accessing views, resulting in cleaner code, and zero performance drawbacks - there's no way we're going back to findViewById().