This article is published simultaneously on my wechat official account. You can follow it by scanning the QR code at the bottom of the article or searching Guo Lin on wechat. The article is updated every weekday.

Probably one of my favorite Kotlin features on Android is the Kotlin-Android-Extensions extension.

This is not an exaggeration, because in the past, when developing Android applications in Java, we had to write a lot of findViewById, which was boring and meaningless.

While there are third-party libraries such as ButterKnife that simplify the use of findViewById, ButterKnife still requires annotations to bind controls to resource ids, which is not very convenient.

With the kotlin-Android-Extensions extension, this situation has completely changed. Instead of writing cumbersome findViewById code, it can be written in a very simple way.

For example, here’s a layout file called activity_main.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/viewToShowText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>
Copy the code

Very simply, there is only one TextView control in the layout file, and its ID is viewToShowText.

So, if I want to set the contents of the TextView control in the MainActivity, using the Java language I would normally write:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView viewToShowText = findViewById(R.id.viewToShowText);
        viewToShowText.setText("Hello"); }}Copy the code

As you can see, we first get an instance of the TextView control using the findViewById() function, and then call the setText() function to set it to Hello.

The findViewById() function is a pain in the neck, and we’re just getting an instance of the control, so it might not be obvious. If you’re trying to get 10 or even 100 instances of a control and you have to go through findViewById for each one of them, you’re going to get crazy.

So how do you solve this problem in Kotlin? With the Kotlin-Android-Extensions plug-in, we can use the following code to do the same:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewToShowText.text = "Hello"}}Copy the code

As you can see, instead of calling the findViewById() function to get an instance of the control, you can simply call the ID name defined in the XML for the control to set what it displays.

This is done automatically by the Kotlin-Android-Extensions plugin, which helps reduce a lot of trivial code.

However, it was abandoned

A few months ago, a friend of mine asked me to update Android Studio 4.1 and found that the Kotlin-Android-Extensions extension was no longer automatically installed by Android Studio. You need to manually add the plugin to use it. Is it no longer recommended by Google?

At the time, I said, “No way, this plugin works so well, and Kotlin is a future Google technology, maybe it’s just a bug in Android Studio 4.1.”

However, it wasn’t long before I was hit in the face. One day I updated the Gradle version of my project to the latest, and when I was building the project I found this warning:

Google clearly tells us that the Kotlin-Android-Extensions plugin is deprecated and that ViewBinding is recommended instead.

I’m a little annoyed at the frequency with which Google is iterating on this technology. If the Kotlin-Android-Extensions extension is Google’s main technology, it should have a longer life, or it shouldn’t be integrated into Android Studio by default. I made a lot of use of this technique in my new book, Line 1, 3rd Edition, which came out last year.

However, the good news is that ViewBinding is not complicated and it is easy to switch to a ViewBinding from the Kotlin-Android-Extensions plugin. This article is another DLC for Line 1, Version 3. How to use ViewBinding to replace the Kotlin-Android-Extensions plugin.

Why was it abandoned

Before I start with ViewBinding, I’d like to discuss why the Kotlin-Android-Extensions plug-in was deprecated.

The frequency with which Google iterates on technology often leaves us dead, but there’s no way Google is going to scrap a major technology for no reason, so there’s definitely a problem with the Kotlin-Android-Extensions extension.

So what’s the problem?

One drawback that comes to mind is that the Kotlin-Android-Extensions plug-in only supports Kotlin, not Java. Of course, I don’t think this is the main reason, because now Google is developing all kinds of new technologies that are fully compatible with Kotlin and don’t think much about Java anymore, such as coroutines, Jetpack Compose, etc.

So what’s the main reason? This may be explained by the implementation of the Kotlin-Android-Extensions plugin. We have just seen the code using the Kotlin-Android-Extensions plugin. It is very simple:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewToShowText.text = "Hello"}}Copy the code

So why does this code work?

To see the Kotlin Bytecode for this code, go to Tools > Kotlin > Show Kotlin Bytecode in the navigation bar at the top of Android Studio. Then Decompile the bytecode into Java code by clicking the Decompile button in the pop-up window.

To make it easier to read, I have cleaned up the decompiled code as follows:

public final class MainActivity extends AppCompatActivity {
   private HashMap _$_findViewCache;

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(1300023);
      TextView var10000 = (TextView)this._$_findCachedViewById(id.textView);
      var10000.setText((CharSequence)"Hello");
   }

   public View _$_findCachedViewById(int var1) {
      if (this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }
      View var2 = (View)this._$_findViewCache.get(var1);
      if (var2 == null) {
         var2 = this.findViewById(var1);
         this._$_findViewCache.put(var1, var2);
      }
      returnvar2; }}Copy the code

