Referencing Views

Okay, it’s something we do with every view, we create a few elements and we link them to a controlling Activity, Fragment or View. Standardly we use findViewById, or maybe even Kotlin Synthetics, but how safe are these approaches?

In the beginning we used findViewById, this was a nice approach, before Android 8API 26, it returned a standard View which we had to cast, after Android 8API 26 we then were given a type that extended View, but this isn’t safe as it could return a null type if not available at the time of calling, which would result in a NullPointerException, which we obviously don’t want.

Next we were offered ButterKnife, personal opinion is that it was a great library, but I really didn’t think it was necessary so I won’t go into further details on this. On the back of that, it’s has now stopped being heavily developed and will probably be unsupported very shortly.

After this we were provided with Kotlin Synthetics which is actually really nice to use, very simple API, once a build has taken place, view ids are then referncable view static imports in classes. Whilst it’s nice and it’s easy to use, it doesn’t offer compile time safety as it’s extremely easy to import a view which isn’t actually part of the view you are working with. This can cause time wasted in assessing what has gone wrong. Very minimal build speed effects, can’t be used in Java.

Sometime around the same time as Kotlin Synthetics we were also introduced to Data Binding. A very powerful tool but has a fairly steep learning curve and does increase build times due to annotation processing. It’s offered by Google so is stable and has lots of support but still isn’t the simplest possible solution when aligning with various architecture patterns.

Then along came View Binding, the main character in this story. View Binding is offered by Google and was designed to create a null safe, simple solution that was wrote with build speeds in mind, therefore very minimal build speed increase.

View Binding theoretically creates a solution that has the best parts of all the previous solutions without importing any libraries or any overheads.

Let’s take a look at how it works.

build.gradle

android {
    ...
    viewBinding {
        enabled = true
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </com.google.android.material.appbar.AppBarLayout>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        app:srcCompat="@android:drawable/ic_dialog_email" />

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

</androidx.coordinatorlayout.widget.CoordinatorLayout>

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        bindView()
        setActionBar()
        addEventListeners()
    }

    private fun bindView() {
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
    }

    private fun setActionBar() {
        setSupportActionBar(binding.toolbar)
    }

    private fun addEventListeners() {
        binding.fab.setOnClickListener { fab ->
            Snackbar.make(fab, "I'm a snackbar", Snackbar.LENGTH_LONG)
        }
    }
}

So step by step

  • We add to the project level build.gradle we set viewbinding to true. This will tell gradle to generate a Binding class for each XML. Each Binding class will be created in Camal-Case naming , for example activity_main.xml = ActivityMainBinding. If we want to skip an XML file we simple add the flag tools:viewBindingIgnore=”true” to the parent view of that XML.
  • Next, create your view, giving every view that you want to reference an id. The parent view will always be created and called rootView.
  • Next we create our Activity, we create a lateinit propery that will be our binding. In our example above we use private lateinit var binding: ActivityMainBinding. This will ensure the entire Activity has null safe references to the view.
  • We then simple call the following

 binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

That’s it done! You now have a null safe reference to all yours views in the supporting XML. Your activity has it’s contentView satisfied and you’ve not increased build times. What a simple approach!

via GIPHY

If your curious as to what’s generated behind the scenes, and what this binding class is, then here it is.

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

  @NonNull
  public final FloatingActionButton fab;

  @NonNull
  public final Toolbar toolbar;

  private ActivityMainBinding(@NonNull CoordinatorLayout rootView,
      @NonNull FloatingActionButton fab, @NonNull Toolbar toolbar) {
    this.rootView = rootView;
    this.fab = fab;
    this.toolbar = toolbar;
  }

  @Override
  @NonNull
  public CoordinatorLayout 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.
    int id;
    missingId: {
      id = R.id.fab;
      FloatingActionButton fab = rootView.findViewById(id);
      if (fab == null) {
        break missingId;
      }

      id = R.id.toolbar;
      Toolbar toolbar = rootView.findViewById(id);
      if (toolbar == null) {
        break missingId;
      }

      return new ActivityMainBinding((CoordinatorLayout) rootView, fab, toolbar);
    }
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}
Tags:

Leave a Reply

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