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!

Jim Carrey na fúria

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:

Código atual da Activity de lista de notas

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:

Enviando o código do dialog para uma classe específica

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:

Recebendo properties via construtor primário

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!

Gato surpreso

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:

Chamando a Higher-Order Function quando a nota é criada

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():

Implementando a Higher-Order Function na chamada da 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:

Adicionando a nota recebida na lista de notas e atualizando o RecyclerView

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:

Realizando SAM Conversion na interface View.OnClickListener

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:

Substituindo a interface CallbackResponse por HOF

Então, na nossa Activity, podemos implementar a HOF da função list() com expressão lambda:

Implementando a função list com expressão lambda

E na AddNoteDialog, implementamos a função insert() da mesma maneira:

Implementando a função insert com expressão lambda

Inclusive, veja que implementamos a interface do Java DialogInterface.OnClickListener que possui apenas uma única assinatura, logo, podemos aplicar a SAM Conversion também:

Aplicando a SAM Conversion na interface 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:

Acessando o this da Activity dentro da expressão lambda

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:

Alternativa para delegar responsabilidade do callback

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:

Implementação genérica de callback

Então, da mesma maneira como fizemos anteriormente, podemos enviar uma HOF de sucesso, que por sinal, será genérica também:

Adicionando HOF no callback genérico

Então, no nosso Web Client, podemos chamar esse callback da seguinte maneira:

Chamando o callback genérico no Web Client

Veja o quão mais simples ficou! Agora basta apenas tratar o response assim como fazíamos:

Tratando a resposta do callback genérico

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:

Chamando a função callback tanto na list com na insert
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:

Adicionando a HOF para casos em que o callback falha

Então, no Web Client, basta apenas implementarmos:

Implementando o HOF de falha no Web Client

Da mesma maneira, podemos disponibilizar ações para casos de falha para quem chama o Web Client:

Adicionando a HOF de falha no Web Client

Então, tanto a Activity, como no dialog, somos capazes de executar alguma ação caso surgir esse ponto de falha:

Adicionando toast no ponto de falha do web client
Adicionando toast no post de falha do web client

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:

Testando a App com 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 que modifica o tempo de timeout do 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 😅

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 😃

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.