Image of an arrow

View Bindings in Android

Avatar

alussiercullen

Android development is always moving forward with new features to make building apps easier; UI construction with compose, dependency injection with Hilt, game development extensions, emoji compatibility libraries, the list goes on. New projects have no concerns leveraging these sorts of features. However, legacy projects have to find a balance when migrating to new software systems. At Savoir-faire Linux, we maintain a variety of android projects and among them is one we’ve been maintaining for more than half a decade now. Throughout the years, we’ve integrated many modernizations to the project; one of the best ones was migrating to view bindings.

The Problem

In the past with this project, the code for all our frontend classes looked something like this.

class CountActivity : AppCompatActivity() {

    private lateinit var btnDown: Button
    private lateinit var btnUp: Button
    private lateinit var tvCount: TextView
    private lateinit var llCount: LinearLayout

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        setContentView(R.layout.activity_count)

        btnDown = findViewById(R.id.btnSync)
        btnUp = findViewById(R.id.btnExport)
        tvCount = findViewById(R.id.tvCount)
        llCount = findViewById(R.id.llSync)
        ...
    }
    ...
}

For each of our frontend classes, we had a large block of assignments that we’d have to make using findViewById() to keep references for each UI element and more easily be able to interact with them. As we can see, this does not help with code brevity, which harms readability, and adds overhead on making any new frontend classes. Not to mention, findViewById() is not null safe or type safe.

The major issue with findViewById() null safety is that the id used exists in the global context of all layouts. It’s easy to accidentally target a layout outside the one you’re dealing with and produce a null pointer exception. On top of that, returns from findViewById() must be correctly cast to the relevant type for the view, making a cast to the wrong type another easy mistake.

We would preferably want some kind of solution that isn’t as cluttered and is more resistant to implementation errors.

View Bindings

The answer to our woes is view bindings. Any modern project is likely to already be using these, but legacy projects may not have migrated to them yet. If that’s the case for one of your projects, then you should. Perhaps it’s findViewById(), or perhaps your project is still using one of the deprecated alternatives such as Butterknife or Kotlin Synthetics (the recent release of Kotlin 1.8 has actually made the plugin completely unavailable). These other options are lacking compared to view bindings, sharing many of the same issues, as is briefly visualized in the following table.

View BindingfindViewById()ButterknifeKotlin Synthetics
Low Code Footprint✔️✔️
Isolated Layout IDs✔️
Null Safety✔️
Type Safety✔️✔️
Undeprecated✔️✔️

 

View bindings clearly come out as the best option.

In order to enable the use of view bindings, you must first edit the app level gradle file of your project to contain the following.

android {
    ...
    buildFeatures {
        viewBinding true
    }
}

Then in your activities, for example, you would set your content view and binding as follows.

class CountActivity : AppCompatActivity() {

    private lateinit var binding: ActivityCountBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityCountBinding.inflate(layoutInflater)
        setContentView(binding.root)
        ...
    }
    ...
}

Finally to access elements of the view you would simply do that through the binding.

fun updateCount(count: Int) {
    binding.tvCount.text = count.toString()
}

It’s that easy, but what about using them in contexts outside of activities?

Fragments

In fragments, there’s a little more work to do. A fragment’s lifecycle is such that the view is destroyed before the fragment itself is. Due to this, we want to be sure to null out the view binding of the fragment on view destruction in order to prevent memory leaks. This means our class would look something like the following.

class CountFragment : Fragment() {

    private var _binding: FragmentCountBinding? = null
    private val binding get() = _binding!!

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

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
        ...
    }
    ...
}

One thing to note is that the unlike the standard inflate, which returns the root view of the layout, the binding inflate function returns the binding so the root needs to be read from it for our onCreateView() return. Also, while it isn’t necessary, we define an auxiliary binding property to avoid having to write null checks everywhere. In fragments, accessing the view is always dangerous, regardless of how you implement it, so be sure to always deal with the view between the relevant lifecycle calls and never outside of that.

Recycler Adapters

In the case of recycler adapters and their view holders, we don’t have any special considerations for lifecycle. The recycler adapter manages multiple individual view holders and the view in each of the holders only exists as long as the holder itself does. We implement bindings in adapters as follows.

class CountAdapter() : RecyclerView.Adapter() {

    override fun onCreateViewHolder(parent: ViewGroup, position: Int): CountViewHolder {
        val binding = ViewHolderCountBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return CountViewHolder(binding)
    }
    ...

    inner class CountViewHolder(val binding: ViewHolderCountBinding) : RecyclerView.ViewHolder(binding.root) {
        ...
    }
}

Custom Views

One case we had to deal with in our migration that may be less common is the use of custom views with layout inflation. Usually you would only do this when your custom view is a ViewGroup, otherwise you wouldn’t need a layout file. Implementing view bindings in such a view would simply be done during construction, like this.

class CountView(context: Context) : LinearLayout(context) {

    private var binding: ViewCountBinding

    init {
        binding = ViewCountBinding.inflate(LayoutInflater.from(context), this, true)
        ...
    }
    ...
}

One thing to be careful of here is to match the ViewGroup you are extending to the one at the top level of your layout file, but that’s regardless of using view bindings or not.

Custom Views Holders

Another similar case that we encountered in our migration work was custom classes which would inflate a view and keep it in a property. This mechanism is a part of a whole dynamic view building system we have for generating piecewise UIs from json data. To avoid getting muddied in the details of that, here’s a simpler example of what a custom view holder could look like.