As you can see, the Kotlin-Android-Extensions plugin will actually generate a _$_findCachedViewById() function for you (this is a strange way of naming it so it doesn’t conflict with the developer-defined function name). This function first attempts to fetch the control instance cache for the resource ID parameter passed in from a HashMap, and if it is not already cached, the findViewById() function is called to find the control instance and write it to the HashMap cache. The next time the same control instance is fetched, it can be fetched directly from the HashMap cache.

This is how the Kotlin-Android-Extensions plugin works, and it’s actually quite simple.

However, this realization principle also exposes some problems.

For example, each Activity needs to use an extra HashMap data structure to store all instances of the controls, adding some memory overhead.

Also, although HashMap is an O(1) time complex data structure, this is only a theoretical time complexity, and the actual call is certainly not as fast as accessing the control instance directly, so the Kotlin-Android-Extensions extension also reduces the efficiency of the application.

Most importantly, these are black boxes for most developers, and people using the Kotlin-Android-Extensions plugin may not be aware of these hidden “holes”, which will become more obvious when we introduce RecyclerView Adapter later.

Whether or not the above analysis isn’t enough of a reason to scrap the Kotlin-Android-Extensions plug-in, it is. The next step is to learn how to use ViewBinding to replace the kotlin-Android-Extensions extension. Rest assured, this is not a difficult matter.

What is a ViewBinding

ViewBinding as a whole is very simple, and it has only one purpose: to avoid writing findViewById, which is quite different from its very complex sibling, DataBinding.

There are two things to be aware of when using a ViewBinding. First, make sure your Android Studio is version 3.6 or higher. Second, add the following configuration to your project’s build.gradle module:

android {
    ...
    buildFeatures {
        viewBinding true}}Copy the code

This completes the preparatory work. Next, I will discuss the usage of ViewBinding from the aspects of Activity, Fragment, Adapter and introduction of layout.

Use a ViewBinding in your Activity

Once ViewBinding is enabled, Android Studio automatically generates a Binding class for each layout file we write.

The Binding class is named after the layout file is humped and ends with a Binding.

For example, if we defined an activity_main.xml layout, the Binding class for it would be an ActivityMainBinding.

Of course, if there are layout files for which you do not want to generate a Binding class, you can add the following declaration at the root of the layout file:

<LinearLayout
    xmlns:tools="http://schemas.android.com/tools"
    .
    tools:viewBindingIgnore="true">.</LinearLayout>
Copy the code

Let’s take a look at how to use a ViewBinding to set the contents of a TextView in the MainActivity as follows:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.textView.text = "Hello"}}Copy the code

ViewBinding is that simple to use. This layout is loaded by calling the Binding class of the activity_main.xml layout file, the inflate() function of the ActivityMainBinding, which takes a LayoutInflater parameter, This is available directly in the Activity.

The next step is to get an instance of the root element in activity_main.xml by calling the getRoot() function of the Binding class, and an instance of the element with the id textView by calling the getTextView() function.

Obviously, we should pass an instance of the root element into the setContentView() function so that the Activity can successfully display the contents of the activity_main.xml layout. Then get an instance of the TextView control and give it the text to display.

Of course, if you need to operate on the control outside of onCreate(), declare the binding variable as a global variable, like this:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.textView.text = "Hello"}}Copy the code

Note that variables declared by Kotlin must be initialized at the same time they are declared. We can’t declare a global binding variable and initialize it at the same time, so we use the lateInit keyword to lazily initialize the binding variable.

The example I’ve given here is very simple, but the way ViewBinding is used is pretty much the same, and once you’ve mastered this set of rules you can basically follow them.

Use a ViewBinding in the Fragment

Let’s learn how to use ViewBinding in fragments. This section is also very simple, because using a ViewBinding in a Fragment is basically the same as using a ViewBinding in an Activity.

I’m going to show you how to use ViewBinding in fragments and activities in code.

If we have a layout file called fragment_main.xml, then when ViewBinding is enabled, a corresponding FragmentMainBinding class will be generated.

If we wanted to display the layout in the MainFragment, we could write:

class MainFragment : Fragment() {

    private var _binding: FragmentMainBinding? = null

    private val binding get() = _binding!!

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View {
        _binding = FragmentMainBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView(a) {
        super.onDestroyView()
        _binding = null}}Copy the code

The actual logic of this code is much less complicated than it looks.

The core logic is still to call the FragmentMainBinding’s inflate() function to load the fragment_main.xml layout file, but since this is in the Fragment, we use the overloading of the inflate() function with three parameters, This is the same way we normally load layout files in fragments.

The next difference is that since we loaded the layout in the onCreateView() function, we should leave the binding variable empty in the corresponding onDestroyView() function, This ensures that the binding variable’s valid life cycle is between onCreateView() and onDestroyView().

The Kotlin null-type system, however, makes it necessary to write some odd-looking extra code to do this simple thing. I won’t introduce the Kotlin empty type system here, but if you don’t know it, you can refer to chapter 2 of The first Line of code, 3rd edition.

Well, this is one of the few places WHERE I admit that Java is more compact than Kotlin, because using Java code to do the same thing is simply by writing:

public class MainFragment extends Fragment {
    
