Listener de clique no RecyclerView com Kotlin

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áveis lateinit, certifique-se que a mesma foi inicializada, conforme a verificação via if. 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

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 😉

24