Criando lista com RecyclerView no Android com Kotlin
No artigo onde aprendemos a criar uma App com suporte ao Kotlin, criamos o projeto Ceep com o intuito de fazer uma App similar ao Google Keep, porém, ainda não implementamos a lista para conter as notas da nossa App…
Considerando essa necessidade, neste artigo, veremos como podemos implementar a nossa lista utilizando a API RecyclerView do Google.
Quer aprender mais sobre Kotlin tanto no mundo mobile como no back-end? Então confira este agregador de conteúdo onde listo todos os conteúdos que escrevi de Kotlin e os que serão publicados mais pra frente 😉
Caso não tenha a mínima ideia do que seja essa API, recomendo este artigo que ensina passo a passo o que é o RecyclerView e como é possível implementá-lo com Java no Android.
Preparando o ambiente
Para criarmos essa tela, iremos utilizar tanto o RecyclerView como também o componente CardView, portanto vamos adicionar as seguintes dependências no arquivo build.gradle:
Em seguida, precisamos apenas sincronizar o Gradle que teremos ambas as APIs disponíveis.
É válido notar que estou usando a versão 3 do AS que faz uso do gradle wrapper versão 4.1, por isso foi utilizado o
implementation
ao invés docompile
para definir as dependências.Também é importante observar que usei a versão
26.0.2
em ambas as libs para o exemplo, porém, se tiver alguma versão mais atualizada, fique à vontade em utilizá-la.
Adicionado o RecyclerView no layout
Agora que temos o RecyclerView no projeto, vamos modificar o nosso layout activity_note_list
para que agora ele contenha o componente RecyclerView
:
Com o RecyclerView
disponível, podemos começar com a configuração do Adapter que vai conter as notas.
Criando o Adapter para representar as notas
Para isso, vamos criar a classe NoteListAdapter
e fazer extensão da classe RecyclerView.Adapter
:
Veja que o compilador acaba reclamando no momento que fazemos a extensão do Adapter
justamente porque ele exige que passemos uma classe que faça a extensão de um RecyclerView.ViewHolder
via generics, ou seja, o nosso famoso ViewHolder que precisamos criar para o Adapter
de um RecyclerView
.
Sendo assim, vamos criar a classe ViewHolder
que vai estender a classe RecyclerView.ViewHolder
:
Calma aí! Como sabemos, no Kotlin, por padrão, todos os membros dentro de um arquivo Kotlin são public
, ou seja, estamos com duas classes públicas em um único arquivo e ele compila! Algo que no Java não é pertimido, certo?
Entretanto, esse é um comportamento comum dentro do Kotlin, isto é, podemos definir mais de uma classe pública dentro de um único arquivo…
Cuidados ao criar mais de uma classe dentro de um arquivo Kotlin
Por mais que a linguagem permita esse tipo de comportamento, é muito importante tomarmos cuidado e não abusarmos dessa abordagem, pois existe a possibilidade criarmos um código cada vez mais difícil de ler ou manter.
A minha recomendação é que evite o máximo possível criar mais de uma classe pública dentro do mesmo arquivo, ou então, simplesmente não crie. Dessa forma, evitamos mais complexidade desnecessária.
Criando uma Nested class no Kotlin
Considerando a ideia de que precisamos da classe ViewHolder
que vai servir apenas para o nosso NoteListAdapter
, nada mais justo do que torná-la uma Nested class:
Observe que agora, caso alguém, além da classe NoteListAdapter
, quiser acessar a nossa classe ViewHolder
não será possível.
Já que a princípio o nosso
ViewHolder
não vai acessar nenhum membro da classeNoteListAdapter
, como também, não faz sentido algum alguém acessá-la de fora, mantê-lo como Nested class, ao invés de uma Inner class por exemplo, é uma solução bem vinda!
Peculiaridades entre o Adapter e o ViewHolder do RecyclerView
Bacana, agora que criamos o nosso ViewHolder
, precisamos resolver os problemas de compilação que surgiram.
O primeiro deles é justamente fazer com que o RecyclerView.Adapter
receba o ViewHolder
via generics:
Agora, precisamos também enviar uma View
para o construtor do RecyclerView.ViewHolder
pois é partir dela que o Adapter conseguirá reciclar as views:
Pronto! Basta apenas pedirmos para o AS inserir as funções do RecyclerView.Adapter
que precisam ser sobrescritas:
O nosso próximo passo é começar a implementar as 3 funções que foram sobrescritas. Vamos lá?
Note que ao pedirmos para o AS sobrescrever as funções do Adapter, surgiu a chamada da função
TODO()
, não se preocupe!Essa função quando executada, basicamente da um crash na App e envia uma exception indicando que não existe a implementação da função chamada.
Portanto, podemos mantê-la enquanto não finalizarmos a implementação de cada da função e, quando finalizarmos a implementação, basta apenas removermos essa chamada.
Implemetando a função getItemCount()
Começaremos pela getItemCount()
que basicamente precisa do tamanho da lista que iremos enviar para o Adapter.
Para isso, podemos criar a property notes
para representar a nossa lista e retornamos o seu tamanho na função getItemCount()
:
Repara que ao mesmo tempo que recebemos a lista de notas via construtor primário, a tornamos uma property da NoteListAdapter
no momento que adicionamos a keyword val
.
Também, fazemos com que essa property seja privada, pois não há a necessidade de alguém, a não ser o nosso próprio Adapter, saber da existência dela.
Criando o modelo para representar cada nota
Mas tem um detalhe nesse passo… Em nenhum momento criamos a classe Note
, sendo assim, vamos criá-la agora:
Como a ideia do exemplo é ser uma App simples, ter apenas o título e a descrição é o suficiente. Em seguida, basta apenas fazer o import da classe Note
dentro do NoteListAdapter
.
Agora que temos a nossa lista de notas, podemos continuar com a implementação das funções sobrescritas.
Implementando o onCreateViewHolder()
Dessa vez, iremos implementar a função responsável por criar o nosso ViewHolder
, ou seja, a onCreateViewHolder()
.
Dentro dela, precisamos inflar o layout para representar cada uma das notas. Mas até o momento não criamos um layout pra isso, ou seja, vamos criar o layout note_item
com o seguinte código:
Em seguinda, vamos inflar o layout, porém, para que isso seja possível, precisamos também receber um Context
via construtor:
E então, podemos retorna a instância do nosso ViewHolder
enviando a view
via construtor:
Implementando o onBindViewHolder()
Agora nos resta implementar a última função que foi sobrescrita que tem a finalidade de realizar a junção das informações contida no ViewHolder
com os componentes do layout, ou seja, a onBindViewHolder()
.
Para isso, primeiramente, precisamos fazer com que o nosso ViewHolder
contenha os componentes do layout que inflamos, ou seja, os TextView
s do note_item
.
Utilizando o Synthetic para buscar as views dentro do Adapter
Uma das abordagens que costumamos fazer, é justamente chamar o findViewById
a partir do objeto itemView
que nada mais é do que a View
que criamos.
Mas, como vimos, por meio do Kotlin Android Extensions, podemos evitar esse tipo de tarefa utilizando o Synthetic, ou seja, por meio do itemView
podemos chamar os componentes pelos seus ids:
Portanto, o que precisamos fazer agora é justamente pegar uma nota por meio da posição recebida do onBindViewHolder()
e realizar o processo de bind, ou seja, colocar as informações do objeto devolvido em cada um dos TextView
s:
Ops! Veja que nesse momento o AS apresenta um problema de compilação! Isso acontece, pois o holder
recebido por parâmetro, pode ser null
…
Nunca tivemos esse tipo de comportamento quando fazíamos no Java, certo? Então porque no Kotlin isso acontece?
Entendendo o Null Safety
O Kotlin foi desenvolvido baseando-se em diversas features encontradas em várias linguagens de programação, como é o caso do C#, Swift, JavaScript entre outras…
Dentre as features disponíveis, temos a Null Safety que tem a proposta de evitar que o programador tome a famosa NPE, ou seja, a exceção NullPointerException
.
Em outras palavras, temos diversas restrições quando estamos lidando com valores null
ou que podem ser null
dentro do Kotlin.
De modo geral, quando um valor for null
ou existir a possibilidade de ser null
o código não vai compilar!
“Mas por que isso só surgiu agora?”
Se observarmos o parâmetro holder
veremos que ele está declarado da seguinte maneira:
holder: ViewHolder?
O operador ?
, durante a declaração de uma variável, parâmetro, property ou retorno de função, nos indica que existe uma possibilidade que o valor seja null
, portanto, o Kotlin exige que a gente trate esse objeto e garanta que ele não seja null
.
Em Java, garantimos que um valor não é null
da seguinte maneira:
if (holder != null) {
holder.title.text = note.title
holder.description.text = note.description
}
Em Kotlin, esse código também é válido, porém, existem técnicas mais sucintas que veremos a seguir.
Utilizando Safe Calls
O Kotlin nos fornece um operador conhecido como Safe Call, a partir dele, temos a capacidade de garantir que um membro de um objeto (property, funções ou classes) vai ser chamado apenas se ele não for null
.
Para usarmos esse operador, basta apenas incluir novamente o operador ?
antes de chamar o membro do objeto que pode ser null
, como é o caso do holder
:
holder?.title.text = note.title
holder?.description.text = note.description
Dessa forma, não precisamos verificar se o holder
é null
, ou seja, tanto a property title
como a description
serão chamadas apenas se o holder
não for null
, logo, estamos evitando o NPE.
Mas repara que agora, quando chamamos a propertytext
de ambos os TextView
s temos o mesmo problema deles poderem ser null
… E se analisarmos a nossa property title
ela não indica que pode ser null
…
“Por que isso acontece?”
Quando utilizamos uma Safe Call e tentamos realizar mais uma chamada de um membro de forma encadeada, precisamos garantir que o membro não seja null
também. E Agora?
Realizando uma Safe Call em modo chain
Simples! Podemos fazer uma Safe Call em modo chain, ou seja, de forma encadeada:
holder?.title?.text = note.title
holder?.description?.text = note.description
Bacana, esse ajuste resolve o nosso problema, mas veja que estamos deixando o nosso código cada vez mais sujo com essa quantidade de Safe Calls, e mais, caso tivéssemos que chamar mais membros, seria uma meleca e tanto… Será que não tem uma alternativa ainda mais sucinta?
Se analisarmos o nosso código, sabemos que o único membro que precisamos garantir que não é null
é o holder
, concorda?
Considerando essa analise, uma abordagem ideal seria da seguinte maneira:
- Verificar se o
holder
não énull
; - Se isso for verdade, todo o código executado a seguir, deve considerar o
holder
como nãonull
.
Será que isso é possível?
Dando permissão de execução com o let
Podemos realizar esse tipo de comportamento por meio da função let
que pode ser chamada por um objeto, como é o caso do holder
, e então, executa um bloco de função com o objeto que a chamou embutido dentro dela.
Traduzindo para o código, temos o seguinte resultado:
holder?.let {
holder.title.text = note.title
holder.description.text = note.description
}
Veja que nessa abordagem o nosso código compila! Porém, nos deixa na seguinte situação:
“Que diabos aconteceu aqui?”
De uma forma mais carinhosa, quando fizemos a Safe Call e chamamos o let
com o nosso holder
, o nosso holder
foi para dentro do escopo do let
como um objeto que não é null
.
Portanto, ao utilizá-lo dentro do escopo do let
, não existe mais a necessidade de verificar se ele ou seus membros são null
! Algo incrível, né?
Utilizando o objeto it
Além disso, quando realizamos esse tipo de abordagem, o objeto enviado pode ser chamado utilizando a keyword it
que nada mais é do que o próprio objeto embutido dentro do let
, nesse caso o nosso próprio holder
:
Veja que agora estamos fazendo o processo de bind de uma forma bem mais sucinta e elegante. Claro, é importante ressaltar que se o holder
for null
, o let
não será executado!
Delegando a responsabilidade
Bacana, conseguimos fazer o processo de bind mas perceba que esse tipo de comportamento, faz todo o sentido dentro do próprio ViewHolder
, ou melhor, a boa prática nessa situação é fazer com que o nosso próprio ViewHolder
faça isso pra gente!
Para isso, basta apenas criarmos a função bindView()
que recebe uma nota dentro do ViewHolder
e, dentro dela, a gente faz o processo de bind:
Observe que dessa forma, apenas a própria classe ViewHolder
sabe da existência dos componentes, como também, ela mesma ficou responsável em computar as informações dos mesmos.
Configurando o RecyclerView
Agora que já temos o nosso Adapter implementado, precisamos apenas utilizá-lo e terminar a configuração do nosso RecyclerView
. Sendo assim, na nossa NoteListActivity
, vamos buscar o RecyclerView
e settar o Adapter:
Repara que estamos retornando uma lista hardcoded apenas para teste, mas poderia ser uma busca por um banco de dados ou via Web Service.
Em seguida, precisamos apenas indicar o layout do RecyclerView
por meio do LayoutManager
:
Utilizamos StaggeredGridLayoutManager
para termos o mesmo aspecto visto no Google Keep. Agora ao rodarmos a nossa App, temos o seguinte resultado:
Uhul! Finalmente conseguimos implementar a primeira amostra da nossa App. 😄
Código fonte
Se preferir consultar o código fonte desenvolvido no artigo, deixo aqui o link do GitHub. 😉
Para saber mais
Além de implementar a lista no Android, uma das atividades comuns é fazer com que as informações destas listas venham, por exemplo, de um servidor, muito conhecido também como API.
Considerando essas características, vou deixar alguns artigos que dão continuidade na App Ceep visando o conteúdo sobre APIs REST:
- Criando CRUD API com Spring Boot
- Consumindo CRUD API no Android com Retrofit
Conclusão
Nesse artigo aprendemos a implementar o RecyclerView
em um projeto Android com Kotlin.
Sendo mais específico, vimos passo a passo como fazemos a implementação do Adapter do RecyclerView e conseguimos compreender todas as peculiaridades que precisamos lidar durante a implementação como é o caso de:
- Definir o ViewHolder via generics no Adapter para que as funções implementadas utilizem como referência
- Criar o ViewHolder com uma Nested class para garantir que apenas o Adapter vai usá-la
- Utilização do Synthetic para facilitar a busca dos componentes do layout
- Garantir que o ViewHolder não seja
null
e depois realizar o processo de bind
Aproveite também e deixe o seu comentário sobre o que achou da implementação do RecyclerView
, como também, o seu feedback sobre o que achou do Kotlin e como ele nos ajuda durante o desenvolvimento.