Mostrando os Pokémon com Coil em Android
Tags: kotlin, android, pokemon, recyclerview
Published: 2022-11-03 03:06:05

No último [post](https://dev.to/ronaldocoding/construindo-uma-pokedex-com-android-63o), nós conseguimos construir uma arquitetura MVVM simples, conectar nosso app a PokéAPI e listar os pokémon de Kanto por meio de log. Mas não listamos os pokémon na tela da nossa Pokédex muito menos mostramos as imagens dos monstrinhos. Sendo assim, nesse post vamos refatorar um pouco nosso código, implementar uma Recycler View e usar o Coil para exibir os pokémon na tela.

## Corrigindo um pequeno erro

Anterioirmente, eu acabei cometendo um erro no mapeamento da API, especificamente na estrutura do JSON que continha a imagem que queremos usar do pokémon. Eu mapeei assim:

```json
"sprites": {
	"official-artwork": {
	  "front-default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/4.png"
	}
}
```

O que deveria ser assim:

```json
"sprites": {
	"other": {
		"official-artwork": {
			"front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/4.png"
		}
    }
}
```

Para corrigir isso, precisamos fazer alguns ajustes simples: primeiro vamos criar a classe `Other`:

```kotlin
package br.com.pokedex.api

import com.google.gson.annotations.SerializedName

data class Other(
    @SerializedName("official-artwork") val officialArtwork: OfficialArtWork
)
```

Após isso, basta modificar a classe `Sprites` dessa forma:

```kotlin
package br.com.pokedex.api

import com.google.gson.annotations.SerializedName

data class Sprites(
    @SerializedName("other") val other: Other
)
```

E, por fim, corrigir o mapeamento `front-default` da classe `OfficialArtWork` para `front_default`:

```kotlin
package br.com.pokedex.api

import com.google.gson.annotations.SerializedName

data class OfficialArtWork(
    @SerializedName("front_default") val frontDefault: String? = null
)
```

Pronto: agora nosso mapeamento da api está correto!

## Refatorando o código

Outro “erro” do último post foi misturar os conceitos de classes Models e classes DTO (_Data Transfer Object_) ao fazer o mapeamento da API. Classes DTO são um padrão de software com o intuito de transferir informações entre as camadas de um sistema. Ele serve, por exemplo, para receber dados em um estado muito específico sem fazer contato entre as camadas inferiores da aplicação. No nosso caso, ele é útil justamente para serializar o JSON recebido como resposta pela PokéAPI. Sendo assim, vamos renomear todas as nossas antes classes Models para classes DTO e colocá-las em um pacote específico `dto`:


![Classes DTO](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/581uwxjg1cnm6lk3zxrq.png)

Agora que já temos nossas classes DTO, vamos realmente fazer nossas classes Models, que servirão como receptoras dos dados armazenados pelas DTO. Primeiro temos a classe que irá representar um único pokémon, a `SinglePokemon`:

```kotlin
package br.com.pokedex.model

data class SinglePokemon(
    val name: String,
    val id: Int,
    val imageUrl: String,
    val types: List<Type>
)
```

Agora vamos desenvolver a classe para representar os tipos do pokémon, a classe `Type`:

```kotlin
package br.com.pokedex.model

data class Type (
    val name: String
)
```

Bem simples, não? Essas duas classes Model contém todos os dados dos pokémon que nós vamos precisar no momento. Agora vamos criar funções para mapear classes DTO para classes Models em um arquivo chamado `PokedexMappers`. A primeira delas é uma função para mapear uma `SlotTypeDTO` para uma `Type`:

```kotlin
package br.com.pokedex.data.mapper

import br.com.pokedex.api.dto.SlotTypeDTO
import br.com.pokedex.model.Type
import br.com.pokedex.util.emptyString

fun SlotTypeDTO.toModel() = Type(
    name = typeDTO.name ?: emptyString()
)
```

Repare que nós usamos uma função chamada `emptyString()`, ela retorna apenas uma string vazia, como seu nome diz, fazemos isso para tornar nosso código mais idiomático, aqui seu código em um arquivo chamado `StringExt`:

```kotlin
package br.com.pokedex.util

fun emptyString() = ""
```

Agora vamos fazer uma função para mapear uma lista de `SlotTypeDTO` para uma lista de `Type`:

```kotlin
package br.com.pokedex.data.mapper

import br.com.pokedex.api.dto.SlotTypeDTO
import br.com.pokedex.model.Type
import br.com.pokedex.util.emptyString

fun SlotTypeDTO.toModel() = Type(
    name = typeDTO.name ?: emptyString()
)

fun List<SlotTypeDTO>.toModel(): List<Type> {
    val types = mutableListOf<Type>()
    types.add(this.first().toModel())
    this.first().let { firstType ->
        this.last().let { secondType ->
            if(secondType != firstType) {
                types.add(secondType.toModel())
            } else {
                types.add(Type(emptyString()))
            }
        }
    }
    return types.toList()
}
```

Essa é uma pouco mais complexa, mas o que fazemos aqui é criar uma `MutableList` de `Type` e adicionar o primeiro tipo do pokémon, depois analisamos se ele tem um segundo tipo obtendo o último elemento da lista de `SlotTypeDTO`, caso esse elemento seja diferente do primeiro, isso significa que há um segundo tipo, então nós o adicionamos na lista, caso seja igual, não há um segundo tipo, então nós adicionamos uma string vazia. Por fim, retornarmos a lista como uma `List<Type>`.

Agora basta mapearmos uma `SinglePokemonDTO` para uma `SinglePokemon`:

```kotlin
package br.com.pokedex.data.mapper

import br.com.pokedex.api.dto.SinglePokemonDTO
import br.com.pokedex.api.dto.SlotTypeDTO
import br.com.pokedex.model.SinglePokemon
import br.com.pokedex.model.Type
import br.com.pokedex.util.emptyString
import br.com.pokedex.util.zeroNumber

fun SlotTypeDTO.toModel() = Type(
    name = typeDTO.name ?: emptyString()
)

fun List<SlotTypeDTO>.toModel(): List<Type> {
    val types = mutableListOf<Type>()
    types.add(this.first().toModel())
    this.first().let { firstType ->
        this.last().let { secondType ->
            if(secondType != firstType) {
                types.add(secondType.toModel())
            } else {
                types.add(Type(emptyString()))
            }
        }
    }
    return types.toList()
}

fun SinglePokemonDTO.toModel() = SinglePokemon(
    name = name ?: emptyString(),
    id = id ?: zeroNumber(),
    imageUrl = sprites.other.officialArtworkDTO.frontDefault ?: emptyString(),
    types = types.toModel()
)
```

Note que também usamos uma função `zeroNumber()` para deixar nosso código mais legível. Eis o código dela no arquivo `IntExt`:

```kotlin
package br.com.pokedex.util

fun zeroNumber() = 0
```

Agora temos que substituir a menção a essas classes DTO no código para suas respectivas models, com especial atenção para a seguinte:

```kotlin
package br.com.pokedex.data.repository

import br.com.pokedex.api.PokemonApi
import br.com.pokedex.data.mapper.toModel
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class PokemonRepositoryImpl {

    private val api: PokemonApi = Retrofit.Builder()
        .baseUrl("https://pokeapi.co/api/v2/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()
        .create(PokemonApi::class.java)

    suspend fun getSinglePokemon(id: Int) = api.getSinglePokemon(id).toModel()
}
```

Aqui nós usamos a função que mapeia para um único pokémon: em resumo, mudamos o tipo de retorno de `getSinglePokemon()` para `SinglePokemon`. 

Por fim, mas não menos importante, vamos renomear nossa interface `PokemonService` para `PokemonApi`, com o intuito de deixar claro de que se trata da interface que contém as chamadas à PokéAPI:

```kotlin
package br.com.pokedex.api

import br.com.pokedex.api.dto.SinglePokemonDTO
import retrofit2.http.GET
import retrofit2.http.Path

interface PokemonApi {

    @GET("pokemon/{id}/")
    suspend fun getSinglePokemon(
        @Path("id") id: Int?
    ): SinglePokemonDTO

}
```

E pronto: refatoramos nosso código! Agora ele está melhor do que antes nos quesitos de legibilidade e consistência de dados.

## Construindo a Recycler View

Para a nossa Pokédex ser realmente uma Pokédex, precisamos mostrar nossos monstrinhos de bolso como uma lista e para isso existem diversas soluções em Android, como a Recycler View. Vamos usa ela por sua eficiência em exibir conjuntos grandes e dinâmicos de dados.

Com Recycler View nós fornecemos os dados e definimos a aparência de cada item, após isso, como o próprio nome indica, esses dados são reciclados: quando um item rola para fora da tela, o Recycler reutiliza sua visualização para novos itens que passarem a aparecer na tela. Isso melhora muito o desempenho, aperfeiçoando a capacidade de resposta do app e reduzindo o consumo de energia. Mais informações aqui: [developer.android](https://developer.android.com/guide/topics/ui/layout/recyclerview?hl=pt-br).

Implementar um Recycler View não é uma tarefa trivial: é necessário (1) primeiro decidir se será uma lista ou uma grade, (2) depois criar a aparência e o comportamento de cada elemento da lista, (3) estender da classe `ViewHolder`, responsável por fornecer todas as funcionalidades para os itens da lista, e por fim, (4) definir o `Adapter` que associa seus dados à visualização `ViewHolder`. Ufa, bastante coisa, não? Então vamos começar logo!

Para início de conversa, vamos fazer a nossa pokédex como uma lista por enquanto e definir como será a aparência dos itens. Sendo assim, criamos o layout `pokemon_card.xml` da seguinte forma:

```kotlin
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:padding="32dp">

    <ImageView
        android:id="@+id/pokemonImage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:importantForAccessibility="no"
        tools:src="@drawable/bulbasaur"/>

    <TextView
        android:id="@+id/pokemonName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/pokemonImage"
        tools:text="Bulbasaur" />

    <TextView
        android:id="@+id/pokemonId"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="@id/pokemonName"
        app:layout_constraintEnd_toEndOf="@id/pokemonName"
        app:layout_constraintTop_toBottomOf="@id/pokemonName"
        tools:text="#001" />

    <TextView
        android:id="@+id/firstPokemonType"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="@id/pokemonName"
        app:layout_constraintEnd_toEndOf="@id/pokemonName"
        app:layout_constraintTop_toBottomOf="@id/pokemonId"
        tools:text="Grass" />

    <TextView
        android:id="@+id/secondPokemonType"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        app:layout_constraintStart_toStartOf="@id/firstPokemonType"
        app:layout_constraintEnd_toEndOf="@id/firstPokemonType"
        app:layout_constraintTop_toBottomOf="@id/firstPokemonType"
        tools:text="Poison" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

É um layout bem simples: temos a imagem do pokémon, seu nome, seu id e seus tipos. Repare que o segundo tipo está `gone` como padrão, ou seja, não está na tela, faremos assim pois nem todo pokémon têm dois tipos. Com esse layout teremos esse resultado:


![Preview do pokemon_card.xml](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bc03cp80q319o5bcirai.png)

Em seguida vamos adicionar a `ViewGroup` do `RecyclerView` na nossa `pokedex_activity_xml` e informar que seu `listitem` é o layout que acabamos de construir:

```kotlin
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/pokedexRecyclerView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:listitem="@layout/pokemon_card" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

Está feito, agora vamos criar nosso `Adapter` e por consequência estender e personalizar a `ViewHolder`, vamos chamar essa classe de `PokedexAdapter`:

```kotlin
package br.com.pokedex.presentation

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import br.com.pokedex.databinding.PokemonCardBinding
import br.com.pokedex.model.SinglePokemon
import br.com.pokedex.util.showIf

class PokedexAdapter(
    private val context: Context,
    private val pokemon: List<SinglePokemon>
) : RecyclerView.Adapter<PokedexAdapter.PokemonViewHolder>() {

    inner class PokemonViewHolder(binding: PokemonCardBinding) :
        RecyclerView.ViewHolder(binding.root) {
        private val name = binding.pokemonName
        private val id = binding.pokemonId
        private val firstType = binding.firstPokemonType
        private val secondType = binding.secondPokemonType

        fun bind(singlePokemon: SinglePokemon) {
            name.text = singlePokemon.name
            id.text = singlePokemon.id.toString()
            firstType.text = singlePokemon.types.first().name
            secondType.text = singlePokemon.types.last().name
            secondType.apply {
                showIf(text.isNotEmpty())
            }
        }
    }

    override fun onCreateViewHolder(
			parent: ViewGroup, 
			viewType: Int
		): PokemonViewHolder {
        return PokemonViewHolder(
            PokemonCardBinding.inflate(
                LayoutInflater.from(context),
                parent,
                false
            )
        )
    }

    override fun onBindViewHolder(
			holder: PokemonViewHolder, 
			position: Int
		) {
        holder.bind(pokemon[position])
    }

    override fun getItemCount() = pokemon.size
}
```

Ok, dessa vez o código é bem grande, vamos por partes. Primeiro de tudo, nossa classe estende de `RecyclerView.Adapter` e usa `PokedexViewHolder` para fazer o bind (ligação) de dados. Essa nossa classe tem duas propriedades: um `context`, que será usado para inflar o layout, e uma lista de pokémon, nossa pokédex.

Sendo assim, definimos nossa ViewHolder, que usa view binding para ter acesso às view do layout e tem todas as propriedades necessárias para representar um pokémon. Nossa ViewHolder também tem o método `bind()`, que serve justamente para ligar os dados da lista de pokémon com as views do layout. Repare que nele usamos uma função chamada `showIf()`, essa função serve para tornar um `TextView` visível de acordo com uma condição, nesse caso se o conteúdo não for uma string vazia. Segue o código contido no arquivo `TextViewExt`:

```kotlin
package br.com.pokedex.util

import android.widget.TextView

fun TextView.showIf(condition: Boolean) {
    if(condition) {
        visibility = TextView.VISIBLE
    }
}
```

Note que temos três funções sobreescritas: `onCreateViewHolder()`, `onBindViewHolder()` e `getItemCount()`, esses métodos são o motor da RecyclerView. Abaixo seguem as funções de cada uma de acordo com o site oficial dos desenvolvedores android:

1. `onCreateViewHolder()`: `RecyclerView` chama esse método sempre que precisa criar um novo `ViewHolder`. O método cria e inicializa o `ViewHolder` e a `View` associada, mas *não* preenche o conteúdo da visualização. O `ViewHolder`ainda não foi vinculado a dados específicos.
2. `onBindViewHolder()`: `RecyclerView` chama esse método para associar um `ViewHolder` aos dados. O método busca os dados apropriados e usa esses dados para preencher o layout do fixador de visualização. Por exemplo, se a `RecyclerView` exibir uma lista de nomes, o método poderá encontrar o nome apropriado na lista e preencher o widget `TextView`do fixador de visualização. No nosso caso, os dados preenchidos são de um pokémon específico.
3. `getItemCount()`: a RecyclerView chama esse método para ver o tamanho do conjunto de dados. Por exemplo, em um app de lista de endereços, pode ser o número total de endereços. O RecyclerView usa essa função para determinar quando não há mais itens a serem exibidos.

Ufa, finalmente terminamos nosso adapter, agora vamos configurar a RecyclerView na nossa activity. Para isso basta que definamos seu layout manager e o adapter que acabamos de construir. O layout manager por enquanto será o `LinearLayoutManager`, visto que vamos exibir uma lista. O código que configura a RecyclerView está contido na função `setUpPokedexRecyclerView()`:

```kotlin
package br.com.pokedex.presentation

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import br.com.pokedex.databinding.ActivityPokedexBinding
import br.com.pokedex.model.SinglePokemon

class PokedexActivity: AppCompatActivity() {

    private val binding by lazy {
        ActivityPokedexBinding.inflate(layoutInflater)
    }

    private lateinit var viewModel: PokedexViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        viewModel = ViewModelProvider(this)[PokedexViewModel::class.java]

        viewModel.getPokemon()

        viewModel.pokemon.observe(this@PokedexActivity) { pokedex ->
            setUpPokedexRecyclerView(pokedex)
        }
    }

    private fun setUpPokedexRecyclerView(pokedex: List<SinglePokemon>?) {
        pokedex?.let { pokemonList ->
            binding.pokedexRecyclerView.apply {
                layoutManager = LinearLayoutManager(context)
                adapter = PokedexAdapter(context, pokemonList)
            }
        }
    }

} 
```

Com isso já estamos quase acabando nosso trabalho de hoje, falta apenas exibirmos as imagens dos nossos monstrinhos com o Coil.

## O que é Coil?

Coil é uma biblioteca de carregamento de imagens construída com Kotlin Coroutines. Ela é rápida, leve (adiciona mais ou menos 2000 métodos para a APK, o que comparado ao Picasso e Glide é significantemente menor), fácil de usar e moderna (Coil foi feita em Kotlin e usa bibliotecas modernas como Coroutines, OkHttp, Okio and AndroidX Lifecycles.

E é por isso que vamos usá-la.

Curiosidade: Coil é o acrônimode **Co**routine **I**mage **L**oader

## Mostrando os Pokémon com Coil

De início precisamos incluir o Coil como uma dependência no nosso arquivo gradle:

```
dependencies {
    ...

    // Coil
    implementation("io.coil-kt:coil:2.2.2")

    ...
}
```

Após sincronizarmos nosso arquivo gradle, já temos acesso às funções do gradle. Fique tranquilo, carregar as imagens dos pokémon será a parte mais fácil de hoje, analise o código abaixo:

```kotlin
package br.com.pokedex.presentation

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import br.com.pokedex.databinding.PokemonCardBinding
import br.com.pokedex.model.SinglePokemon
import br.com.pokedex.util.showIf
import coil.load

class PokedexAdapter(
    private val context: Context,
    private val pokemon: List<SinglePokemon>
) : RecyclerView.Adapter<PokedexAdapter.PokemonViewHolder>() {

    inner class PokemonViewHolder(binding: PokemonCardBinding) :
        RecyclerView.ViewHolder(binding.root) {
        private val image = binding.pokemonImage
        private val name = binding.pokemonName
        private val id = binding.pokemonId
        private val firstType = binding.firstPokemonType
        private val secondType = binding.secondPokemonType

        fun bind(singlePokemon: SinglePokemon) {
            loadPokemonImage(image, singlePokemon.imageUrl)
            name.text = singlePokemon.name
            id.text = singlePokemon.id.toString()
            firstType.text = singlePokemon.types.first().name
            secondType.text = singlePokemon.types.last().name
            secondType.apply {
                showIf(text.isNotEmpty())
            }
        }

        private fun loadPokemonImage(image: ImageView, imageUrl: String) {
            image.load(imageUrl)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PokemonViewHolder {
        return PokemonViewHolder(
            PokemonCardBinding.inflate(
                LayoutInflater.from(context),
                parent,
                false
            )
        )
    }

    override fun onBindViewHolder(holder: PokemonViewHolder, position: Int) {
        holder.bind(pokemon[position])
    }

    override fun getItemCount() = pokemon.size
}
```

Conseguiu notar o que mudou? Nós apenas tivemos que usar a função `load()` do Coil para carregar cada imagem dos nossos pokémon e extraímos esse código para a função `loadPokemon()` 😱

E pronto! Basta rodarmos o app para vermos o resultado:

![pokedex](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9vf2z8kk94gbsfxa73hz.png)

Incrível né? Nós finalmente temos uma pokédex minimamente visualizável, parabéns!

## Próximos posts

Nossa pokédex, apesar de visualizável, ainda está bem feia e lenta, levou cerca de 20 segundos para todas as imagens dos monstrinhos de bolso serem carregadas. Nós próximos posts vamos melhorar sua performance e deixá-la mais bonita. 

Link do repositório no github:

{% embed https://github.com/ronaldocoding/pokedex %}

Post anterior:

{% embed https://dev.to/ronaldocoding/construindo-uma-pokedex-com-android-63o %}

Próximo post:

{% embed https://dev.to/ronaldocoding/melhorando-a-arquitetura-da-pokedex-38c9 %}

Obrigado pela atenção e até a próxima!