Listener de clique no RecyclerView com Kotlin
Alex Felipe
Posted on July 6, 2021
Ao utilizar o RecyclerView
no Android, muito provavelmente você se depara com a necessidade de implementar um listener de clique em um dos items do RecyclerView.Adapter
. Porém, não existe uma interface ou método padrão para fazer essa implementação, ou seja, somos obrigados a implementar manualmente!
Dada essa situação, eu vou mostrar pra você duas possibilidades comuns de implementação: interface ou o tipo função (muito utilizados em Higher-Order Function) do Kotlin.
Projeto de exemplo
Para esse exemplo, vou utilizar o Orgs, um App que simula um e-commerce de produtos naturais e bastante utilizado nos conteúdos da Alura.
Se tiver interesse em acompanhar o artigo com o exemplo, você pode obter mais informações do projeto a partir do repotório do GitHub, ou então, pode baixar diretamente a partir do código que vou utilizar. Abaixo segue uma amostra de execução do App.
Resumo do código já implementado
O App Orgs possui uma implementação de RecyclerView.Adapter
para apresentar os produtos cadastrados, a ListaProdutosAdapter
:
class ListaProdutosAdapter(
private val context: Context,
produtos: List<Produto>
) : RecyclerView.Adapter<ListaProdutosAdapter.ViewHolder>() {
private val produtos = produtos.toMutableList()
class ViewHolder(private val binding: ProdutoItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun vincula(produto: Produto) {
// vincula produto com as views
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(context)
val binding = ProdutoItemBinding.inflate(inflater, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val produto = produtos[position]
holder.vincula(produto)
}
override fun getItemCount(): Int = produtos.size
fun atualiza(produtos: List<Produto>) {
// atualiza produtos ao receber novos produtos
}
}
Note que nesta implementação, utilizamos o View Binding para fazer o processo de vínculo de View. Caso seja a sua primeira vez vendo ele, recomendo a leitura deste artigo que explica com mais detalhes o que é o View Binding e como ele funciona.
Como podemos notar, a implementação deste adapter é relativamente simples e comum às implementações de adapters para RecyclerView
. Mas agora vem a questão:
"Onde eu devo modificar o código para obter um listener nos itens?"
Implementando o listener de clique na View do ViewHolder
Considerando que queremos um listener para cada item, a implementação deles precisa ser feita em cada View criada para o ViewHolder
. Em outras palavras, ao criar um ViewHolder
e atribuir uma View para ele, podemos também implementar o listener de clique na View:
class ViewHolder(private val binding: ProdutoItemBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
Log.i("ListaProdutosAdapter", "clicando no item")
}
// ou também
// binding.root.setOnClickListener {
// Log.i("ListaProdutosAdapter", ": clicando no item")
// }
}
}
Com essa implementação, ao clicar em algum produto da lista de produtos, temos o seguinte resultado no log:
br.com.alura.orgs I/ListaProdutosAdapter: clicando no item
Agora a questão que fica é:
"Como podemos, por exemplo, reagir com esse listener na Activity que cria o Adapter?"
Criando o próprio listener
Para permitir que Activities ou qualquer outra classe reaja ao listener de um RecyclerView.Adapter
, precisamos criar o nosso próprio listener! Ele pode ser criado a partir de:
- Interfaces
- Tipo função
Listeners com interfaces
De uma maneira geral, o uso de interfaces é o mais conhecido pela comunidade do Android, pois, via Java, é a maneira mais utilizada! Portanto, vamos começar com essa implementação:
class ListaProdutosAdapter(
private val context: Context,
produtos: List<Produto>
) : RecyclerView.Adapter<ListaProdutosAdapter.ViewHolder>() {
private val produtos = produtos.toMutableList()
interface QuandoClicaNoItemListener {
fun quandoClica()
}
// restante do código
}
Note que neste exemplo, eu criei uma interface interna, justamente para indicar que o evento de clique é vinculado com o Adapter. Mas não significa que a interface deve ser criada dentro do adapter, fica a seu critério.
Com a interface criada, o próximo passo é criar uma property para que seja possível a implementação da interface:
class ListaProdutosAdapter(
private val context: Context,
produtos: List<Produto>,
var quandoClicaNoItemListener: QuandoClicaNoItemListener =
object : QuandoClicaNoItemListener {
override fun quandoClica() {
}
}
) : RecyclerView.Adapter<ListaProdutosAdapter.ViewHolder>() {
private val produtos = produtos.toMutableList()
interface QuandoClicaNoItemListener {
fun quandoClica()
}
inner class ViewHolder(private val binding: ProdutoItemBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
Log.i("ListaProdutosAdapter", "clicando no item")
quandoClicaNoItemListener()
}
}
}
// restante do código
}
Observe que o listener tem uma implementação padrão para evitar a obrigação de implementação ao criar a instância do adapter! Essa implementação padrão não tem comportamento adicional!
A partir do momento que temos acesso à property de listener (quandoClica()
), dentro do listener de clique da View
do ViewHolder
, podemos chamar o método quandoClica()
da interface para permitir que, quem implementar a interface, vai conseguir executar um código quando houver o clique!
Podemos testar esse código fazendo a seguinte implementação na Activity:
class ListaProdutosActivity : AppCompatActivity() {
private val dao = ProdutosDao()
private val adapter = ListaProdutosAdapter(
context = this,
produtos = dao.buscaTodos()
)
private val binding by lazy {
ActivityListaProdutosActivityBinding.inflate(layoutInflater)
}
// restante do código
private fun configuraRecyclerView() {
val recyclerView = binding.activityListaProdutosRecyclerView
recyclerView.adapter = adapter
adapter.quandoClicaNoItemListener =
object : ListaProdutosAdapter.QuandoClicaNoItemListener {
override fun quandoClica() {
Log.i("ListaProdutosActivity", "quandoClica: ")
}
}
}
}
Veja que temos uma implementação de classe anônima a partir de um Object Expression! Ao testar o App, temos o seguinte resultado ao clicar em um produto:
br.com.alura.orgs I/ListaProdutosAdapter: clicando no item
br.com.alura.orgs I/ListaProdutosActivity: quandoClica:
Agora somos capazes de reagir com o listener do Adapter! Inclusive, podemos personalizar a interface para, por exemplo, receber o produto clicado:
class ListaProdutosAdapter(
private val context: Context,
produtos: List<Produto>,
var quandoClicaNoItemListener: QuandoClicaNoItemListener =
object : QuandoClicaNoItemListener {
override fun quandoClica(produto: Produto) {
}
}
) : RecyclerView.Adapter<ListaProdutosAdapter.ViewHolder>() {
private val produtos = produtos.toMutableList()
interface QuandoClicaNoItemListener {
fun quandoClica(produto: Produto)
}
inner class ViewHolder(private val binding: ProdutoItemBinding) :
RecyclerView.ViewHolder(binding.root) {
private lateinit var produto: Produto
init {
itemView.setOnClickListener{
Log.i("ListaProdutosAdapter", "clicando no item")
if(::produto.isInitialized) {
quandoClicaNoItemListener.quandoClica(produto)
}
}
}
fun vincula(produto: Produto) {
this.produto = produto
// restante do código
}
// restante do código
}
// restante do código
}
Caso seja a sua primeira vez vendo uma property
lateinit
, não se assuste! Basicamente, é um recurso do Kotlin para criar properties que podem ser inicializadas posteriormente, dessa forma, não precisamos colocar um valor padrão inválido ou trabalhar com nullables. Mas, antes de utilizar variáveislateinit
, certifique-se que a mesma foi inicializada, conforme a verificação viaif
. Caso contrário, será lançada uma exception e o App vai quebrar!
Com essa modificação, o ViewHolder
agora tem acesso a uma property mutável do tipo Produto
. Ela é atualizada cada vez que o método vincula()
é acionado.
Isso é necessário, pois ViewHolder
em RecyclerView.Adapter
são reutlizados, ou seja, sem a atualização da property, podemos enviar um produto errado! Sempre lembre-se disso!
Além disso, o método quandoClica()
do listener recebe um Produto
e, ao chamá-lo, enviamos o produto como argumento para que seja acessível por quem implementar o listener.
A partir dessa mudança, veja que a implementação padrão no construtor da ListaProdutosAdapter
modificou o quandoClica()
, pois agora ele vai receber o produto ao chamar o método quandoClica()
. Isso vale também para a Activity:
adapter.quandoClicaNoItemListener =
object : ListaProdutosAdapter.QuandoClicaNoItemListener {
override fun quandoClica(produto: Produto) {
Log.i("ListaProdutosActivity", "quandoClica: ${produto.nome}")
}
}
Ao rodar o código e clicar no produto, temos um resultado diferente:
br.com.alura.orgs I/ListaProdutosAdapter: clicando no item
br.com.alura.orgs I/ListaProdutosActivity: quandoClica: Salada de frutas
Agora temos acesso ao produto! E podemos fazer a ação que desejamos com o produto, como por exemplo, enviar para uma nova Activity que vai exibir o seu conteúdo!
"Ok, aprendemos a fazer a implementação com a interface, mas como fica com o tipo função do Kotlin?"
Listener com o tipo função do Kotlin
Com o tipo função é bastante similar, a diferença é que temos um código mais simplificado, pois não precisamos criar uma estrutura como uma interface, e toda a implementação pode ser via expressão lambda! Vamos começar com o adapter:
class ListaProdutosAdapter(
private val context: Context,
produtos: List<Produto>,
var quandoClicaNoItemListener: (produto: Produto) -> Unit = {}
// var quandoClicaNoItemListener: QuandoClicaNoItemListener =
// object : QuandoClicaNoItemListener {
// override fun quandoClica(produto: Produto) {
//
// }
// }
) : RecyclerView.Adapter<ListaProdutosAdapter.ViewHolder>() {
private val produtos = produtos.toMutableList()
// interface QuandoClicaNoItemListener {
// fun quandoClica(produto: Produto)
// }
inner class ViewHolder(private val binding: ProdutoItemBinding) :
RecyclerView.ViewHolder(binding.root) {
private lateinit var produto: Produto
init {
itemView.setOnClickListener{
Log.i("ListaProdutosAdapter", "clicando no item")
if(::produto.isInitialized) {
quandoClicaNoItemListener(produto)
}
}
}
fun vincula(produto: Produto) {
this.produto = produto
// restante do código
}
// restante do código
}
// restante do código
}
Dê 9 linhas e uma leitura mais complexa, considerando a implementação padrão, fomos para 1 linha de uma forma mais simplificada com o tipo função! E temos mais resultados na Activity:
private fun configuraRecyclerView() {
val recyclerView = binding.activityListaProdutosRecyclerView
recyclerView.adapter= adapter
adapter.quandoClicaNoItemListener = {
Log.i("ListaProdutosActivity", "quandoClica: ${it.nome}")
}
//object : ListaProdutosAdapter.QuandoClicaNoItemListener, (produto: Produto) -> Unit {
// override fun quandoClica(produto: Produto) {
// Log.i("ListaProdutosActivity", "quandoClica: ${produto.nome}")
// }
//}
}
De uma leitura mais complexa com o Object Expression, agora temos uma expressão lambda bem mais simplificada!
Conclusão
Com o acesso ao Kotlin, o uso do tipo função é mais desejado para implementações de listeners próprios! Seja pela simplicidade ou até mesmo pela forma mais idiomática de escrever código em Kotlin.
E você, o que achou dessas técnicas para criar listeners próprios no Kotlin? Se gostou, deixe like, comentário e compartilhe com a comunidade este conteúdo 😄
Código final
Agora que finalizou a leitura, se preferir, você pode consultar este commit para verificar o código finalizado.
Aprender mais
Que tal aprender mais sobre Android, RecyclerView
, Kotlin entre outras técnicas e tecnologias deste mundo de mobile? Além dos conteúdos abertos, também produzo cursos de Android na Alura seja para quem está iniciando ou para quem quer aprimorar mais ainda o conhecimento. Se você já assina a Alura, seguem os cursos. Caso você ainda não conhece a Alura e tem interesse, tenho uma boa notícia pra você também, a partir deste link, você tem 10% de desconto na sua assinatura 😉
Posted on July 6, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.