    private FragmentMainBinding binding;

    @Override
    public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        binding = FragmentMainBinding.inflate(inflater, container, false);
        return binding.getRoot();
    }

    @Override
    public void onDestroyView(a) {
        super.onDestroyView();
        binding = null; }}Copy the code

Both pieces of code do exactly the same thing and mean exactly the same thing, but obviously the Java version is a little easier to understand.

Use ViewBinding in Adapter

Next, let’s take a look at a more interesting scenario where ViewBinding is used in Adapter and where the Kotlin-Android-Extensions plugin was most misused.

I believe that every Android developer has used RecyclerView, have also written Adapter, Adapter is actually a very good test of a developer’s skills.

I was asked a long time ago in the interview why we need to write ViewHolder in the ListView Adapter (at that time, there was no RecyclerView). The answer is that you don’t have to call findViewById() frequently while the list is scrolling, thereby reducing some unnecessary performance cost.

RecyclerView integrates the common best practices of ListView directly as the default implementation, so whenever we use RecyclerView, we must write ViewHolder.

However, some friends have some misuses here, and I will explain them through a specific example.

Suppose we define fruit_item. XML as the RecyclerView child’s layout:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp">

    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp" />

    <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:layout_marginTop="10dp" />

</LinearLayout>
Copy the code

Then write the following RecyclerView Adapter to load and display the subitem layout:

class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
        val fruitName: TextView = view.findViewById(R.id.fruitName)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val fruit = fruitList[position]
        holder.fruitImage.setImageResource(fruit.imageId)
        holder.fruitName.text = fruit.name
    }

    override fun getItemCount(a) = fruitList.size

}
Copy the code

This is a way to compare standards and traditions, and it can be said that there is no problem, the first line of code version 3 on RecyclerView part of the explanation is also used this way to write.

However, some readers have told me that declaring control variables in a ViewHolder and writing findViewById() is too complicated. I found an easier way to do this with the kotlin-Android-Extensions extension:

class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val fruit = fruitList[position]
        holder.itemView.fruitImage.setImageResource(fruit.imageId)
        holder.itemView.fruitName.text = fruit.name
    }

    override fun getItemCount(a) = fruitList.size

}
Copy the code

As you can see, there is no control declaration in the ViewHolder, just an empty ViewHolder defined. It can then be used by calling holder.itemView directly from onBindViewHolder(), followed by the name of the control ID.

This does simplify a lot of code, but is it the right way to write it?

If all you’re judging by is whether the code works, the answer is yes, it does. But this way of writing I can say is completely incorrect. Why? We just need to decompile this code to see what the Corresponding Java code looks like.

Again, I’ve simplified the code to make it easier to read, keeping only the key parts, as shown below:

public final class FruitAdapter extends Adapter {...public final class ViewHolder extends androidx.recyclerview.widget.RecyclerView.ViewHolder {
      public ViewHolder(@NotNull View view) {
         super(view); }}public void onBindViewHolder(@NotNull FruitAdapter.ViewHolder holder, int position) {
      Fruit fruit = (Fruit)this.fruitList.get(position); View var10000 = holder.itemView; ((ImageView)var10000.findViewById(id.fruitImage)).setImageResource(fruit.getImageId()); var10000 = holder.itemView; TextView var4 = (TextView)var10000.findViewById(id.fruitName); var4.setText((CharSequence)fruit.getName()); }}Copy the code

If you noticed, the onBindViewHolder() function now calls findViewById() each time to retrieve the control instance, rendering the ViewHolder useless.

Therefore, this is a typical misuse of the Kotlin-Android-Extensions plugin in Adapter. It’s also a hidden trap, because if you don’t decompile the Kotlin code, you might not know that your ViewHolder is actually doing anything.

Now that we’re done with the kotlin-Android-Extensions’ “pit”, let’s look at how to use ViewBinding in Adapter. Remember that our goal is always not to write findViewById.

If you are already familiar with the use of ViewBinding in activities and fragments, you should now be able to follow the same path as using ViewBinding in Adapters.

Let’s take a look at the code first, and then I’ll explain it briefly:

class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {

