Consumindo API REST no Android com Retrofit em Kotlin — Parte 4

Rapaz com fones de ouvido

No artigo anterior, realizamos uma refatoração na qual aumentou a nossa flexibilidade no código, como por exemplo, agora utilizamos uma técnica genérica para callback por meio das Higher-Order Functions.

Entretanto, vimos que ainda faltou a implementação tanto da remoção como da alteração das nossas notas, sendo assim, neste artigo implementaremos a feature que vai permitir a alteração de notas! Preparado? Então bora começar.

Coringa em “E aqui vamos nós”

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

Atualmente a nossa App lista todas as nossas notas que estão na API e, quando tocamos no FAB, ela abre um dialog que permite adicionar as informações da nota, então, quando tudo dá certo, a nota é apresentada na lista de notas…

Considerando essa análise, qual técnica podemos aplicar para permitir a alteração de uma nota?

Uma possibilidade seria adicionar um evento de clique em cada uma das notas que, ao serem tocadas, abriria novamente o mesmo dialog, porém, com as informações da nota e, ao invés de salvar uma nota, alteraria a nota na API.

“Mas como podemos fazer tal implementação?”

Basicamente, temos as implementações a serem consideradas:

  • Requisição que permita alterar uma nota que já exista.
  • Dialog de alteração para alterar a nota desejada.
  • Listener de clique de cada item da lista de notas para pegar a nota que precisa ser alterada.

Considerando cada uma das necessidades, o nosso primeiro passo será criar a representação da requisição que vai permitir alterar a nota na API.

Implementando a requisição para alterar a nota

Dentro da interface NoteService, começaremos criando a função alter() que vai receber uma nota, realizar uma requisição do tipo PUT e devolverá a nota alterada:

@PUT("notes")
fun alter(@Body note: Note): Call<Note>

Entretanto, como vimos no artigo onde criamos a API com o Spring Boot, o endpoint que faz essa alteração da nota, além de esperar o JSON, espera uma URL no seguinte formato:

notes/id_da_nota

Em outras palavras, além de enviarmos a nota, precisamos também enviar esse id que pode ser um valor variável…

“Como podemos fazer isso no Retrofit?”

Enviando um valor variável no caminho da URL

Para isso, o Retrofit nos disponibiliza a annotation @Path que permite adicionar esse tipo de informação, portanto, podemos modificar o código e deixar da seguinte maneira:

Representação da requisição para alterar uma nota

Note que adicionamos a informação {id} na URL indicando que naquela parte do endereço vai surgir um valor variável com a chave id, então, enviamos o valor "id" como parâmetro da @Path.

Essa abordagem é necessária para que o Retrofit consiga fazer o bind do valor que vai surgir na URL com o parâmetro anotado, ou seja, o id: Int.

Ajustando o Web Client para realizar a requisição de alteração

Em seguida, criaremos a função alter() dentro do Web Client para realizar a requisição. Faremos uma implementação bem similar às funções list() e insert():

fun alter(note: Note, success: (note: Note) -> Unit,
failure: (throwable: Throwable) -> Unit) {
RetrofitInitializer().noteService().alter(note)
}

Quando realizamos essa chamada temos um problema, pois precisamos enviar o id da nota, mas não temos essa informação até o momento… E agora?

Adicionando o id na nota

Basicamente, podemos alterar o nosso modelo para que ele tenha um id:

class Note(
val id: Int,
val title: String,
val description: String)

Entretanto, com apenas esse ajuste, temos um problema de compilação em alguns pontos do nosso código, pois a nossa App até o momento não realiza instância de notas com a informação de id!

Portanto, precisamos adicionar um valor padrão para a property id que não seja válido na API, por exemplo, 0:

class Note(
val id: Int = 0,
val title: String,
val description: String)

Também, temos que alterar a forma como instanciamos a classe Note na função show() do dialog para que utilize o named parameter:

Modificando a instância da classe Note com o named parameter

Com o id disponível, podemos enviá-lo via parâmetro no momento que chamamos o service:

