Consumindo API REST no Android com Retrofit em Kotlin — Parte 2
No artigo onde implementamos o Retrofit para buscar todas as notas do servidor, fizemos uma configuração e utilização básica do Retrofit, ou seja, além de não realizar as outras tarefas de CRUD, no caso, inserção, alteração e remoção, deixamos o código um tanto quanto não apropriado para uma App.
Em outras palavras, neste artigo vamos melhorar o nosso código atual, e também, implementaremos a funcionalidade de enviar uma nota para a API. Bora começar?
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 😉
Analisando o código
Atualmente a implementação do callback está dentro do onCreate()
da Activity:
Uma responsabilidade um tanto quanto inapropriada para essa entidade da App. Sendo assim, o nosso objetivo inicial é isolar esse callback em um lugar específico.
Delegando a responsabilidade para uma entidade específica
Uma das técnicas que podemos aplicar, é criando uma classe específica para lidar com requisições de notas, como por exemplo, um Web Client.
Sendo assim, vamos criar a classe NoteWebClient
e, dentro dela, manderemos todo o código que faz a requisição para a função list()
:
Então, lá na Activity, podemos chamar essa função:
Entretanto, observe que da maneira que está, temos 2 problemas:
- A função
configureList()
precisa ser chamada dentro da Activity. - A
configureList()
pode ser chamada apenas quando o callback entrar na funçãoonResponse()
.
Pensando justamente neste cenário, como podemos implementar um comportamento dentro da Activity que vai ser executado no momento que o callback do Web Client for acionado?
Enviando uma interface via parâmetro
Uma das técnicas comuns, até mesmo em desenvolvimento Java, é enviar uma interface que vai ficar responsável em avisar a Activity quando, por exemplo, o callback responder.
“Mas como isso funciona na prática?”
Basicamente, podemos criar a interface chamada NoteResponse
que vai conter a assinatura success()
responsável em apenas receber uma lista:
Então, declaramos que a list()
do Web Client, recebe a interface NoteResponse
e, ao invés de chamar a função configureList()
, chamamos a success()
enviando a lista notes
:
Dessa forma, na Activity, somos capazes de implementar a interface da mesma maneira quando usamos classes anônimas do Java, como por exemplo, em listeners. Mas agora, vem a questão:
“Como implementamos uma classe anônima no Kotlin?”
Implementando interfaces com o Object Expression
Para obtermos o mesmo resultado de implementação de classes anônimas do Java no Kotlin, utilizamos a feature Object Expression:
Então veja que agora sobrescrevemos a função success()
quando é chamada de dentro do list()
.
Em outras palavras, todo o código que implementarmos dentro deste escopo, vai ser executado apenas quando o onResponse()
chamar a função success()
.
Isso significa que o parâmetro notes
é a lista que recebemos do servidor!
Portanto, podemos chamar a função configureList()
dentro da nossa implementação:
Testando a App, vemos que ainda temos o mesmo resultado, ou seja, o processo de delegar a responsabilidade deu certo! 😃
Essa solução que apresentei é conhecida como o Design Pattern Delegate.
Portanto, daremos continuidade na App fazendo com que ela insira uma nota na API também.
Começando com a implementação de envio de nota para a API
Para que seja possível o envio de notas precisamos, de alguma forma, realizar os seguintes passos:
- Criar um componente visual para iniciar o processo.
- Apresentar um formulário para o usuário.
- Declarar a requisição para envio da nota.
- Execução da requisição e atualização da lista de notas.
Considerando todos esses pontos, vamos implementá-los passo a passo.
Criando o componente para permitir a adição
Um componente bem comum para indicar que existe uma possibilidade de inserção é o FloatActionButton.
Porém, ele não vem por padrão no projeto, ou seja, adicionaremos a lib de suporte de design do Google por meio da seguinte dependência:
dependencies {
// demais dependências
implementation 'com.android.support:design:26.0.2'
}
Estou adicionando a versão 26.0.2 para manter a mesma compatibilidade com as outras libs de suporte da Google, ou seja, tente sempre usar a mesma versão para essas libs.
Após sincronizar com o Gradle, lá no layout, podemos adicionar o FAB (FloatingActionButton) da seguinte maneira:
Veja que a App apresenta o FAB, porém, é muito comum vermos este tipo de botão com um ícone, como por exemplo, uma cruzinha indicando inserção. Considerando este aspecto, vamos adicionar esta representação visual.
Inserindo o ícone de adição no FAB
Para adicionar um ícone, você pode baixar no site material.io, ou então, pode usar o próprio AS que ele adiciona pra você. De qualquer forma, o nome do ícone é add e ele vai ficar da seguinte maneira dentro do projeto:
Veja que ele fica dentro de main/drawable e para cada tamanho diferente ele adiciona um prefixo. Após conter o ícone dentro do projeto, basta apenas inserir pelo atributo src do FAB:
Fiz a inserção do ícone via AS
Então, executando a App, temos o seguinte resultado visual:
Pronto, finalizamos a parte visual para permitir a inserção de uma nota, vamos agora para o código!
Adicionando a tela para receber as informações do usuário
Uma das técnicas comuns para recebermos as informações do usuário seria abrir uma Activity nova que ficaria responsável em computar as informações da nota.
Entretanto, já que atualmente a nossa nota recebe apenas informações de título e descrição, podemos implementar uma solução mais enxuta e objetiva, ou seja, um Dialog.
O código de implementação do Dialog vai ficar da seguinte maneira:
Considerando que o foco do post não é a construção de Dialogs, preferi não dar tanta atenção nesta parte. Entretanto, se tiver dúvida, além do link que forneci durante o artigo, fique à vontade em perguntar
Apenas para deixar claro alguns parâmetros, o window.decorView
representa a view da Activity, por isso pode ser usado como o ViewGroup
para a função inflate()
.
Veja que estamos criando um Dialog baseando-se em um layout. Segue o código do layout:
Ao invés de um
EditText
simples, utilizei oTextInputEditText
para apresentar um aspecto visual comum no Material Design.
Criamos o código do Dialog, porém, se executarmos a App do jeito que está, o Dialog vai ser criado e apresentado logo de cara, ou seja, precisamos indicar que esse código do Dialog só vai ser executado apenas por uma ação, que no nosso caso, é clicando o FAB.
Portanto, vamos implementar a função setOnClickListener()
:
Veja que neste momento, foram apresentados alguns problemas de compilação na referência do this
. Isso acontece, pois na implementação do Object Expression o this
acaba se tornando o objeto da interface View.OnClickListener
.
Utilizando labels para referenciar o objeto this
Em outras palavras, precisamos indicar que o this
é uma referência à Activity, para isso, podemos utilizar uma label com o nome da classe que queremos referenciar:
Repare que agora o código volta a compilar, pois ao invés de referenciar o objeto da interface, estamos referenciando o objeto da Activity. Vamos testar a App e ver como ficou:
Note que agora temos uma caixinha que nos permite enviar informações das nossas notas. Entretanto, da maneira como está atualmente, não temos nenhuma opção para salvar a nota baseando-se nas informações que estão vindo do Dialog.
Adicionando botões no Dialog
Sendo assim, vamos adicionar um botão para salvar a nota chamando a função setPositiveButton()
do AlertDialog.Builder
:
Para essa função, precisamos enviar 2 parâmetros:
- Texto para ser apresentado.
- Implementação da interface
DialogInterface.onClickListener
.
Portanto, podemos realizar a seguinte implementação:
Veja que indicamos que o valor do texto da opção vai ser "Save"
e, por enquanto, só implementamos a interface por meio do Object Expression. Portanto, dentro da função onClick()
, vamos pegar as informações dos campos do Dialog.
Para isso podemos usar o Synthetic a partir da variável createdView
que é o objeto do tipo View
que tem acesso a todos os componentes do Dialog.
Agora que temos uma nota que está sendo criada a partir de uma interação com o usuário. Precisamos enviá-la para a API, mas, como podemos fazer isso?
Criando a requisição para enviar uma nota para a API
Para isso, o nosso primeiro passo é criar a representação da requisição, ou seja, vamos criar a assinatura insert()
na interface NoteService
:
Então, basta apenas anotarmos com @POST
para indicar que vai ser uma requisição do tipo POST e o caminho que é "notes"
:
Agora, precisamos indicar que nesta requisição, enviaremos uma nota. Mas como podemos indicar isso?
Enviando objetos nas representações de requisição do Service
Quando queremos enviar uma informação no Retrofit, o primeiro passo é enviar um objeto via parâmetro:
Entretanto, quando enviamos um parâmetro para um Service, precisamos indicar o que ele representa na requisição, ou seja, se ele faz parte, por exemplo, do corpo da requisição ou de um valor relacionado a um cabeçalho…
Como no nosso caso, precisamos enviar via corpo da requisição, podemos utilizar a annotation @Body
:
Por fim, indicamos que o retorno desta requisição será uma Call
para realizarmos a requisição, porém, o que esperamos de retorno do servidor para esta requisição?
Repara que na assinatura list()
esperamos uma lista de notas, certo? Mas lembra que no post onde implementamos a CRUD API ele nos devolve a própria nota? Claro, junto com o id que foi criado no servidor…
Sendo mais objetivo, isso significa que podemos indicar que o retorno é uma Call
que vai conter uma Note
:
Temos a nossa representação para inserir uma nota!
Declarando a função de inserção no Web Client
O nosso próximo passo, é implementar a função que vai fazer a chamada dentro do nosso NoteWebClient
, portanto, criaremos a função insert()
:
Nela, precisamos da nota que será enviada para a API, logo, pediremos via parâmetro:
Então, podemos realizar a requisição da mesma maneira como fizemos na função list()
:
Bacana, criamos a função que vai permitir enviar a nota que é criada pelo usuário. Entretanto, temos que chamar essa função após o momento que criamos o objeto de acordo com as informações que o usuário vai enviar:
Por mais que chamemos o Web Client, o que vai acontece com a tela do usuário caso seja enviado apenas para o servidor? Vamos testar?
Veja que não é apresentado nenhum erro, como também, o nosso usuário não vê a nota que ele adicionou na lista de notas! Mais engraçado ainda, é quando fechamos a App e abrimos de novo ou se simplesmente pedimos para o AS executá-la:
Repara que a nota foi salva e agora ela aparece!
Basicamente, não tomamos nenhuma ação após a finalização da requisição, como por exemplo, modificar a lista de notas.
Em outras palavras, da mesma maneira como fizemos na função list()
, precisamos também enviar uma interface na qual vai permitir com que tomemos uma ação assim que a requisição for realizada.
Recebendo a interface para permitir a implementação
Sendo assim, além de enviarmos o parâmetro note
vamos enviar o noteResponse
:
Então, basta apenas chamarmos a função success()
:
Entretanto, note que essa função exige que enviemos uma lista de notas, sendo que agora temos apenas um objeto do tipo Note
… E agora?
Utilizando o Generics para aumentar a flexibilidade
Quando entramos no caso em que precisamos utilizar uma mesma função que recebe um tipo específico, porém, em determinadas situações, ela precisa receber tipos diferentes, como é o caso da nossa success()
dentro da função insert()
.
Podemos aplicar o Generics da mesma maneira como faríamos no Java. Portanto, ao invés de receber uma lista de notas ou uma nota, a nossa função success()
vai receber um tipo genérico chamado t
:
E para deixar mais descritivo o que esse T
significa, vamos modificar o nome do parâmetro e deixar como response
:
Mas repara que neste ponto do código, o T
não compila!
Isso acontece, pois quando declaramos o Generics, precisamos também indicar no nome da interface que quem for usá-la, vai precisar especificar o tipo genérico que ela vai lidar.
Podemos indicar esse tipo de comportamento da seguinte maneira:
Veja que foi adicionado <T>
logo após o nome da interface.
Agora, em todo ponto de código que usar essa interface, vai precisar especificar o tipo, portanto, vamos ver como fica no nosso NoteWebClient
, primeiro vamos modificar na função list()
:
Veja que agora estamos recebendo por parâmetro um NoteResponse<List<Note>>
e conseguimos chamar o success()
enviando uma lista de notas! Mas como fica o código na função insert()
?
Repare que agora, também estamos utilizando a interface NoteResponse
, desta vez, enviamos via generics a classe Note
e, consequentemente, conseguimos chamar a função success()
enviando um objeto do tipo Note
!
Em outras palavras, criamos uma interface de resposta às callbacks genérica, inclusive, podemos até mesmo renomeá-la para CallbackResponse
, pois, dessa forma, podemos reutilizá-la com outros recursos:
Sempre que for renomear variáveis, funções, interfaces ou classes, considere o uso de recursos da IDE, como é o caso do Rename do AS, o atalho para utilizado é o Shift + F6 ou, via menu, Refactor > Rename…
Agora, na Activity, precisamos resolver o problema de compilação, o primeiro é justamente no momento que chamamos a list()
do nosso Web Client:
Observe que implementamos um CallbackResponse
sendo que agora, ele precisa ser mais específico, ou seja, basta apenas mudar para CallbackResponse<List<Note>>
:
Então, no momento que chamamos a função insert()
lá na Activity, basta apenas enviarmos como segundo parâmetro, uma implementação da interface CallbackResponse<Note>
:
Em seguida, precisamos apenas pegar esse objeto e adicionar na lista que contém todas as notas.
Entretanto, repara que atualmente a nossa lista está sendo recebida diretamente da resposta da requisição e sendo enviada para a função configureList()
que faz a configuração do RecyclerView
.
Portanto, para esta situação, podemos criar uma property para armazenar a lista de notas:
Repara que criamos uma property que inicialmente trata-se de uma lista vazia, ou seja, quando recebemos a lista como resposta de callback da função list()
, basicamente o que precisamos fazer, é adicionar todos os elementos nela.
Peculiaridades das Collections do Kotlin
Entretanto, as Collections do Kotlin possui uma característica bem diferente da abordagem vista no Java, pois a interface List
no Kotlin é imutável.
Isso significa que a interface List
não nos disponibiliza funções que alterem sua estrutura, como por exemplo, o add()
ou allAll()
.
“Então como podemos usar uma lista da maneira como fazemos no Java?”
Caso a nossa intenção é possuir uma lista mutável, o Kotlin nos fornece a interface MutableList
. Em outras palavras, podemos modificar a nossa property para ser uma lista mutável:
private val notes: MutableList<Note> = listOf()
Consequentemente, precisamos também criar uma instância de lista vazia para devolver uma lista mutável por meio da função mutableListOf()
:
private val notes: MutableList<Note> = mutableListOf()
Agora, podemos adicionar as listas que recebemos em resposta do callback da função list()
do Web Client:
Também, podemos adicionar a nota que recebemos de resposta do callback da função insert()
do Web Client:
Repara que da mesma forma temos que atualizar a lista do RecyclerView chamando a função configureList()
.
É válido observar que essa não é a maneira ideal para atualizarmos um RecyclerView, ou seja, normalmente lidamos diretamente com o Adapter notificando-o quando algo é alterado.
Pronto! Conseguimos configurar a nossa lista para que seja modificada assim que adicionarmos uma nota na API, vamos testar pra vê o que acontece?
Uhul! Agora a nossa App, além de pegar as notas da API, também consegue salvá-las!
Código fonte
Caso tenha tido alguma dúvida ou queira ter acesso ao código fonte, fique à vontade em consultar o projeto que deixei no GitHub.
Para saber mais
Por mais que tenhamos deixado o nosso código melhor em relação à primeira abordagem que fizemos, ainda existem pontos que podemos melhorar.
Um deles é fazendo com que o código do Dialog seja separado da mesma maneira como fizemos com o Retrofit e seus callbacks.
Outro ponto é que a implementação dos Callbacks são bem similares e o código é bem verboso… Existem técnicas para deixá-los cada vez mais resumidos! Interessado? Então dê uma olhada na terceira parte dessa série de Retrofit 😉
Conclusão
Neste artigo, aprendemos a separar os callbacks do Retrofit utilizando uma interface responsável em delegar a resposta da call. Também vimos que podemos ter o mesmo comportamento da implementação de classe anônima do Java utilizando o Object Expression.
Além disso, fomos capazes de implementar a funcionalidade para enviar uma nota para a API de tal forma que ela é atualizada na lista de notas. Nesta implementação, aprendemos a usar o FAB e a criar um Dialog. Então, vimos que temos a capacidade de enviar objetos no corpo da requisição com o Retrofit usando a annotation @Body
.
Por fim, aplicamos um pouco de generics para viabilizar o uso da interface de resposta, como também, vimos que as collections no Kotlin são imutáveis por padrão.
O que achou do conteúdo que foi abordado neste artigo? Deixe o seu comentário 😄