View / View Binding

View Binding: Internals

In September 2019 Google released Android Studio 3.6 Canary 11 to the Canary channel. This had an interesting new addition: View Binding. In this short series we’ll take a look at how it works, and what impact it may have on performance.

Often we need to interact with Views from our XML layouts in our Java or Kotlin code. The traditional approach to this is using findViewById() or, more recently for Kotlin, using synthetic view properties. Both of these have potential runtime performance issues, and can be error prone. There are also various third party solutions which address these issues with varying degrees of success. But we now have an official solution

View Binding is Google’s shiny new approach to this, and I think is pretty good. It is also really simple to implement and use. The official documentation explains everything you need to know to start using View Binding, and I do not feel that simply repeating all of that here adds any value to it, so instead we’ll look deeper. So if you’re unfamiliar with how to use View Binding, then I suggest you read the official documentation, and resume reading this post once you have.

Hopefully you now know how easy it is to set up and use View Binding in your project, but everyone I initially spoke to them about it had one of three questions (and sometimes a combination of them): “So how does it work?”; “What are the benefits?”; and “What impact will it have on our build speed”.

View Binding works in similar way to Data Binding and, in fact, is done as part of the Data Binding processor during the build. Whereas Data Binding needs to be explicitly enabled for each XML layout file, View Binding, once enabled, is done for every layout that is not explicitly disabled. It generates a binding class for each XML layout. The best way to understand how it works it to compare the XML layout the the generated code. Let’s look at a really simple layout:




    


We have a ConstraintLayout containing a single TextView – which is basically the default layout in the project that Android Studio created for me. The only change I made was to give the TextView and android:id attribute. At present it is necessary to add the tools:ignore="UnusedResources" attribute to the root ViewGroup because lint does not detect that this is actually referenced from the generated code, so we get lint warnings without it.

The binding class that gets generated looks like this:

// Generated by view binder compiler. Do not edit!
package com.stylingandroid.viewbinding.databinding;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.viewbinding.ViewBinding;
import com.stylingandroid.viewbinding.R;
import java.lang.NullPointerException;
import java.lang.Override;
import java.lang.String;

public final class ActivityMainBinding implements ViewBinding {
  @NonNull
  private final ConstraintLayout rootView;

  @NonNull
  public final TextView text1;

  private ActivityMainBinding(@NonNull ConstraintLayout rootView, @NonNull TextView text1) {
    this.rootView = rootView;
    this.text1 = text1;
  }

  @Override
  @NonNull
  public ConstraintLayout getRoot() {
    return rootView;
  }

  @NonNull
  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, null, false);
  }

  @NonNull
  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    View root = inflater.inflate(R.layout.activity_main, parent, false);
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }

  @NonNull
  public static ActivityMainBinding bind(@NonNull View rootView) {
    // The body of this method is generated in a way you would not otherwise write.
    // This is done to optimize the compiled bytecode for size and performance.
    String missingId;
    missingId: {
      TextView text1 = rootView.findViewById(R.id.text1);
      if (text1 == null) {
        missingId = "text1";
        break missingId;
      }
      return new ActivityMainBinding((ConstraintLayout) rootView, text1);
    }
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

There really isn’t anything complicated going on here other than the optimisations in the bind() method which are for performance reasons, and I’m not going to go in to bytecode level subtleties – I trust Jake Wharton‘s knowledge of Java bytecode in this. However, it is Java which may hurt the eyes of some who only Kotlin these days!

As to the benefits, let’s first consider runtime performance. One of the big issues with findViewById() is that it performs a top-down search of the View hierarchy until it finds a match. It is not optimised, and has no internal cache, so if we make the call that method twice in quick succession with the same View ID, then both invocations will require the same top-down search.

While the binding uses findViewById() internally, it caches the result, so it is only ever going to call findViewById() once for any View within the layout., and it does this up-front at the layout inflation phase. While this has an initial up-front cost, it means that there is no hidden cost of accessing the text1 field of a MainActivityBinding instance. While there are a very few corner cases where this might be marginally less efficient than calling findViewById() when needed, the efficiency delta will be pretty trivial, so for the vast majority of use-cases the View Binding approach will be more efficient at runtime.

One other benefit worth considering is that sometimes we encounter crashes using Kotlin synthetic view properties if we attempt to access them too early in the Android lifecycle before they have been initialised. While it is still necessary to perform the inflation of the layout and binding at the correct phase of the Android lifecycle, that initialisation will actually be done within the Activity or Fragment code making it much more visible precisely when in the lifecycle the initialisation is taking place. Potentially this makes it much easier to spot these kinds of errors.

Another important benefit is that of null safety. If the bind() method returns without throwing an exception then it is guaranteed that the fields representing the View instances will not be null. The only exception here is if you were to then programatically remove Views from the hierarchy. If you are going to do this, then it is probably best to disable View Binding for any layouts where this might happen to avoid any problems.

The improvements in runtime performance will incur a cost at build-time because it uses code generation. It will also increase your APK size and method count.

The cost to method count will be 1 class containing 6 methods for each XML layout file. As Dex method restrictions are no longer as much of a big deal as they once were, this isn’t much of an issue.

The impact of APK size is less predictable because the size of each generated class will depend upon the complexity of the layout – specifically how many Views exist with an id attribute. For the layout we looked at earlier the size of the class within the release APK is 552 bytes. For a more complex layout with 10 views of different types, this increased to 989 bytes. While neither of these are huge, the impact will depend on how many layouts you have within your project. For a project where I added an additional 1000 layouts each containing 10 child Views the release APK the APK increased from 3.9 MB to 4.1MB without proguarding. I think that few projects will contain 1000 XML layouts, but many will contain layouts rather more complex than having 10 child Views. However, a 200 KB increase in APK size isn’t a big deal, in my opinion.

In the concluding article in this series we’ll turn our attention to performance.

The source code for this article is available here.

© 2019 – 2020, Mark Allison. All rights reserved.

Copyright © 2019 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.