fun alter(note: Note, success: (note: Note) -> Unit,
failure: (throwable: Throwable) -> Unit) {
RetrofitInitializer().noteService().alter(note, note.id)
}

Então, podemos executar a call da mesma maneira como fizemos nas demais funções:

Realizando a chamada da requisição de alteração

Agora, precisamos apenas realizar essa chamada na Activity, justamente no momento em que o nosso usuário realiza a ação de alterar uma nota. Em outras palavras, faremos essa implementação agora!

Analisando a situação atual para permitir a alteração da nota

Em uma pequena análise, podemos concluir que o nosso próximo passo é implementar o evento de listener no RecyclerView para que nos possibilite tomar uma ação caso uma nota seja tocada, certo?

Entretanto, por padrão, a API do RecyclerView possui funções de evento de clique apenas para a própria view, ou seja, não somos capazes de utilizar uma função de listener para cada um dos itens assim como vemos na ListView no modo padrão… E agora?

Já que o RecyclerView não possui esse tipo de comportamento, somos obrigados a implementá-lo! Portanto, daremos início nesta implementação.

Adicionando evento no Adapter do RecyclerView

Como vimos, a classe NoteListAdapter é a responsável por criar cada uma das views do RecyclerView, portanto, ela é uma forte candidata para realizarmos tal implementação… Mas por onde podemos começar?

Lembra que a classe ViewHolder tem a finalidade de representar cada uma das views que são criadas?

Isso significa que podemos, por exemplo, implementar um evento de clique a partir do parâmetro itemView que realmente representa a view da ViewHolder. Uma possível implementação seria a seguinte:

Adicionando função de clique no ViewHolder

Note que, dessa forma, no momento que criamos cada uma das views a partir da função onBindViewHolder(), já chamamos a função que vai realizar o evento de clique.

Entretanto, desta maneira, não somos capazes de delegar a ação do clique para quem está chamando o adapter, que no nosso caso, é a Activity.

Em outras palavras, podemos realizar o mesmo processo de delegate que vimos no post anterior.

Utilizando Higher-Order Function para delegar o evento do clique

Sendo assim, indicaremos que o adapter vai receber uma HOF que equivale ao evento de clique de cada um dos itens:

class NoteListAdapter(private val notes: List<Note>,
private val context: Context,
private val onItemClickListener: () -> Unit) : Adapter<NoteListAdapter.ViewHolder>() {
    // restante do código
}

Então, basta apenas enviar a HOF via parâmetro da função onClick() e executá-la:

Delegando ação de clique com a HOF

Dessa forma, na chamada do adapter, somos obrigados a implementar a ação do clique:

Implementando a HOF do adapter na Activity

Testando essa pequena implementação, temos o seguinte resultado:

Apresentando o Toast quando tocar na nota

Veja que funciona! Portanto, precisamos apenas implementar o código que vai pegar a nota que foi tocada e enviá-la para a API.

Pegando a nota que foi tocada

Uma das técnicas é indicar que a nossa HOF vai receber uma nota, pois, dessa forma, fazemos com que o próprio adapter fique responsável em mandar essas informações:

class NoteListAdapter(
private val notes: List<Note>,
private val context: Context,
private val onItemClickListener: (note: Note) -> Unit) : Adapter<NoteListAdapter.ViewHolder>() {

Então, enviamos a nota via parâmetro da função onClick() que é a responsável em executar a HOF:

class NoteListAdapter(
private val notes: List<Note>,
private val context: Context,
private val onItemClickListener: (note: Note) -> Unit) : Adapter<NoteListAdapter.ViewHolder>() {

override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
val note = notes[position]
holder?.let {
it
.bindView(note)
it.onClick(note, onItemClickListener)
}
}

// restante do código

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

// restante do código

fun onClick(note: Note, execute: (note: Note) -> Unit) {
itemView.setOnClickListener {
execute(note)
}
}

}

}

Note que do jeito que enviamos funciona, porém, podemos deixar mais objetivo chamando o itemView diretamente:

override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
val note = notes[position]
holder?.let {
it
.bindView(note)
it.itemView.setOnClickListener {
onItemClickListener(note)
}
}
}

