View Binding Android

Alex Felipe
Alex Felipe

Compartilhe

Neste artigo, vamos explorar o View Binding, uma técnica com o objetivo de substituir a forma como buscamos views no Android, seja pelo findViewById(), ou até mesmo, pelo Synthetic do Kotlin Android Extension que agora é obsoleto.

Projeto de exemplo

Para demonstrar os exemplos, vamos utilizar o Todo, um App capaz de cadastrar e listar tarefas:

  

As implementações foram feitas da seguinte maneira:

  • Lista de tarefas em uma Activity com um RecyclerView
  • Formulário de criação de tarefa em um Fragment

O motivo de utilizar essas entidades, é demonstrar as diferentes implementações com o View Binding.

Caso tenha interesse em acompanhar o conteúdo aplicando os exemplos, você pode acessar o repositório do projeto no GitHub.

Por que utilizar o View Binding?

Como vimos, o View Binding trata-se de uma alternativa para buscar Views do Android, porém, por padrão, temos acesso ao findViewById(). Então, por que deveríamos considerar o View Binding?

Os principais motivos para o uso do View Binding são:

  • Escrever um código mais fácil para interagir com as Views
  • Obter mais segurança ao acessar uma View:
    • segurança de nulo: o View Binding cria diretamente as referências com a View, portanto, não há risco de buscar uma View que não existe no layout.
    • segurança de tipo: cada campo encontrado no layout pelo View Binding, mantém o mesmo tipo apresentado no arquivo de layout, dessa forma, evitamos qualquer problema de casting.

Agora que conhecemos os motivos para utilizar o View Binding, vamos começar com a configuração.

Configurando o View Binding no projeto Android

O View Binding é configurado por módulo de um projeto Android, portanto, dentro do arquivo build.gradle do módulo App, adicionamos a seguinte instrução:

android {
    ...
    buildFeatures {
        viewBinding true
    }
}

Basta sincronizar que o View Binding está configurado! Simples assim. 🙂

Como o View Binding funciona?

Após habilitar o View Binding, automaticamente, ele gerará classes que representam cada arquivo de layout do módulo no seguinte padrão: nomeDoLayoutBinding.

Dentro do projeto temos os seguintes arquivos de layout:

  • formulario_nota_activity.xml
  • formulario_nota_fragment.xml
  • item_nota.xml
  • lista_notas_activity.xml

Isso significa que ao configurar o View Binding, ele gera as seguintes classes para os layouts:

  • FormularioNotaActivityBinding
  • FormularioNotaFragmentBinding
  • ItemNotaBinding
  • ListaNotasActivity

A partir dessas referências, podemos acessar as views de cada layout considerando os destaques do View Binding.

Para começar o nosso exemplo, vamos acessar a ListaNotasActivity:

class ListaNotasActivity : AppCompatActivity(R.layout.lista_notas_activity) {

    // restante do código

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

        val recyclerView = findViewById<RecyclerView>(R.id.lista_notas_activity_recyclerview)
        recyclerView.adapter = adapter
        configuraFab()
    }

    private fun configuraFab() {
        val fab = findViewById<ExtendedFloatingActionButton>(R.id.lista_notas_activity_fab)
        fab.setOnClickListener {
            // restante do código
        }
    }

}

Note que utilizamos o findViewById() para buscar o RecyclerView e o ExtendedFloatingActionButton. Dado esse exemplo, vamos adaptar o código para usar o View Binding.

Utilizando o View Binding na Activity

Para utilizar o View Binding, primeiro precisamos realizar o processo de inflar a View. Para isso, no onCreate(), acesse a referência ListaNotasBinding e chame o método inflate() enviando o layoutInflater da Activity:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding = ListaNotasActivityBinding.inflate(layoutInflater)
    // restante do código
}

Observe que criamos a variável binding, esse padrão de nomeação é comum quando realizamos o processo de binding de View com o View Binding.

A partir da variável binding, podemos acessar as views, como, por exemplo, o RecyclerView:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding = ListaNotasActivityBinding.inflate(layoutInflater)
    val recyclerView = binding.listaNotasActivityRecyclerview
    recyclerView.adapter = adapter
    configuraFab()
}

E para ter um acesso por todos os membros, podemos até mesmo tornar o binding uma property lazy:

class ListaNotasActivity : AppCompatActivity(R.layout.lista_notas_activity) {

    private val binding by lazy {
        ListaNotasActivityBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val recyclerView = binding.listaNotasActivityRecyclerview
        // restante do código
    }

    // restante do código

    private fun configuraFab() {
        val fab = binding.listaNotasActivityFab
        // restante do código
    }

}

Se testarmos o App apenas com essa modificação, o RecyclerView e o FAB não funcionam! Isso acontece porque precisamos vincular a View do View Binding com a Activity, para isso, podemos chamar o método setContentView() enviando o root do binding

class ListaNotasActivity : AppCompatActivity() {

