Engineering

17 Dec 2021

Replacing Kotlin Synthetics with Android View Binding in RecyclerView

A chart showcasing 4 parts
A chart showcasing 4 parts
A chart showcasing 4 parts

Kotlin Android Extensions are officially deprecated. The biggest blow from that for most is the loss of Synthetics, which allowed you to replace calls to findViewById() with the view’s id from the layout file.

Synthetics had some great upsides

  1. No boilerplate code — Just apply the plugin in build.gradle and you’re good to go. From then on Android Studio would auto-complete the view’s id for you, and allow you to reference the correct type’s properties, as if by magic.

  2. Type safety — Under the hood, Synthetics generated an accompanying class for each layout file that contained references to all its views, with their correct types (most of the time)


It’s using findViewById()!

For the most part, Synthetics were quite safe to use in Activities and Fragments. However, my biggest issue with them was how tricky they were to use correctly in RecyclerViews and ViewHolders. Back in 2019 I suddenly had the urge to understand how Synthetics worked, so I picked some code at random, which happened to be one of my ViewHolders, and decompiled the byte code to Java:

public final class MeaninglessViewHolder extends ViewHolder {

    public final void bind(@NotNull SomeItem item) {
        ...
        TextView var2 = (TextView)var10000.findViewById(id.deceptive_text_view);
        ...
    }
}

Under the hood, the bind() method was using findViewById()! The exact reason we use ViewHolder, and why the pattern is baked into RecyclerView, is to avoid this. findViewById() does a view hierarchy traversal every time, and now we’re doing it every time we bind data to a view in a scrolling list, which is SLOW. I immediately showed my team, and all our future ViewHolders contained view properties that we initialised to the Synthetics value, which was just short-hand for findViewById, and re-introduced a lot of boilerplate, but at least the findViewById() calls were confined to the constructor:

public final class VerboseViewHolder extends ViewHolder {
    private final TextView annoyingTextView;

    public final void bind(@NotNull SomeItem item) {
        ...
        this.annoyingTextView.setText((CharSequence)item.getValue());
    }

    public VerboseViewHolder(@NotNull View itemView) {
        ...
        TextView var10001 = (TextView)itemView.findViewById(id.deceptive_text_view);
        ...
        this.annoyingTextView = var10001;
    }
}

I only researched the correct way to implement Synthetics in ViewHolders while researching this article, but it also has some boilerplate, and is still easy to mess up. You have to make your ViewHolder implement LayoutContainer, override a containerView property, oh and add androidExtensions { experimental true } to your build.gradle (which breaks Synthetics in flavor-specific layouts).

There’s also a gotcha to watch out for. This code results in findViewById() still being called:

fun bind(item: SomeItem) {
    itemView.deceptive_text_view.text = item.value
}

Apparently you have to do this instead:

fun bind(item: SomeItem) {
    deceptive_text_view.text = item.value
}

For Synthetics to cache the view lookup, you have to get rid of itemView. before the deceptive_text_view reference! This leads to the expected Java:

public final class RealViewHolder extends ViewHolder implements LayoutContainer {

    public final void bind(@NotNull SomeItem item) {
        ...
        TextView var10000 = (TextView)this._$_findCachedViewById(id.deceptive_text_view);
        ...
    }
}

What a headache! Thankfully, the extensions are deprecated, and all of this is a lot easier with the official replacement, Android View Binding.

Android View Binding

First, let’s go over how to set up View Binding, and then we will see how easy it is to use in ViewHolders.

To get started, remove kotlin-android-extensions from the plugins section in build.gradle, and if you had been following the LayoutContainer approach in ViewHolders, be sure to also remove androidExtensions { experimental true }. Then, add the below to the android block:

android {
    ...
    buildFeatures {
        viewBinding true
    }
}

Now every layout file will get an accompanying class generated for it, named PascalCasedLayoutFileBinding, e.g. ActivityMainBinding for a layout file named activity_main. This class will contain type-safe references to each view with an id (the ones without ids are ignored). You can also opt out of generating this class for a layout by adding tools:viewBindingIgnore="true" in the layout file’s root view.

The official documentation clearly describes how to use View Binding in Activities and Fragments, but it doesn’t mention how it should be used in RecyclerView Adapter and ViewHolder as a Synthetics replacement. Though not immediately obvious, it’s actually quite simple. Step one is to get an instance of the class. You do this by calling the binding class’s static inflate method in the adapter’s onCreateViewHolder method. In this case the item view’s layout file is called item_view so its binding class is ItemViewBinding:

class Adapter(private val items: List<SomeItem>) : RecyclerView.Adapter<RealViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RealViewHolder {
        val binding = ItemViewBinding.inflate(LayoutInflater.from(parent.context))
        return RealViewHolder(binding)
    }
    ...
}

As you can see above, we pass the binding instance to the ViewHolder’s constructor. This is so that the ViewHolder can access the binding’s view references. The binding instance effectively fulfills the ViewHolder’s “view holding” responsibility and the ViewHolder is just a wrapper for it:

The bind() method is identical to the Synthetics version, except for the word binding. It’s a lot more performant however, because it is just property access instead of a HashMap lookup in the best case, and a horribly slow view hierarchy traversal in the worst case.

Conclusion

And that’s how simple it is to use Android View Binding as a Kotlin Synthetics replacement in RecyclerViews. It’s safe, fast, just as convenient as Synthetics, and almost impossible to screw up. I hope this has been useful and helped clear up some confusion around using View Binding in RecyclerView.

Learn more

Explore our services

Our services are designed to bring your ideas to life.
Explore our digital services to learn more.
Our services are designed to bring your ideas to life. Explore our digital services to learn more.

Our Services

Our Services

Our Services

get started

Bring your ideas to life

We'd love to hear about your project and how we can help bring your ideas to life.

Book a Call

© 2024 Glucode. All rights reserved.

get started

Bring your ideas to life

We'd love to hear about your project and how we can help bring your ideas to life.

Book a Call

© 2024 Glucode. All rights reserved.

get started

Bring your ideas to life

We'd love to hear about your project and how we can help bring your ideas to life.

Book a Call

© 2024 Glucode. All rights reserved.