Dessa forma, podemos remover a função onClick() dentro do ViewHolder, pois não há mais a necessidade da mesma 😄

Agora que temos a nota, basta apenas modificar a expressão lambda que implementa a HOF para que ela receba uma nota:

recyclerView.adapter = NoteListAdapter(notes, this) { note ->

}

Com a nota em mãos, precisamos apenas implementar o dialog que vai permitir que o usuário altere a nota que foi tocada. Portanto, vamos modificar a nossa classe AddNoteDialog para que ela permita alterar uma nota também!

Modificando a classe do dialog para que permita a ação de adicionar e alterar uma nota ao mesmo tempo

Entretanto, veja que o nome dela atualmente é AddNoteDialog sendo que além de adicionar ela via permitir a alteração de uma nota, logo, faz todo o sentido renomeá-la para NoteDialog apenas:

class NoteDialog(private val viewGroup: ViewGroup,
private val context: Context) {
// restante do código
}

Com esse ajuste, podemos começar a implementação criando a função alter() que vai receber uma nota:

fun alter(note: Note){

}

Em seguida, basta apenas inicializar os campos do dialog, entretanto, veja que tanto a view que foi criada como os campos, estão acessíveis somente dentro da função show()… E agora?

Refatorando o código da classe de dialog

Bom, podemos realizar uma refatoração para que a função alter() tenha acesso também a todos os membros necessários. Logo, vamos transformar cada um deles em properties:

classe de dialog com as properties

Repare que para inicializar as properties que representam os campos do dialog, foi necessário criar a property createdView que está sendo inicializada pela função createView(). Esta função foi extraída a partir de um bloco de código que estava dentro da função show().

Agora, dentro da função alter(), podemos realizar a inicialização dos campos:

fun alter(note: Note) {
titleField.setText(note.title)
descriptionField.setText(note.description)
}

Desta vez, ao invés de utilizar a property text utilizamos a função setText(). Isso tem um motivo que faz sentido!


Mais detalhes sobre a interoperabilidade

O campo que estamos lidando trata-se de um EditText que é uma classe implementada em Java e, seus métodos de acesso para o atributo text (já que estamos falando de Java hehe) possuem valores diferentes!

Em outras palavras, o setText() recebe um CharSequence e o getText() retorna um Editable.

“Mas o que isso implica? ”

Nesse tipo de situação o Kotlin não consegue converter o os métodos de acesso para uma property da mesma maneira como acontece na TextView, por exemplo.


Implementando o dialog na função de alteração de nota

Enfim… Continuando com a nossa implementação, basicamente precisamos chamar o dialog novamente, certo? Portanto podemos realizar os mesmos passos que fizemos na função show():

fun alter(note: Note) {
titleField.setText(note.title)
descriptionField.setText(note.description)
AlertDialog.Builder(context)
.setTitle("Alter note")
.setView(createdView)
.setPositiveButton("Save") { _, _ ->

}
.show()
}

Note que a única diferença é que foi modificado o título do dialog para "Alter note". Em seguida, precisamos criar novamente a nota da mesma maneira como fizemos no dialog de adição:

fun alter(note: Note) {
titleField.setText(note.title)
descriptionField.setText(note.description)
AlertDialog.Builder(context)
.setTitle("Alter note")
.setView(createdView)
.setPositiveButton("Save") { _, _ ->
val title = titleField.text.toString()
val description = descriptionField.text.toString()
val alteredNote = Note(title = title, description = description)
}
.show()
}

Implementando a requisição de alteração

Com a nota alterada, basta apenas chamar o Web Client e realizar a requisição:

val title = titleField.text.toString()
val description = descriptionField.text.toString()
val alteredNote = Note(title, description)
NoteWebClient().alter(alteredNote, {

}
, {

}
)

Entretanto, veja que não temos uma ação a ser tomada quando chega a resposta ou se falha…

Como vimos anteriormente, delegamos a resposta de sucesso para quem chama a função show() e apresentamos uma mensagem de falha quando a resposta apresenta algum erro.