class CountViewHolder(context: Context, val parent: ViewGroup) {
    var view: View
    private var cbCount: CheckBox? = null

    init {
        val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        view = inflater.inflate(R.layout.custom_view_holder_count, parent, false)
        cbCount = view.findViewById(R.id.cbCount)
    }

    fun render() {
        parent.addView(view)
    }
}

In this case, we just replace the view with a view binding and access the view itself via the binding root when needed.

class CountViewHolder(context: Context, val parent: ViewGroup) {
    var binding: CustomViewHolderCountBinding

    init {
        val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        binding = CustomViewHolderCountBinding.inflate(inflater, parent, false)
    }

    fun render() {
        parent.addView(binding.root)
    }
}

Menus

What about menus? Unfortunately for us, menus do not use layout resource files so view bindings aren’t generated for them. So even though menus have their own xml definitions and we can use traditional layouts within them, view bindings can’t help us here.

How it Works

As just mentioned, view binding only works on layout resources. When you enable view binding in your app, performing a build will autogenerate a java class for each layout file. Each of these java classes then contains what might otherwise be in your front end classes: members which define each UI element and assignments for them that are done with findViewById() (actually findChildViewById() but it accomplishes the same thing).

For example, in our fragment from earlier we have the layout fragment_count.xml and the autogenerated binding class FragmentCountBinding.java.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/tvCount"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

 

public final class FragmentCountBinding implements ViewBinding {

  @NonNull private final FrameLayout rootView;
  @NonNull public final TextView tvCount;

  private FragmentCountBinding(@NonNull FrameLayout rootView, @NonNull TextView tvCount) {
    this.rootView = rootView;
    this.tvCount = tvCount;
  }
  ...

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

  @NonNull public static FragmentCountBinding bind(@NonNull View rootView) {
    int id;
    missingId: {
      id = R.id.tvCount;
      TextView tvCount = ViewBindings.findChildViewById(rootView, id);
      if (tvCount == null) {
        break missingId;
      }

      return new FragmentCountBinding((FrameLayout) rootView, tvCount);
    }
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

When we call inflate() on this binding, it calls its bind() method, extracts each view from the layout, and then creates and returns the view binding complete with convenient references to each child view in the layout and the root.

Miscellaneous Advice

In our migration process, we encountered a few hiccups that weren’t directly related to view binding implementation so here are a few extra pointers.

Placeholder View IDs

Any includes in your layouts need to have IDs whereas you previously may not have had them set. An ID is needed for the view bindings to properly recognize and bind them into the parent view.

Layout Variants

If you use any layout variants, like for landscape/portrait, you may have discrepancies in what views exist in one versus the other. You’ll be happy to know that there is nothing to worry about in these cases. Any views that don’t exist in all variants will be nullable and will only be set if the relevant view is loaded.

Layout Filenames

Use unique layout filenames and IDs. One real head-scratcher problem we had was with a particular binding which android studio refused to work properly with. Autocompletes wouldn’t work on it and it would give unresolved reference errors in the editor, but the app built and ran fine. The view binding file was even generated and seemed fully intact. Turns out the problem was the placeholder we were trying to access in the binding was identical to another in one of our external modules. We changed the name of the module’s placeholder and voila, everything worked as expected again.

Unresolved errors

You may experience issues with certain parts of your bindings giving you random unresolved reference highlights. If you don’t think it’s related to the above issue and your app still builds and runs fine then try a Build > Clean Project then Build > Rebuild Project. If that doesn’t work, try File > Invalidate Caches..., that usually sorts things out for us.

That’s a Wrap

Hopefully we’ve made a convincing enough demonstration for you that view bindings are easy to use and you should migrate to using them if you haven’t. With the examples provided, there should be no trouble fully integrating them into any app. Otherwise, we hope there’s information in here that could give you a hand next time you’re having trouble with your own view bindings.

Leave a comment

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


Similar articles

Image of an arrow

Savoir-faire Linux participated in the tenth edition of DrupalCamp Montréal, which was held this year at Concordia University. It was the occasion to catch up with a good proportion of the Drupal developer community in Montreal, to exchange ideas with other companies that work with this technology, and to have an overview of how Drupal […]

When It Comes to Websites, Page Speed Matters! This article is motivated by our website project accomplished by our Integration Platforms and AI Department using Liferay 7 (the latest version of Liferay Portal) for one of our clients– a large Canadian corporation in telecommunications and media industry. Alexis, our front-end developer, shares with you his first-hand experience […]

Thumbnail image

Web Development Getting Started with Server-Side Rendering in Angular By Kévin Barralon This week we released a tutorial for developers using the Angular JavaScript framework to set them up with a pre-configured server-side rendering environment. This environment allows for the pre-rendering of an Angular site so that its contents can be made visible to robots […]

Thumbnail image

Server-Side Rendering Management: An Angular’s Novelty Imposing a Challenge Angular is a framework using the TypeScript Programming Language. Its 5th version (pentagonal-donut) was released in November 2017, containing new features and bugfixes. It is accompanied by the command line tool Angular CLI and new features such as a server-side rendering framework that has become very popular within the community of Angular […]

Thumbnail image

Design What Is Design System? By Patrick Bracquart For several years now, the complexity of the websites and applications has pushed us towards rethinking the concept of design, as well as the methodologies we use in the design process. This rethink is pushed by an ever-expanding field of necessary skills and positions in design such […]