View Binding Android
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 umRecyclerView
- 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.