Layouts / View Binding

View Binding: Merge

View Binding was introduced in 2019 and is really nice. I have written about it before. However I recently discovered a small gotcha that is easy to work around, but it did me a frighten when I first encountered it, and is not, at the time of writing, mentioned in the official documentation. In this post we’ll look at and understand the issue; and then look at three different, but all quite simple, approaches to dealing with it.

The way in which I encountered the issue was a little odd, and it was very much a corner case that caused it. I was working on the sample code project for another blog post and there was only a very simple layout as the focus of the subject matter was code-based rather than layouts, and a simple layout would suffice. The Activity layout that was auto-created by Android Studio was a ConstaraintLayout containing a simple TextView. As I was tidying up the code I realised that ConstraintLayout was a little overkill so I decided to replace it with a far more lightweight FrameLayout. So far, so good. When I ran lint, I get a MergeRootFrame warning which was correct because the FrameLayout could actually be replaced with a <merge>...</merge> block, which I did. We’ll discuss why this is a good change shortly.

Everything was now working perfectly when I realised that I hadn’t enabled View Binding on the project. I promptly did so, and as I made the necessary changes to the onCreate() method of my Activity I found that the signature of the inflate() method of the generated binding class was not what I expected, and certainly didn’t match the example in the official docs.

After much head scratching I realised that the use of the merge root tag resulted in a different signature for the inflate() method of the generated binding because it now required an additional ViewGroup argument.

This actually makes perfect sense if we consider what <merge> actually does. For any Activity there is a ContentFrameLayout with an id of android.R.id.content. When we call setContentView() the layout we specify gets inflated in to that parent. When I had a FrameLayout at the root of my layout this would have added the FrameLayout and its child TextView as children of this ContentFrameLayout. The FrameLayout itself is actually serving no purpose because the ContentFrameLayout itself is essentially a FrameLayout. If we use <merge> instead, then during inflation the <merge> itself is ignored, and the TextView will be added directly to the ContentFrameLayout, which flattens our layout hierarchy slightly. This is a good thing.

When we call setContentView() with a layout resource ID, then that method adds the children of the merge to the ContentFrameLayout automagically for us because it is a method of Activity which knows about the ContentFrameLayout. Using View Binding requires us to first inflate the layout using the inflate() method of the generated binding class, and then use a different variant of setContentView() which takes a View argument rather than a layout resource id. So when the binding is generated for a layout with a <merge> root it has no knowledge of the parent in to which the children of the <merge> should be added and therefore requires a ViewGroup argument which is not required for other layout root elements.

To show this in code, when we don’t have a layout with a <merge> root we do this:

class IgnoreActivity : AppCompatActivity() {

    private lateinit var binding: ActivityIgnoreBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityIgnoreBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}

Inflating the layout first bypasses the work that setContentView(@LayoutId layoutId: Int) is able to do. This is fine in most scenarios, but when it comes to inflating a layout a <merge> root it requires a parent ViewGroup in to which the children of the <merge> should be added. This is the additional argument that is required for the inflate() method of the binding class that gets generated for a layout with a <merge> root.

The possible work arounds for this are actually much easier to understand once we understand that issue itself.

The first option that we have is to restore the FrameLayout in place of the <merge> root, add a lint ignore attribute to suppress the warning, and then the generated binding can be inflated without providing a parent:


    .
    .
    .

This technically works, but suppressing warnings is a bad smell. If we suppress the warning we are bringing back the original issue and increasing our layout hierarchy depth. I would consider this a last resort fix and there are other better options.

The second option is to manually flatten our layout by removing the root altogether:


This is actually really clean – we take out the redundant FrameLayout and therefore manually achieve what <merge> does for us. The problem with this is that if ever we need to add additional siblings to the TextView we’ll need to restore a root ViewGroup container of some kind. Not really a big deal, but worth bearing in mind, nonetheless. Personally I feel that this is the best all-round option within an Activity layout because it optimises the layout hierarchy depth, without affecting the associated Activity code.

The third option is to keep the <merge> root and adapt the Activity code to correctly handle it:

class MergeActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMergeBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val content = findViewById(android.R.id.content)
        binding = ActivityMergeBinding.inflate(layoutInflater, content)
    }
}

First we manually look up the ContentFrameLayout with the id android.R.id.content, and then we use this as the additional required argument to inflate() method.

Initially this feels a bit weird because the whole point of View Binding is to avoid using findViewById. However, in this case it is used to find a View that is outside of the layout that we’re responsible for.

The other thing worth mentioning is that we no longer need to call setContentView() because the inflate() method automagically adds the inflated layout to the parent.

Those three options pretty much cover all use-cases that I can think of – the ignore option is the safety net that it would be best to avoid. The choice between the other two really depends on the requirements of your project. Personally I think keeping the layout optimised to avoid changes that break the Activity code is best option as it keeps the Activity and layout XML individually responsible for themselves..

The source code for this article is available here.

© 2020, Mark Allison. All rights reserved.

Copyright © 2020 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.