    inner class ViewHolder(binding: FruitItemBinding) : RecyclerView.ViewHolder(binding.root) {
        val fruitImage: ImageView = binding.fruitImage
        val fruitName: TextView = binding.fruitName
    }

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

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val fruit = fruitList[position]
        holder.fruitImage.setImageResource(fruit.imageId)
        holder.fruitName.text = fruit.name
    }

    override fun getItemCount(a) = fruitList.size

}
Copy the code

The core of this code is basically in the onCreateViewHolder() function and ViewHolder.

First, we call the inflate() function of the FruitItemBinding in the onCreateViewHolder() function to load the fruit_item.xml layout file, exactly the same way ViewBinding is used in the Fragment.

Next, you need to modify the ViewHolder so that its constructor accepts the FruitItemBinding parameter. Note, however, that the parent of ViewHolder, RecyclerView.ViewHolder, will only accept arguments of type View, So we need to call binding.root to get an instance of the root element in fruit_item. XML and pass it to recyclerView.viewholder.

Instead of using the findViewById() function to find the control instance, we can call binding.fruitimage and binding.fruitName to refer directly to the instance of the corresponding control.

That’s how ViewBinding is used in Adapter.

Use a ViewBinding for the incoming layout

There is another special case for using ViewBinding, and that is how to use ViewBinding for incoming layouts.

There are two common ways to introduce layouts, include and merge. About these two ways of use and difference, I in Android best performance practices (four) – layout optimization skills this article has a more detailed explanation, do not understand the friends can go to reference.

Let’s start by learning how to use ViewBinding in include and Merge layouts, respectively.

So let’s start with include, which is an easy case. Let’s say we have the following titlebar.xml layout that we want to introduce as a generic layout into each layout:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
 
    <Button
        android:id="@+id/back"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:text="Back" />
 
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Title"
        android:textSize="20sp" />
 
    <Button
        android:id="@+id/done"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:text="Done" />
 
</RelativeLayout>
Copy the code

So if I wanted to introduce this layout in activity_main.xml, I would just write:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
 
    <include 
        layout="@layout/titlebar" />.</LinearLayout>
Copy the code

This does introduce titlebar.xml into the activity_main.xml layout, but the problem is that you will find that the ViewBinding is not associated with the controls in titlebar.xml.

So how to solve this problem? We simply add an ID to the imported layout when we include it, like this:


      
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include 
        android:id="@+id/titleBar"
        layout="@layout/titlebar" />.</LinearLayout>
Copy the code

Then, in MainActivity, we can reference the control defined in titlebar.xml by writing it as follows:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.titleBar.title.text = "Title"
        binding.titleBar.back.setOnClickListener {
        }
        binding.titleBar.done.setOnClickListener {
        }
    }

}
Copy the code

Now let’s look at merge. The biggest difference between Merge and include is that the layout introduced by using the Merge tag can in some cases reduce the nesting of a layer of layout, and less nesting of layout usually means more efficiency.

For example, we make the following changes to titlebar.xml:

<merge xmlns:android="http://schemas.android.com/apk/res/android">
 
    <Button
        android:id="@+id/back"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:text="Back" />

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Title"
        android:textSize="20sp" />

    <Button
        android:id="@+id/done"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:text="Done" />
 
</merge>
Copy the code

As you can see, the outermost layout uses the merge tag, which means that when you include the layout anywhere, the contents of the merge tag are filled directly into the include location without adding any additional layout structure.

Unfortunately, if written this way, the application will crash. Because the merge tag is not a layout, we cannot give it an ID when we include it as we did earlier.

So how do you use ViewBinding in this case? First, to avoid crashes, we should remove the id specified when the layout was introduced in activity_main.xml, as shown below:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include
        layout="@layout/titlebar" />

</LinearLayout>
Copy the code

Then modify the code in MainActivity as follows:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var titlebarBinding: TitlebarBinding

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        titlebarBinding = TitlebarBinding.bind(binding.root)
        setContentView(binding.root)
        titlebarBinding.title.text = "Title"
        titlebarBinding.back.setOnClickListener {
        }
        titlebarBinding.done.setOnClickListener {
        }
    }

}
Copy the code

As you can see, here we define a titlebarBinding variable again. TitlebarBinding is obviously the Binding class that Android Studio automatically generates from our titlebar.xml layout file.

In the onCreate() function, we call the titlebarbinding.bind () function to associate the titlebar.xml layout with the activity_main.xml layout.

The next step is to use the titlebarBinding variable directly to refer to the individual controls defined in titlebar.xml.

Well, that’s about all there is to say about ViewBinding, at least I can’t think of any more uses, and this article will cover all ViewBinding related issues you might encounter in your work.

On the other hand, if you want to learn about Kotlin and the latest on Android, check out my new book, Line 1, Version 3. Click here for details.


Pay attention to my technical public account “Guo Lin”, there are high-quality technical articles pushed every working day.