Image of an arrow

View Bindings dans Android

Avatar

alussiercullen

Le développement Android progresse constamment avec de nouvelles fonctionnalités pour faciliter la création d’applications. Construction d’interface utilisateur avec Jetpack Compose, injection de dépendances avec Hilt, extensions de développement de jeux, librairies de compatibilité emoji, et la liste continue. Les nouveaux projets n’ont aucun souci à prendre avantage de ces nouvelles fonctionnalités. Cependant, les projets anciens doivent trouver un équilibre lors de la migration vers de nouveaux systèmes de logiciels. Chez Savoir-faire Linux, nous maintenons une variété de projets Android et parmi eux, il y en a un que nous maintenons depuis plus d’une demi-décennie maintenant. Au fil des ans, nous avons intégré de nombreuses modernisations au projet; l’une des meilleures était la migration aux 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.


Articles similaires

Image of an arrow

En juin dernier, Savoir-faire Linux a participé à la 10ᵉ édition du DrupalCamp Montréal, qui a eu lieu à l’Université Concordia. Ce fût l’occasion de voir une bonne partie de la communauté de développeurs Drupal montréalaise, d’échanger avec les autres entreprises qui travaillent avec cette technologie et de faire l’état de l’évolution de Drupal de […]

Thumbnail image

Développement Web Bien démarrer avec le rendu côté serveur avec Angular Par Kévin Barralon Cette semaine, nous avons mis en ligne un tutoriel pour les développeurs qui utilisent le framework JavaScript Angular. Ce tutoriel a pour but de les aider à initialiser un projet Angular avec le rendu côté serveur pré-configuré. L’intérêt de la mise […]

Gestion du rendu côté serveur : Une nouveauté Angular imposant un défi Angular est un framework utilisant le langage de programmation TypeScript. La version 5 a été mise en ligne en novembre 2017, avec de nouvelles fonctionnalités et corrections de bugs. Cette dernière version est accompagnée de l’outil en ligne de commande Angular CLI, mais […]

Thumbnail image

Design Qu’est ce que design system ? Par Patrick Bracquart Depuis plusieurs années, la complexité des sites et applications nous ont poussé à repenser le design et les méthodologies tant les champs de compétences nécessaires se sont élargis (analyste web, designer UI/UX, designer d’interaction, développeur front-end, …). C’est dans ce contexte qu’est apparu le design […]

Thumbnail image

Spécial PyCon Canada 2017 (Suite !) Que retenir de la dernière PyCon Canada ? Quelques éléments de réponse avec les développeurs de l’équipe Innovation de Savoir-faire Linux ! Conférence ‘Double Click: Continue Building Better CLIs’ Par Kévin Barralon Cette conférence donnée lors de la PyCon Canada visait à présenter Click, un package Python permettant de […]