Consumindo API REST no Android com Retrofit em Kotlin — Parte 3
No segundo artigo de Retrofit com Kotlin, conseguimos implementar a funcionalidade para adicionar notas na API.
Também vimos, algumas técnicas para isolar os callbacks do Retrofit. Além disso, ainda faltaram algumas implementações, como por exemplo, alterar e remover notas.
Entretanto, antes de começarmos a implementar essas features, o nosso foco neste artigo é analisar o código atual e aplicar algumas técnicas de refatoração considerando algumas novidades que temos no Kotlin.
Se prepare que teremos bastante coisa para melhorar! Let’s go!
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 contexto da App
Atualmente a nossa App permite a inserção de uma nota por meio de um dialog que apresenta um mini formulário para o usuário adicionar as informações de título e descrição…
Isso significa que podemos utilizar o mesmo aspecto visual para alterar uma nota também, ou seja, faz todo o sentido reutilizarmos o dialog que criamos para adicionar a nota… Vamos dar uma olhada na implementação:
Um tanto quanto acoplada com a Activity, né? Em outras palavras, antes mesmo de possibilitarmos a alteração de notas, vamos aplicar o mesmo processo de delegação de responsabilidade que fizemos nos callbacks.
Delegando responsabilidade do dialog
Para isso, podemos criar a classe AddNoteDialog
enviando todo o código do dialog que implementamos na Activity:
Repare que criamos a função show()
que vai encapsular todo o código do dialog, mas, ao realizar essa mudança, são apresentados diversos problemas de compilação! Não se preocupe, vamos resolver cada um deles!
A primeira ação que podemos tomar é resolvendo algumas referências que eram acessíveis na Activity, como por exemplo, o this
para o Context
, como também, oViewGroup
para criar o layout.
Recebendo as properties via construtor primário
Para isso, podemos receber via construtor primário todas as referências necessárias:
Muito bom! Resolvemos as referências, mas repara que existe um código que precisa ser executado na Activity:
NoteWebClient().insert(note, object : CallbackResponse<Note> {
override fun success(note: Note) {
this@NoteListActivity.notes.add(note)
configureList()
}
})
Como já vimos anteriormente, podemos delegar essa responsabilidade utilizando uma interface, certo? Entretanto, concorda que esse tipo de comportamento tende a crescer?
Em outras palavras, se tivermos que criar uma interface nova cada vez que for necessário delegar uma responsabilidade, a quantidade de arquivos dentro do nosso projeto tende a aumentar!
Claro, a princípio, não vemos uma outra alternativa viável com o que conhecemos atualmente, concorda?
Porém, no Kotlin temos uma feature muito bacana que nos permite delegar a responsabilidade da mesma maneira como fizemos até o momento, mas, sem a necessidade de criar interfaces novas!
Delegando responsabilidade com Higher-Order Function
Basicamente, ao invés de enviar uma interface, podemos enviar uma função via parâmetro da seguinte maneira:
fun show(created: (createdNote: Note) -> Unit) {
}
Bem diferente do que vimos até o momento, né? Não se preocupe, vamos entender detalhadamente o que aconteceu no nesse trecho de código.
Veja que a princípio, temos o parâmetro created
que indica o nome da função que enviamos.
Só que além do nome da função, também estamos indicando que ela recebe um o parâmetro createdNote
que é um objeto do tipo Note
. Então, temos também uma flechinha seguindo da classe Unit
que indica o tipo de retorno dessa função.
De forma resumida, declaramos que a função created
vai receber uma nota e não vai devolver nada. Quando temos esse tipo de parâmetro, tecnicamente, temos uma Higher-Order Function.
“Legal, mas o que posso fazer com isso?”
Em outras palavras, trata-se de uma função que nos permite chamá-la em um ponto específico do código da mesma maneira como fizemos com a nossa interface.
Sendo assim, podemos fazer a seguinte chamada para entregar a nota criada pelo usuário:
Delegamos a responsabilidade da mesma maneira que fizemos antes! A diferença é que agora não criamos uma nova interface. Nesse momento você deve estar pensando:
“Mas como fica essa chamada lá na Activity?”
Implementando Higher-Order Function com expressão lambda
Na Activity, criamos uma instância da classe AddNoteDialog
e o próprio auto complete do Android Studio já nos sugere a seguinte implementação para a função show()
:
Essa syntax na qual abrimos um escopo de função em uma chamada de função é conhecida como expressão lambda ou funções anônimas.
Isso significa que todo o código que colocarmos dentro desta expressão lambda será executado quando a HOF (Higher-Order Function) created()
for chamada.
Porém, considerando que a nossa HOF está recebendo um objeto do tipo Note
, como acessamos ele?
Apelidando parâmetros na expressão lambda
Uma das técnicas iniciais é apelidando o parâmetro da HOF da seguinte maneira:
AddNoteDialog(this@NoteListActivity,
window.decorView as ViewGroup)
.show { createdNote ->
}
Repare que apelidamos com o mesmo nome do parâmetro da HOF, mas poderia, por exemplo, ser note
apenas:
AddNoteDialog(this@NoteListActivity,
window.decorView as ViewGroup)
.show { note -> }
Então, basta apenas a gente utilizar esse objeto que apelidamos na função desejada:
Observe que temos exatamente o mesmo comportamento que fizemos com a interface, porém de uma maneira mais objetiva e sem a necessidade de criar novas interfaces!
E se eu te contar que da pra simplificar mais ainda?
Utilizando o it em expressões lambda
Quando entramos nesses casos, ou seja, de uma HOF com apenas um único parâmetro, temos a capacidade de utilizar o it
da mesma maneira como fizemos no let
na implementação do adapter do RecyclerView.
AddNoteDialog(this@NoteListActivity,
window.decorView as ViewGroup)
.show {
this@NoteListActivity.notes.add(it)
configureList()
}
Em outras palavras, nesse contexto, o it
é justamente a nota que foi criada pelo usuário, bem mais simples, né?
Aproveitando a menção do
let
, se você verificar a implementação dele, verá que ele recebe uma HOF também 😅
Implementando interfaces do Java com SAM Conversion
Além da HOF, temos a capacidade de utilizar expressões lambda na implementação de interfaces do Java, ou seja, o nosso famoso View.OnClickListener
pode ser implementado da seguinte maneira:
Assim como vemos no Java 8, essa técnica também é conhecida como SAM Conversion, sendo que o SAM vem de Single Abstract Method, ou na tradução, conversão de método abstrato único.
O próprio AS sugere a conversão de Object Expression para SAM Conversion quando trata-se de uma interface do Java.
Um detalhe importante sobre a SAM Conversion é que trata-se de uma exclusividade de interfaces do Java que possuem apenas uma única assinatura, por isso foi possível converter o View.OnClickListener
.
Considerando essa informação, é válido notar que não podemos fazer a SAM Conversion nas interfaces declaradas em Kotlin, como é o caso da CallbackResponse
. 😢
Em outras palavras, caso seja desejada a implementação via expressão lambda, faz todo sentido utilizar HOFs, ainda mais quando nosso objetivo é aplicar o Delegate, portanto, vamos modificar o nosso NoteWebClient
:
Então, na nossa Activity, podemos implementar a HOF da função list()
com expressão lambda:
E na AddNoteDialog
, implementamos a função insert()
da mesma maneira:
Inclusive, veja que implementamos a interface do Java DialogInterface.OnClickListener
que possui apenas uma única assinatura, logo, podemos aplicar a SAM Conversion também:
DialogInterface.OnClickListener
Bem sucinto, concorda? Note que nessa conversão os apelidos foram mantidos, por que isso aconteceu?
É justamente por que temos mais de um parâmetro na assinatura da interface DialogInterface.OnClickListener
, ou seja, nesses casos somos obrigados a deixá-los na expressão lambda.
Mas repara que nenhum deles é utilizado, certo? Isto é, temos um código que não tem utilidade e temos que interpretá-lo…
Renomeando apelidos que não são utilizados
Para esse tipo de situação, o Kotlin nos permite renomeá-los com _
para indicar que eles não estão sendo utilizados:
.setPositiveButton("Save") { _, _ ->
// restante do código
}
Agora, nem precisamos tentar compreender o que eles significam, pois eles não possue relevância para nossa implementação.
Com esses ajustes da nossa refatoração, podemos até mesmo remover a interface CallbackResponse
, pois as HOFs já nos atendem muito bem.
Escopo de expressões lambdas
Repara que na Activity, fazemos diversas referências ao this
com a label para poder indicar que refere-se à NoteListActivity
, pois, quando estávamos no escopo do Object Expression, foi necessário para poder distinguir da referência da interface.
Entretanto, quando utilizamos expressões lambda, o escopo é um pouquinho diferente.
“Como assim?”
Dentro do escopo de uma expressão lambda, temos o mesmo acesso de quem a implementa, neste caso, da NoteListActivity
, seja por implementação da HOF ou da SAM Conversion.
Isso significa que podemos chamar funções ou properties da NoteListActivity
, e também, não precisamos mais utilizar as labels! Portanto, podemos chamar o this
sem nenhum problema:
Outro detalhe importante, é que não temos outra variável com o mesmo nome da property notes
, logo, não precisamos mais da chamada do this
para referenciá-la dentro das expressões lambdas das funções list()
e show()
:
NoteWebClient().list {
notes.addAll(it)
configureList()
}
fab_add_note.setOnClickListener {
AddNoteDialog(window.decorView as ViewGroup, this)
.show {
notes.add(it)
configureList()
}
}
Veja como o nosso código ficou mais simples! 😄
Closures e expressões lambdas
Legal, mas provavelmente você deve estar pensando:
“Se o escopo da HOF eleva para quem a chama, significa que todos os membros da Activity terão acesso às variáveis que surgiram dentro da expressão lambda?”
Neste caso, a resposta é não, ou seja, o objeto it, apelidos ou até mesmo variáveis que são criadas dentro da expressão lambda, só são acessíveis dentro do escopo dela.
Em outras palavras, todo ambiente contido dentro de uma expressão lambda envolvendo essas peculiaridades de escopo, tecnicamente, é conhecido como Closure.
Melhorando o código dos callbacks do Retrofit
Fizemos uma grande refatoração no nosso código e ele ficou bem mais enxuto comparadando com a nossa implementação inicial. Entretanto, veja como estão os callbacks do Retrofit atualmente:
Um tanto quanto grande, né? Será que existe alguma técnica que podemos aplicar aqui também?
Considerando tudo que vimos até o momento, podemos também delegar a responsabilidade de callback para uma classe específica, como por exemplo, a RetrofitCallback
:
package br.com.alexf.ceep.retrofit.callback
class RetrofitCallback {
}
Então, dentro dela, declarmos a função callback()
que, internamente, vai fazer toda implementação da interface Callback
:
class RetrofitCallback {
fun callback() {
}
}
Por mais que a ideia seja boa, repare que a implementação de ambos os callbacks que fizemos, tiveram valores de objetos diferentes, como por exemplo, o de inserção esperava uma nota e o de busca uma lista de notas, ou seja, para atender às duas necessidades teríamos que fazer algo do gênero:
Uma função para cada tipo diferente… Algo nada apropriado, concorda?
Afinal, se amanhã surgir mais um modelo, teremos que implementar mais uma nova callback()
… Já vimos algo parecido no nosso CallbackResponse
, lembra?
Criando funções genéricas
Ou seja, a ideia é fazer com que a implementação da interface Callback
seja genérica também! Para isso, podemos fazer com que a função callback()
seja genérica da seguinte maneira:
Então, da mesma maneira como fizemos anteriormente, podemos enviar uma HOF de sucesso, que por sinal, será genérica também:
Então, no nosso Web Client, podemos chamar esse callback da seguinte maneira:
Veja o quão mais simples ficou! Agora basta apenas tratar o response
assim como fazíamos:
Repare que agora é bem mais simples de implementar um comportamento após a chamada de um callback do Retrofit.
Casos em que não há necessidade de usar classes
Perceba que a classe RetrofitCallback
é uma classe muito genérica que não possui nenhum tipo de encapsulamento como properties e funções privadas e, além disso, a única função que ela possui (callback()
) também é bem genérica…
Sendo mais objetivo, faz sentido mantermos essa classe?
Já que o Kotlin vem com a proposta de ser multi paradigma, tanto para o lado OO como para o funcional, temos a capacidade de declarar funções sem a necessidade de uma classe:
Então, podemos chamar a callback()
da dentro do nosso Web Client seguinte maneira:
Note que já modifiquei para utilizar o objeto
it
diretamente.
Repare que estamos fazendo o import da função diretamente! Podemos até fazer uma melhoria, ou seja, deixar o arquivo desta função dentro do pacote retrofit apenas para não ficar uma chamada ambígua:
package br.com.alexf.ceep.retrofit
// imports
fun <T> callback(response: (response: Response<T>?) -> Unit): Callback<T> {
// restante do código
}
Mudamos bastante coisa no nosso código e quando executamos tudo que havíamos feito antes é mantido, a diferença é que agora o nosso código ficou muito mais flexível no sentido de reutilização e facilidade de leitura.
Permitindo ações para situações de falha
Um último ponto importante, é que o nosso callback genérico permite apenas o tratamento dos casos que dão sucesso, mas, quando entramos no onFailure()
não temos como tomar uma ação!
Sendo assim, faz todo o sentido enviarmos também uma HOF em casos de falha:
Então, no Web Client, basta apenas implementarmos:
Da mesma maneira, podemos disponibilizar ações para casos de falha para quem chama o Web Client:
Então, tanto a Activity, como no dialog, somos capazes de executar alguma ação caso surgir esse ponto de falha:
Agora, ao executar a nossa App em casos que falham, o nosso usuário recebe um feedback do que aconteceu. Podemos testar recebendo um timeout:
Repare que agora quando a conexão falha o Toast aparece! Portanto, o nosso usuário fica sabendo do problema quando ele acontecer.
Caso queira fazer o mesmo teste de timeout, basta aplicar o seguinte código na classe que inicializa o Retrofit:
Código fonte
Se tiver interesse em consultar o código fonte que foi desenvolvido durante o artigo, fique à vontade em consultar o repositório no GitHub.
Para saber mais
Por mais que tenhamos criado uma implementação de callback genérico, ainda existem outras variações de callbacks genéricos que podemos criar, como por exemplo esta outra variação:
Então, para implementá-la, usamos a chamada de expressão lambda sem os parênteses da mesma maneira que fazemos na SAM Conversion:
call.enqueue(callback { response, thorwable ->
response?.let {
// ação caso o response for retornado
}
thorwable?.let {
// ação caso o throwable for retornado
}
})
Fica até menos verboso, né? Inclusive, é possível até mesmo aplicar a Single-Expression Function nesse caso, pois tanto as funções do Callback
como a HOF, retornam Unit
!
Existem outras variações e até mesmo personalizações, e pra isso vou deixar a imaginação de vocês tomar conta 😅
E que tal avançar mais com o Retrofit no Kotlin? Gostou da ideia? Então dê uma olhada na quarta parte desta série de artigos:
Conclusão
Neste artigo aprendemos diversas técnicas durante a refatoração dentro do Kotlin, como a SAM Conversion, expressão lambda, HOF e funções genéricas. Vimos que cada uma delas nos fornecem um poder e tanto na hora de refatorar e deixar o código flexível.
Inclusive, agora fomos capazes de permitir que que chama o Web Client também seja capaz de lidar com situações nas quais as requisições falham.
Aproveitando, me conta o que achou do conteúdo e deixe o seu feedback nos comentários 😃