    private val adapter by lazy {
        ListaNotasAdapter(this)
    }
    private val binding by lazy {
        ListaNotasActivityBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val recyclerView = binding.listaNotasActivityRecyclerview
        recyclerView.adapter = adapter
        configuraFab()
        setContentView(binding.root)
    }

        // restante do código

}

A partir do momento que utilizamos o View Binding, é importante remover a referência do layout no construtor da Activity ou em um setContentView().

Com esse ajuste, o nosso código funciona sem qualquer problema!

Implementando o View Binding no RecyclerView

No ListaNotasAdapter do RecyclerView, o uso do ViewBinding é feito durante a criação do ViewHolder. A técnica é similar ao da Activity, mas seguindo os padrões de inflate do adapter:

class ListaNotasAdapter(
    private val context: Context,
    notas: List<Nota> = listOf()
) : RecyclerView.Adapter<ListaNotasAdapter.ViewHolder>() {

    private val notas = notas.toMutableList()

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): ViewHolder {
                val binding = ItemNotaBinding
            .inflate(
                LayoutInflater.from(context),
                parent,
                false
            )
        return ViewHolder(binding)
    }

    // restante do código

    class ViewHolder(binding: ItemNotaBinding) :
        RecyclerView.ViewHolder(binding.root) {

        private val titulo = binding.itemNotaTitulo
        private val descricao = binding.itemNotaDescricao

        fun vincula(nota: Nota) {
            titulo.text = nota.titulo
            descricao.text = nota.descricao
        }

    }

}

Observe que a grande diferença é que o ViewHolder recebe o View Binding do layout desejado, nesse caso, o ItemNotaBinding. Então, é enviada a view com o binding.root para o construtor do RecyclerView.ViewHolder() e o parâmetro binding pode ser utilizado para buscar as views desejadas.

Utilizando o View Binding no Fragment

No Fragment temos um cenário diferente. Pois, devido ao processo de recriar a View cada vez que ele é acessado, somos responsáveis em criar o View Binding e limpá-lo quando a view é destruída:

class FormularioNotaFragment : Fragment() {

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

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val botao = binding.formularioNotaBotaoSalvarFragment
        botao.setOnClickListener {
            salvaNota()
        }
    }

    private fun salvaNota() {
        val campoTitulo = binding.formularioNotaTituloFragment
        val titulo = campoTitulo.text.toString()
        val campoDescricao = binding.formularioNotaDescricaoFragment
        val descricao = campoDescricao.text.toString()
        val nota = Nota(titulo, descricao)
        NotaDao().salva(nota)
        activity?.onBackPressed()
    }

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

}

Da mesma maneira que foi feito na Activity, no Fragment também é importante remover a referência de layout no construtor.

Essa possível implementação é sugerida na página da documentação, e explica que mesmo usando o null assertion operator (!!), bastante perigoso por conta de NPE, não corremos o risco de NPE se acessarmos o binding nos estados de ciclo de vida do Fragment entre onCreateView() e onDestroyView():

  

Com base no fluxograma, podemos acessar o binding a partir do onViewCreated(), até o onSaveInstanceState().

Tempo de build e ignorando layouts com View Binding

Por gerar código, o View Binding também custa tempo de build que, segundo a documentação, por não ter anotações durante a configuração, ele é mais rápido que alternativas como o Data Binding.

Além disso, se preferir evitar a geração de código em layouts específicos, seja pelo tempo de build ou por não usar referências do layout, como é o caso da FormularioNotaActivity, que tem apenas um container de Fragment, podemos ignorar layouts adicionando tools:viewBindingIgnore="true" no root da View do layout.

Considerando o exemplo da FormularioNotaActivity, fica da seguinte maneira:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".ui.activity.FormularioNotasActivity"
    tools:viewBindingIgnore="true">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/formulario_notas_fragment_container_activity"
        android:name="br.com.alura.todo.ui.fragment.FormularioNotaFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Com esse ajuste, a classe FormularioNotaActivity não é mais gerada pelo processo de build do projeto.

Conclusão

O View Binding é a técnica mais recente e recomenda pela equipe do Android, portanto, se atualmente você utiliza o synthetic, o recomendado é que migre para o View Binding. Caso mantenha o uso apenas do findViewById(), você pode considerar o uso do View Binding, pois, além de facilitar a busca das views, também existe o benefício de evitar NPE ou problemas de casting.

Código fonte desenvolvido

Caso tenha interesse em consultar as mudanças do projeto, confira este commit.

Alex Felipe
Alex Felipe

Alex é instrutor e desenvolvedor com foco em Java, Kotlin, Android e Spring. É expert em Programação Orientada a Objetos, visando sempre compartilhar as melhores práticas e tendências do mercado para desenvolvimento de software com quem está estudando o assunto. Atuou 2 anos como editor de conteúdo no blog da Alura e hoje ainda escreve artigos técnicos sobre o desenvolvimento.

Veja outros artigos sobre Mobile