Delegando a resposta com HOF

Isso significa que podemos aplicar a mesma implementação nesta função também!

fun alter(note: Note, altered: (alteredNote: Note) -> Unit) {
titleField.setText(note.title)
descriptionField.setText(note.description)
AlertDialog.Builder(context)
.setTitle("Alter note")
.setView(createdView)
.setPositiveButton("Save") { _, _ ->
val title = titleField.text.toString()
val description = descriptionField.text.toString()
val alteredNote = Note(title, description)
NoteWebClient().alter(alteredNote, {
altered(it)
}, {
Toast.makeText(context, "Falha ao alterar nota", Toast.LENGTH_LONG).show()
})
}
.show()
}

Pronto, a função de alteração foi implementa! Entretanto, é importante ressaltar que a função show() está um tanto quanto estranha, pois antes a nossa classe ese chamada AddNoteDialog e agora é NoteDialog.

Ou seja, a função show() na verdade está adicionando uma nota, logo, faz todo o sentido ela se chamar, por exemplo, add():

Ajustando o nome da função show para add
Existem outros pontos de refatoração que podemos considerar nesta classe, porém, vou deixar para o próximo artigo 😉

Observe que agora o nosso código apresenta muito mais significado para o que cada um dos membros estão fazendo. Agora, podemos chamar o nosso dialog na expressão lambda da HOF do adapter:

recyclerView.adapter = NoteListAdapter(notes, this) { note ->
NoteDialog(window.decorView as ViewGroup, this).alter(note) {

}
}

Então, precisamos alterar a nota de acordo com a resposta que estamos recebendo. Para isso, podemos usar a função set() da List. Porém, esta função exige que enviemos como primeiro parâmetro a posição do item que queremos alterar, sendo que só temos apenas a nota… E agora?

Pegando a posição da nota que foi alterada

Basicamente, podemos alterar a assinatura da HOF para que envie também uma posição:

class NoteListAdapter(
private val notes: List<Note>,
private val context: Context,
private val onItemClickListener: (note: Note, position: Int) -> Unit) : Adapter<NoteListAdapter.ViewHolder>()

Com essa modificação, basta apenas enviar o parâmetro position da função onBindViewHolder():

Recebendo a posição na HOF

Agora, basta apenas receber o parâmetro position na expressão lambda e alterar a nota:

recyclerView.adapter = NoteListAdapter(notes, this) { note, position ->
NoteDialog(window.decorView as ViewGroup, this).alter(note) {
notes.set(position, it)
configureList()
}
}

Implementando o set de maneira indexável

Repare que o AS tem uma sugestão para substituir a implementação da função set() para um modo indexável:

NoteDialog(window.decorView as ViewGroup, this).alter(note) {
notes[position] = it
configureList()
}

Note que é uma implementação bem similar ao que vemos em arrays. Com esses ajustes, podemos testar a nossa App e ver o que acontece:

Após tocar em save no dialog de alteração a nota não é alterada

Repara que o dialog aparece com o título indicando a alteração da nota, também, todas as informações da nota tocada são apresentadas, entretanto, ao alterar a nota nada acontece! Por que será!? 😕

Cuidados que precisam ser tomados

Para entendermos esse detalhe, basta apenas verificarmos a nota que estamos enviando para a requisição:

val title = titleField.text.toString()
val description = descriptionField.text.toString()
val alteredNote = Note(title = title, description = description)
NoteWebClient().alter(alteredNote, {
altered(it)
}, {
Toast.makeText(context, "Falha ao alterar nota", Toast.LENGTH_LONG).show()
})

Observe que criamos uma nota nova baseando-se nas informações que estão vindo nos campos do dialog, até aí nenhum problema, porém, vamos verificar novamente a implementação da função alter() do nosso Web Client:

fun alter(note: Note, success: (note: Note) -> Unit,
failure: (throwable: Throwable) -> Unit) {
val call = RetrofitInitializer().noteService().alter(note, note.id)
call.enqueue(callback({ response ->
response?.body()?.let {
success(it)
}
}
, { throwable ->
throwable?.let {
failure(it)
}
}
))
}

A nota que é recebida é enviada para a função alter() do service, mas, além dela, também enviamos o id. Mas agora eu te pergunto:

"Se estamos criando uma nota nova sem informar o id, qual é o valor do id dela?”

É fácil de responder, né? Claro que é 0, ou seja, um id inválido no qual a API não vai responder da maneira esperada…

Para esse tipo de situação temos algumas alternativas:

  • Tornar as properties mutáveis para alterar a própria nota que enviamos -> Por mais que seja válido, estaremos colocando um comportamento que não é desejado apenas por um caso específico.
  • Tornar a property id mutável e alterar o valor da mesma quando criar a nota nova a partir do parâmetro recebido -> É válido também, entretanto, lembra que criamos a property id do jeito que está justamente para evitar que alguém a altere sendo que não é um comportamento esperado?
  • Utilizar a estrutura de uma data class da mesma maneira como fizemos na API para copiar a nota modificando apenas as properties desejadas -> Repare que esta solução é bem válida, porém, para que ela funcione, seremos obrigados a manter a property id como parâmetro do construtor primário (que atualmente já está assim), pois a data class só implementa as funções padrões baseando-se nas properties que são declaradas via construtor primário.

Repare que temos um tremendo trade-off, pois ambas as alternativas possuem vantagens e desvantagens…

Motivos por considerar a data class nesse cenário

Dentre elas vou optar pela data class, pois em ambas as alternativas, correremos o risco da modificação, porém, neste caso, a modificação só é feita no momento que construir a nota!

Além disso, a data class facilita bastante o processo de cópia de uma referência! Portanto, vamos alterar a nossa classe Note:

data class Note(
val id: Int = 0,
val title: String,
val description: String)

Agora, na função alter(), criamos a nova nota por meio da função copy():

fun alter(note: Note, altered: (alteredNote: Note) -> Unit) {
titleField.setText(note.title)
descriptionField.setText(note.description)
AlertDialog.Builder(context)
.setTitle("Alter note")
.setView(createdView)
.setPositiveButton("Save") { _, _ ->
val title = titleField.text.toString()
val description = descriptionField.text.toString()
val alteredNote = note.copy(title = title, description = description)
// restante do código
.show()
}

Vamos testar novamente a App e ver o que acontece:

Dialog de alteração altera a nota

Opa! Agora a nossa App, além de buscar as notas, também permite alterá-las!

Código fonte

Caso surja alguma dúvida ou simplesmente quiser consultar o código fonte, fique à vontade em dar uma olhada no GitHub do projeto.

Para saber mais

Por mais que tenhamos possibilitado a feature de alteração de nota ocorreu um caso peculiar no qual a nossa App apresentou um aspecto não esperado, como foi o caso de enviar uma requisição com um id de valor 0, no caso inválido e o nosso usuário não saber que ocorreu esse problema.

Basicamente, isso aconteceu devido ao fato de não termos recebido uma resposta de erro no qual executa a função onFailure() do callback!

Em outras palavras, verificar apenas se a resposta não é null não significa que a ação com o servidor tenha dado certo! Portanto, existem outras técnicas que garantem isso, como por exemplo, verificar o status code do protocolo HTTP.

Inclusive, o próprio objeto Response do Retrofit permite nos devolve esse tipo de informação, isto é, por meio dele temos a capacidade de verificar o que aconteceu realmente na requisição.

Conclusão

Neste artigo implementamos a feature de alteração de nota na nossa App.

Porém, vimos que tivemos diversos desafios no meio do caminho, como por exemplo, implementar um listener para o RecyclerView, criar uma data classe para copiar um objeto, modificar a classe de dialog para lidar tanto com a adição e alteração de notas e realizar diversas refatorações…

Entretanto, o nosso código ainda não está 100%, pois existem diversos blocos de códigos que podem ser reaproveitados e adaptados… Não se preocupe, no próximo artigo, além de implementarmos a feature para remover notas, faremos uma nova refatoração para melhorar o nosso código atual.

E aí, o que achou do artigo? 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.