Melhorando a performance da Pokédex com Paging 3 e Flow API
Tags: kotlin, android, paging, flow
Published: 2022-11-17 00:23:57

Até agora nós já conseguimos mostrar todos os pokémon da região de Kanto (os 151 primeiros) na tela do nosso app, junto com seus nomes, `id`s e tipos. Porém, nossa aplicação ainda está muito lenta: são necessários mais que 10 segundos para carregar apenas os primeiros monstrinhos de bolso. Por esse motivo, nesse post nós vamos melhorar a performance da nossa Pokédex usando a versão 3 da biblioteca `Paging` e a `Flow API`.

## O que é `Paging 3`?

A `Paging` é uma biblioteca capaz de implementar um mecanismo chamado paginação, trata-se de carregar grandes quantidades de dados de forma gradual, reduzindo o uso de rede e de recursos do sistema. A versão 3 dessa biblioteca está estável, usa Kotlin coroutines por default e é por isso que vamos usá-la.

Vantagens de `Paging 3`:

- Controla as chaves a serem utilizadas para recuperar a página seguinte e a anterior.
- Automaticamente faz a requisição da página seguinte correta quando o usuário percorre a tela para o fim dos dados carregados.
- Garante que múltiplas requisições não sejam disparadas ao mesmo tempo.
- Rastreia o estado de carregamento e nos permite exibí-lo em uma `RecyclerView` e provê uma funcionalidade de retentativa fácil para casos de falha no carregamento.
- Permite o uso de operações comuns como `map` ou `filter` na lista a ser exibida.
- Provê um jeito fácil de implementar um separador de lista, como um *footer*, que iremos fazer.
- Simplifica o cache dos dados, garantindo que nós não estaremos executando transformações de dados a cada mudança de configuração.

Como podemos ver, é uma ótima solução para o nosso problema de lentidão e limitação de exibição dos pokémon, com essa biblioteca seremos capazes de exibir todos os monstrinhos 🤩

## O que é `Flow API`?

De forma resumida, em coroutines, um `Flow` é um tipo que pode emitir vários valores sequencialmente, ao contrário das `suspend functions`, que retornam apenas um valor. No nosso caso, o usaremos para emitir um fluxo de dados de pokémon a partir do `Paging` para criar a nossa pokédex.

Conceitualmente, um `Flow` é um *stream* (fluxo) *de dados* que pode ser computado de forma assíncrona. Os valores emitidos precisam ser do mesmo tipo. Por exemplo, um `Flow<Int>` é um fluxo que emite valores inteiros. Vejamos o exemplo abaixo:

```kotlin
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.delay

suspend fun main() {
    val flow: Flow<Int> = flow {
        (0..10).forEach {
            delay(2000)
            emit(it)
        }
    }.map {
        it * it
    }

    flow.collect {
        println(it)
    }
}
```

```
0
1
4
9
16
25
36
49
64
81
100
```

Podemos considerar esse exemplo como um *Hello World* de `Flow`, nele nós criamos um fluxo de dados de inteiros que são emitidos a cada 2 (dois) segundos. Esses dados são transformados usando a função `map` e depois coletados usando a função `collect` para enfim serem exibidos no console.

Com esse simples exemplo nós podemos enxergar e entender os componentes e entidades envolvidas no fluxo de dados: o `Flow Builder` (produtor), o `Operator` (intermediário) e o `Collector` (consumidor).


![flow-entities.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bzsu66n870ey4zhw9cb4.png)

Em resumo, o `Flow Builder` realiza as tarefas e emite os dados, o `Operator` transforma os dados de um formato para outro e o `Collector` coleta os dados emitidos pelo `Flow Builder` que foram transformados pelo `Operator`.

Na nossa aplicação, a fonte da dados e o repositório representarão as tarefas do consumidor e intermediário, enquanto a UI será a consumidora.

## Implementando a solução

Para implementar a nossa solução precisaremos criar um `PagingSource`, um `Pager`, um `Flow` de `PagingData` e um adaptador para a UI chamado `PagingDataAdapter`.


![paging-components](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wh25qpkhfq7l7bdcriz3.png)

Ok, mas o que é essa sopa de letrinhas? Calma, vamos entender um por um.

### `PagingSource`

Um `PagingSource` é a definição de uma fonte de dados para a paginação e a forma como esses dados serão recuperados de uma única fonte. O `PagingSource` deve ser parte da camada *repository*. 

Vamos sobrescrever as funções `getRefreshKey()`, que provê uma chave para a função `load()`, e a própria função  `load()`, que recupera os dados paginados da fonte de dados e retorna esses dados carregados junto com as informações sobre as chaves posterior e anterior. Ambas essas funções são `suspend functions` e por isso poderemos usar as funções que fazem as requisições à PokéAPI.

Primeiro de tudo, adicionaremos a dependência da biblioteca `Paging` ao nosso arquivo `gradle.build`:

```
def pagingVersion = "3.1.1"

dependencies {
		...
		
    // Paging
    implementation "androidx.paging:paging-runtime:$pagingVersion"
		
		...
}
```

Agora procederemos para o desenvolvimento da `PagingSource`, que chamaremos de `PokedexPagingSource`. O construtor primário da nossa classe vai receber uma instância de `PokemonApi` e ela vai herdar de `PagingSource<Int, SinglePokemon>`, ou seja, ela recebe uma chave inteira e devolve os dados de um pokémon. Iniciaremos com a construção do método `getRefreshKey()`, responsável por prover uma chave para o método `load()`:

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

import androidx.paging.PagingSource
import androidx.paging.PagingState
import br.com.pokedex.data.api.PokemonApi
import br.com.pokedex.domain.model.SinglePokemon

class PokedexPagingSource(
    private val api: PokemonApi
) : PagingSource<Int, SinglePokemon>() {

    override fun getRefreshKey(state: PagingState<Int, SinglePokemon>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }
}
```

A chave que esse método retorna deve fazer com que `load()` carregue itens suficientes para preencher a *viewport* ao redor da última posição acessada, permitindo que a próxima geração os anime de forma transparente. 

A última posição acessada pode ser recuperada via `state.anchorPosition`, a qual é tipicamente o mais alto ou mais baixo item na *viewport* devido ao acesso ser disparado pela ligação dos itens conforme eles rolam na tela. Mas no nosso caso, a última posição acessada não é o bastante, nós precisamos saber qual a chave/página mais próxima a essa posição e é por isso que usamos a função `closestPageToPosition`.

Por exemplo, digamos que temos que carregar 20 itens por página, carregar duas páginas significariam usar as chaves 1 e 2 e ter 40 itens carregados no total. Nessa situação, quando formos procurar pelo item na posição 30, `state.anchorPosition` retornaria 30, mas essa não pode ser nossa chave, nós precisa saber em qual página esse item está, ou seja, precisamos saber que é a página 2. É por isso que usamos `state.anchorPosition` e tentamos recuperar sua chave anterior, chave 1, `plus(1)` (”mais um”), resultando na chave 2. Caso a chave anterior seja nula, tentamos recuperar através da chave posterior, chave 3, `minus(1)` (”menos um”), resultando também na chave 2.

Chegou a hora de implementar a tão falada função `load()`.  O seu retorno padrão é do tipo `LoadResult<Key, Value>`, no nosso caso é `LoadResult<Int, SinglePokemon>`. A classe `LoadResult` é uma `sealed class`. Uma `sealed class` é uma classe em que todas as suas subclasses devem ser desenvolvidas dentro dela mesma ou no mesmo arquivo. Essa nossa classe de retorno tem três subclasses: `Error`, `Invalid` e `Page`. `Error` representa um erro esperado (como uma falha de conexão de rede), `Invalid` representa a invalidação de qualquer futura requisição do `PagingSource` e `Page` representa o objeto retornado com sucesso.

A classe `Page` tem as seguintes propriedades relevantes para nós:

- `data`: os dados carregados
- `prevKey`: a chave para a página anterior, caso ela exista
- `nextKey`: a chave para a página posterior, caso ela exista

Implementação da função `load()`:

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

import androidx.paging.PagingSource
import androidx.paging.PagingState
import br.com.pokedex.data.api.PokemonApi
import br.com.pokedex.data.datasource.Constants.LAST_OFFSET
import br.com.pokedex.data.datasource.Constants.LAST_POSITION
import br.com.pokedex.data.datasource.Constants.POKEMON_OFFSET
import br.com.pokedex.data.datasource.Constants.POKEMON_STARTING_OFFSET
import br.com.pokedex.data.mapper.toModel
import br.com.pokedex.domain.model.SinglePokemon
import okio.IOException
import retrofit2.HttpException

class PokedexPagingSource(
    private val api: PokemonApi
) : PagingSource<Int, SinglePokemon>() {

    // ...

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, SinglePokemon> {
        val position = params.key ?: POKEMON_STARTING_OFFSET
        return try {
            val response = api.getPokemon(
                if (position == LAST_POSITION) {
                    LAST_OFFSET
                } else {
                    position * POKEMON_OFFSET
                }
            )
            val pokemon = mutableListOf<SinglePokemon>()
            response.body()?.results?.map { result ->
                val singlePokemon = api.getSinglePokemon(result.name)
                singlePokemon.body()?.toModel()?.let { pokemon.add(it) }
            }
            LoadResult.Page(
                data = pokemon,
                prevKey = if (position == POKEMON_STARTING_OFFSET) null else position,
                nextKey = if (position == LAST_POSITION) null else position + 1
            )
        } catch (exception: IOException) {
            return LoadResult.Error(exception)
        } catch (exception: HttpException) {
            return LoadResult.Error(exception)
        }
    }
}
```

Objeto `Constants`:

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

object Constants {
    const val BASE_URL = "https://pokeapi.co/api/v2/"
    const val POKEMON_STARTING_OFFSET = 0
    const val POKEMON_OFFSET = 20
    const val LAST_OFFSET = 885
    const val LAST_POSITION = 45
    const val PAGE_SIZE = 20
}
```

Também foi necessário modificar a `PokemonApi` com dois novos métodos: `getPokemon()`, que retorna certo número de pokémon de acordo com um *offset* e `getSinglePokemon()`, que obtêm os dados do pokémon a partir do seu nome. Além disso, o retorno dos métodos agora é do tipo `Response<T>`, necessário para a implementação com `Paging`:

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

import br.com.pokedex.data.api.dto.PokemonDTO
import br.com.pokedex.data.api.dto.SinglePokemonDTO
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query

interface PokemonApi {

    @GET("pokemon/")
    suspend fun getPokemon(
        @Query("offset") offset: Int?
    ): Response<PokemonDTO>

    @GET("pokemon/{name}")
    suspend fun getSinglePokemon(
        @Path("name") name: String?
    ) : Response<SinglePokemonDTO>

}
```

Classe `PokemonDTO`:

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

import com.google.gson.annotations.SerializedName

data class PokemonDTO(
    @SerializedName("results") val results: List<PokemonResultDTO>
)
```

Iremos entender o código passo-a-passo.

Obtemos o valor da chave atual usando `params.key`, ou seja, usamos a função `getRefreshKey()`, caso seja nulo, usamos a constante `POKEMON_STARTING_OFFSET`:

```kotlin
val position = params.key ?: POKEMON_STARTING_OFFSET
```

O retorno da função é um `try-catch` em que tentamos recuperar os dados de 20 pokémon usando as constantes `LAST_POSITION`, `LAST_OFFSET` e `POKEMON_OFFSET`. A lógica é simples: para evitar obter pokémon com dados inconsistentes (além do `id` 905) nós passamos apenas o *offset*  final, caso seja a última posição, caso não, fazemos o cálculo de `position * POKEMON_OFFSET` para obter os dados dos pokémon da vez. Além disso mapeamos o resultado dessa requisição para uma lista do tipo `SinglePokemon`. Por fim instanciamos um `LoadResult` e damos os valores às suas propriedades:

- `data` recebe a lista de pokémon antes mencionada
- `prevKey` recebe `null`, caso a posição atual seja 0 (zero), caso não, recebe a posição atual
- `nextKey` recebe `null`, caso seja a última posição, caso não, recebe a posição atual mais 1

 

```kotlin
val response = api.getPokemon(
    if (position == LAST_POSITION) {
        LAST_OFFSET
    } else {
        position * POKEMON_OFFSET
    }
)
val pokemon = mutableListOf<SinglePokemon>()
response.body()?.results?.map { result ->
    val singlePokemon = api.getSinglePokemon(result.name)
    singlePokemon.body()?.toModel()?.let { pokemon.add(it) }
}
LoadResult.Page(
    data = pokemon,
    prevKey = if (position == POKEMON_STARTING_OFFSET) null else position,
    nextKey = if (position == LAST_POSITION) null else position + 1
)
```

Finalmente temos os `catchs` para protegermos nossa aplicação quanto a problemas de `IO` (conexão de rede, por exemplo) e `HTTP` (respostas com códigos diferentes de 2XX):

```kotlin
catch (exception: IOException) {
    return LoadResult.Error(exception)
} catch (exception: HttpException) {
    return LoadResult.Error(exception)
}
```

Ufa! Bastante coisa né? Mas calma que ainda tem mais.

### `Pager` e `Flow<PagingData<T>>`

Agora que implementamos uma forma de lidar com nossa fonte de dados remota, já podemos modificar nosso repositório para essa mudança.  A partir desse momento nosso repositório irá retornar um fluxo de dados do tipo `PagingData<SinglePokemon>` a partir de um `Pager`, mas o que é um `Pager`?

Um `Pager` nada mais é do que um construtor para um fluxo reativo (*reactive stream*) de `PagingData`, ele, unido ao `Flow`, lida com todos os estados da paginação, retornando um objeto em caso de sucesso e um erro em caso contrário. 

Para instanciar um `Pager`, precisamos de um `PageConfig`, uma configuração para um `Pager`, no qual nós informamos o tamanho da página, 20, e além disso também é necessário informar a `pagingSourceFactory`, ou seja, como serão fabricados os dados daquele `Pager`, a saber, o `PokédexPagingSource` que definimos a pouco:

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

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import br.com.pokedex.data.api.PokemonApi
import br.com.pokedex.data.datasource.Constants.PAGE_SIZE
import br.com.pokedex.domain.model.SinglePokemon
import br.com.pokedex.domain.repository.PokemonRepository
import kotlinx.coroutines.flow.Flow

class PokemonRepositoryImpl(private val api: PokemonApi) : PokemonRepository {

    override fun getSinglePokemon(): Flow<PagingData<SinglePokemon>> {
        return Pager(
            config = PagingConfig(
                pageSize = PAGE_SIZE
            ),
            pagingSourceFactory = { PokedexPagingSource(api) }
        ).flow
    }
}
```

Por fim usamos a propriedade `flow` de `Pager` para indicar que será retornado um fluxo de `PagingData`, isto é, serão emitidas novas instâncias de `PagingData` em certas circunstâncias.

Interface `PokemonRepository` modificada:

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

import androidx.paging.PagingData
import br.com.pokedex.data.api.Resource
import br.com.pokedex.data.api.dto.SinglePokemonDTO
import br.com.pokedex.domain.model.SinglePokemon
import kotlinx.coroutines.flow.Flow

interface PokemonRepository {

    fun getSinglePokemon(): Flow<PagingData<SinglePokemon>>
}
```

### `PagingDataAdapter`

Como já implementamos a camada de *repository*, vamos modificar a *ViewModel* para refletir essas mudanças. Não usaremos mais *LiveData*, usaremos `Flow`:

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

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import br.com.pokedex.domain.interactor.GetSinglePokemonUseCase
import br.com.pokedex.domain.model.SinglePokemon
import kotlinx.coroutines.flow.Flow

class PokedexViewModel(
	private val useCase: GetSinglePokemonUseCase
) : ViewModel() {

    fun getPokemonFlow(): Flow<PagingData<SinglePokemon>> {
        return useCase.execute().cachedIn(viewModelScope)
    }

}
```

Ficou bem mais simples, né? Nós basicamente mudamos o tipo de retorno da função `getPokemonFlow()`, antes chamada de `getPokemon()`, para `Flow<PagingData<SinglePokemon>>` e chamamos o *UseCase*, mas com a diferença que aplicamos uma nova função a essa chamada: `cachedIn()`. O que essa função faz?

A função `cachedIn()` cria um *cache* (área de memória de acesso rápido) de `PagingData` . Isso faz com que o `flow` seja mantido ativo enquanto o dado escopo está ativo, no nosso caso o escopo da *ViewModel*. Dessa forma garantimos que, qualquer que seja a mudança de configuração (rotação da tela, por exemplo), a nova *Activity* já irá receber os dados existentes, ao invés de fazer novas requisições para obter os dados do zero.

Note que o código do *UseCase*  não foi alterado, graças a sintaxe enxuta do Kotlin:

```kotlin
package br.com.pokedex.domain.interactor

import br.com.pokedex.domain.repository.PokemonRepository

class GetSinglePokemonUseCase(private val repository: PokemonRepository) {

    fun execute() = repository.getSinglePokemon()
}
```

Pronto, já podemos criar o `PagingDataAdapter` que vai tornar possível conectar tudo isso com a UI. Basicamente vamos alterar o *adapter* já existente para refletir as mudanças que fizemos. O `PokemonViewHolder` se manterá o mesmo:

```kotlin
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)
		}
}
```

O retorno da nossa classe será `PagingDataAdapter`, no entanto, para utilizarmos esse tipo de *adapter*  é necessário implementar a classe `DiffUtil.ItemCallback`. Essa classe é responsável por realizar um *callback* (passar uma função como parâmetro de outra) que calcula a diferença entre dois itens não-nulos na lista. Em outras palavras, ela serve para verificar se dois objetos representam o mesmo item e checar se dois itens tem os mesmos dados.

Vamos implementá-la como uma `singleton class`, uma classe de instância única. Kotlin desenvolve esse padrão de projeto com o uso da palavra chave `object` e como essa `singleton class` ficará dentro de outra, ela será um `companion object`:

```kotlin
companion object {
    private val POKEMON_COMPARATOR = object
        : DiffUtil.ItemCallback<SinglePokemon>() {

        override fun areItemsTheSame(
            oldItem: SinglePokemon,
            newItem: SinglePokemon
        ): Boolean =
            oldItem.id == newItem.id

        override fun areContentsTheSame(
            oldItem: SinglePokemon,
            newItem: SinglePokemon
        ): Boolean =
            oldItem == newItem
    }
}
```

Como podemos ver, sobrescrevemos dois métodos de `DiffUtil.ItemCallback`: `areItemsTheSame()` e `areContentsTheSame()`. Eles são encarregados de checar se dois itens são o mesmo item e se dois itens têm o mesmo conteúdo, respectivamente. 

Nossa implementação é bem simples: para verificar se dois itens são o mesmo item, basta compararmos os `id`s dos pokémon e para verificar se dois itens têm o mesmo conteúdo podemos fazer uma comparação direta mesmo.

Podemos partir para o desenvolvimento das funções `onBindViewHolder()` e `onCreateViewHolder()`, não será necessário sobrescrevermos o método `getItemCount()`, o `PagingDataAdapter` será responsável por isso.

Nosso `onBindViewHolder()` ficará dessa forma:

```kotlin
override fun onBindViewHolder(holder: PokemonViewHolder, position: Int) {
    val singlePokemon = getItem(position)
    singlePokemon?.let {
        holder.bind(it)
    }
}
```

Aqui usamos o método `getItem()` fornecido pelo `PagingDataAdapter` que nos retorna o item que precisamos a partir da sua posição, visto que o retorno pode ser nulo, usamos uma *safe call* e finalmente realizamos o `bind()`.

E quanto ao nosso método `onCreateViewHolder()`, nada muda:

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

Está feito! Temos o nosso *adapter*:

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

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

class PokedexAdapter(
    private val context: Context
) : PagingDataAdapter<SinglePokemon, PokedexAdapter.PokemonViewHolder>(
    POKEMON_COMPARATOR
) {

    override fun onBindViewHolder(holder: PokemonViewHolder, position: Int) {
        val singlePokemon = getItem(position)
        singlePokemon?.let {
            holder.bind(it)
        }
    }

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

    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)
        }
    }

    companion object {
        private val POKEMON_COMPARATOR = object
            : DiffUtil.ItemCallback<SinglePokemon>() {

            override fun areItemsTheSame(
                oldItem: SinglePokemon,
                newItem: SinglePokemon
            ): Boolean =
                oldItem.id == newItem.id

            override fun areContentsTheSame(
                oldItem: SinglePokemon,
                newItem: SinglePokemon
            ): Boolean =
                oldItem == newItem
        }
    }

}
```

### Conectando o *adapter* na UI

Procederemos para a conexão do *adapter* na UI. As duas funções mais importantes que construiremos serão as seguintes: `setUpAdapter()` e `getPokemon()`. A função `setUpAdapter()` se encarregará de configurar o nosso *adapter*:

```kotlin
private fun setUpAdapter() {
		pokedexAdapter.addLoadStateListener { loadState ->
		    when(loadState.refresh) {
		        is LoadState.Loading -> setUpLoadingView()
		        is LoadState.Error -> setUpErrorView()
		        else -> setUpSuccessView()
		    }
		}
}
```

O `PagingDataAdapter` nos fornece uma excelente classe para tratamento de erros chamada `CombinedLoadStates`, que provê todos os estados de carregamento do *adapter*. O que nos interessa aqui é o estado *refresh*, que representa o estado do *adapter* no seu primeiro carregamento: `Loading`, `Error` e `NotLoading`, nos interessa apenas os dois primeiros. Caso esse estado seja `Loading`, configuramos a UI para representar um carregamento, caso seja `Erorr`, configuramos a UI para representar um erro e caso não seja nenhum desses dois, consideramos sucesso e apresentamos a lista de pokémon.

Métodos de configuração da UI:

```kotlin
private fun setUpSuccessView() {
		binding.apply {
		    pokedexRecyclerView.showView()
		    pokedexCircularProgressIndicator.hideView()
		    pokedexErrorMessage.hideView()
		}
}

private fun setUpErrorView() {
    binding.apply {
        pokedexRecyclerView.hideView()
        pokedexCircularProgressIndicator.hideView()
        pokedexErrorMessage.showView()
    }
}

private fun setUpLoadingView() {
    binding.apply {
        pokedexRecyclerView.hideView()
        pokedexCircularProgressIndicator.showView()
        pokedexErrorMessage.hideView()
    }
}
```

Já conhecemos `pokedexRecyclerView`, mas aqui referenciamos duas outras *views*: `pokedexCircularProgressIndicator` e `pokedexErrorMessage`. A primeira se trata de uma *progress bar* em forma de círculo fornecida pelo *Material Design* e a segunda é apensa um *TextView* que apresenta uma mensagem de erro. Segue código modificado de `activity_main.xml`:

```xml
<?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"
        android:visibility="visible"/>

    <TextView
        android:id="@+id/pokedexErrorMessage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAlignment="center"
        app:layout_constraintTop_toBottomOf="@id/pokedexRecyclerView"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:text="@string/error_message"
        android:visibility="gone"/>

    <com.google.android.material.progressindicator.CircularProgressIndicator
        android:id="@+id/pokedexCircularProgressIndicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:visibility="gone"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />

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

Deixamos as duas *views* auxiliares `gone` por padrão e controlamos as suas visibilidades por meio dos seguintes métodos:

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

import com.google.android.material.progressindicator.CircularProgressIndicator

fun CircularProgressIndicator.showView() {
    visibility = CircularProgressIndicator.VISIBLE
}

fun CircularProgressIndicator.hideView() {
    visibility = CircularProgressIndicator.GONE
}
```

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

import android.widget.TextView

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

fun TextView.showView() {
    visibility = TextView.VISIBLE
}

fun TextView.hideView() {
    visibility = TextView.GONE
}
```

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

import androidx.recyclerview.widget.RecyclerView

fun RecyclerView.showView() {
    visibility = RecyclerView.VISIBLE
}

fun RecyclerView.hideView() {
    visibility = RecyclerView.GONE
}
```

Também atualizamos o arquivo `strings.xml`, que contém as *strings* do nosso projeto:

```xml
<resources>
    <string name="app_name">Pokédex</string>
    <string name="error_message">Something went wrong\nPlease, check your network connection and try reopen the app</string>
</resources>
```

Agora vamos analisar o método `getPokemon()`. Esse método é responsável por coletar os dados a partir da *ViewModel* e submetê-los para o *adapter*. Para fazer isso usamos as funções `collectLatest` do `Flow` e `submitData()` do `PagingDataAdapter`, respectivamente:

```kotlin
private fun getPokemon() {
		lifecycleScope.launch {
		    viewModel.getPokemonFlow().collectLatest { pokemon ->
		        pokedexAdapter.submitData(pokemon)
		    }
		}
}
```

Por fim, nossa classe `PokedexActivity` ficará dessa forma:

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

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import androidx.recyclerview.widget.LinearLayoutManager
import br.com.pokedex.databinding.ActivityPokedexBinding
import br.com.pokedex.util.hideView
import br.com.pokedex.util.showView
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel

class PokedexActivity : AppCompatActivity() {

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

    private val viewModel: PokedexViewModel by viewModel()
    private val pokedexAdapter by lazy {
        PokedexAdapter(this)
    }

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

        setUpAdapter()
        setUpPokedexRecyclerView()
        getPokemon()
    }

    private fun setUpAdapter() {
        pokedexAdapter.addLoadStateListener { loadState ->
            when(loadState.refresh) {
                is LoadState.Loading -> setUpLoadingView()
                is LoadState.Error -> setUpErrorView()
                else -> setUpSuccessView()
            }
        }
    }

    private fun setUpSuccessView() {
        binding.apply {
            pokedexRecyclerView.showView()
            pokedexCircularProgressIndicator.hideView()
            pokedexErrorMessage.hideView()
        }
    }

    private fun setUpErrorView() {
        binding.apply {
            pokedexRecyclerView.hideView()
            pokedexCircularProgressIndicator.hideView()
            pokedexErrorMessage.showView()
        }
    }

    private fun setUpLoadingView() {
        binding.apply {
            pokedexRecyclerView.hideView()
            pokedexCircularProgressIndicator.showView()
            pokedexErrorMessage.hideView()
        }
    }

    private fun setUpPokedexRecyclerView() {
        binding.pokedexRecyclerView.apply {
            layoutManager = LinearLayoutManager(context)
            adapter = pokedexAdapter
        }
    }

    private fun getPokemon() {
        lifecycleScope.launch {
            viewModel.getPokemonFlow().collectLatest { pokemon ->
                pokedexAdapter.submitData(pokemon)
            }
        }
    }

}
```

Já podemos rodar o app e vermos os três tipos de visualização:

Tela de loading:

![loading.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n5wuyjnwjbfhvf9ora8t.png)

Tela de erro:

![error-view.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ynmgaik3agyl4s4bvvy.png)

Tela de sucesso:

![sucesso.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f0xtvp4ffd0pzffps5ca.png)

Com essa implementação conseguimos reduzir o tempo de espera de carregamento dos pokémon para menos de 5 segundos, uma melhora de mais de 50%! Parabéns para nós 😎👊

Mas ainda temos um problema: apesar de tratarmos possíveis erros no primeiro carregamento da tela, não fazemos isso depois. Ou seja, caso a internet do celular pare de funcionar por algum motivo, o usuário ficará sem saber o que fazer. Precisamos melhorar essa experiência. Como podemos fazer isso? Adicionando um *footer*!

### Melhorando a experiência com *footer*

A biblioteca `Paging` oferece diversas ferramentas para melhorar a experiência da sua implementação, uma delas é o *footer*, que nada mais é do que um rodapé, um texto, botão ou imagem que sempre vai ficar no fim da tela. Usaremos esse *footer* para exibir uma *progress bar* circular de carregamento ou uma mensagem de erro e um botão para tentar carregar os dados restantes novamente.

Antes de tudo vamos criar um layout para esse *footer*, ele será composto de um `CircularProgressIndicator`, para representar o carregamento, um `TextView`, o texto de erro e um `MaterialButton`, o botão para tentar novamente. Todos eles terão visibilidade `gone` por default:

```xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/loadStateErrorMessage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/error_message"
        android:layout_gravity="center"
        android:textAlignment="center"
        android:visibility="gone"/>

    <com.google.android.material.button.MaterialButton
        android:id="@+id/loadStateTryAgainButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/try_again"
        android:layout_gravity="center"
        android:visibility="gone"
        android:layout_marginTop="16dp"/>

    <com.google.android.material.progressindicator.CircularProgressIndicator
        android:id="@+id/loadStateProgressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:layout_gravity="center" />

</LinearLayout>
```

Arquivo `strings.xml` modificado:

```xml
<resources>
    <string name="app_name">Pokédex</string>
    <string name="error_message">Something went wrong\nCheck your network connection and try again</string>
    <string name="try_again">Try again</string>
    <string name="load_error_message">Something went wrong\nTry again</string>
</resources>
```

Para implementarmos esse *footer* também precisaremos criar um *adapter*, o `LoadStateAdapter` e esse *adapter* precisa de um *ViewHolder*. Vamos chamá-lo de `PokedexLoadStateViewHolder`:

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

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.LoadState
import androidx.recyclerview.widget.RecyclerView
import br.com.pokedex.R
import br.com.pokedex.databinding.PokedexLoadStateFooterBinding
import br.com.pokedex.util.hideView
import br.com.pokedex.util.showView

class PokedexLoadStateViewHolder(
    private val binding: PokedexLoadStateFooterBinding,
    tryAgain: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {

    init {
        binding.loadStateTryAgainButton.setOnClickListener { tryAgain.invoke() }
    }

    fun bind(loadState: LoadState) {
        when(loadState) {
            is LoadState.Loading -> {
                binding.loadStateProgressBar.showView()
                binding.loadStateErrorMessage.hideView()
                binding.loadStateTryAgainButton.hideView()
            }
            is LoadState.Error -> {
                binding.loadStateErrorMessage.showView()
                binding.loadStateTryAgainButton.showView()
                binding.loadStateProgressBar.hideView()
            }
            is LoadState.NotLoading -> {
                // Do nothing
            }
        }
    }

    companion object {
        fun create(parent: ViewGroup, tryAgain: () -> Unit) : PokedexLoadStateViewHolder {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.pokedex_load_state_footer, parent, false)
            val binding = PokedexLoadStateFooterBinding.bind(view)
            return PokedexLoadStateViewHolder((binding), tryAgain)
        }
    }
}
```

Código das *extension functions* do `MaterialButton`:

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

import com.google.android.material.button.MaterialButton

fun MaterialButton.showView() {
    visibility = MaterialButton.VISIBLE
}

fun MaterialButton.hideView() {
    visibility = MaterialButton.GONE
}
```

O nosso *ViewHolder*  recebe duas coisas no seu construtor: uma propriedade `binding`, que contém as referências às *views* do nosso layout, um *callback* da função que será executada ao clicar no botão de tentar novamente. Aqui também usamos uma *initializer block*, trata-se de um bloco de código que será executado sempre que a classe for criada. Nele nós informamos que a ação de clique no botão irá invocar a função *callback* `tryAgain()`, ou seja, irá executá-la.

```kotlin
class PokedexLoadStateViewHolder(
    private val binding: PokedexLoadStateFooterBinding,
    tryAgain: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {

    init {
        binding.loadStateTryAgainButton.setOnClickListener { tryAgain.invoke() }
    }
	
    // ...

}
```

Depois disso criamos a função `bind()`, que irá ligar o *adapter* ao *ViewHolder*. Em resumo, ela usa os `loadStates` para tornar a UI reativa, faz com que ela reaja aos estados de sucesso, erro e carregamento:

```kotlin
fun bind(loadState: LoadState) {
        when(loadState) {
            is LoadState.Loading -> {
                binding.loadStateProgressBar.showView()
                binding.loadStateErrorMessage.hideView()
                binding.loadStateTryAgainButton.hideView()
            }
            is LoadState.Error -> {
                binding.loadStateErrorMessage.showView()
                binding.loadStateTryAgainButton.showView()
                binding.loadStateProgressBar.hideView()
            }
            is LoadState.NotLoading -> {
                // Do nothing
            }
        }
    }
```

Por fim, desenvolvemos uma função que retorna uma instância desse *ViewHolder* em um `companion object`, ela recebe uma *ViewGroup*, que chamamos de `parent`, e uma função *callback* `tryAgain()`. O `parent` trata-se do layout-pai desse outro layout, isto é, o `activity_main.xml`, e o `tryAgain()` é a função que será executada ao clicar no botão de tentar novamente:

```kotlin
companion object {
    fun create(parent: ViewGroup, tryAgain: () -> Unit) : PokedexLoadStateViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.pokedex_load_state_footer, parent, false)
        val binding = PokedexLoadStateFooterBinding.bind(view)
        return PokedexLoadStateViewHolder((binding), tryAgain)
    }
}
```

O código do nosso *adapter* será bem simples: sobrescrevemos os métodos `onBindViewHolder()` e `onCreateViewHolder()`. No primeiro, chamamos a função `bind()` do *ViewHolder* e no segundo chamamos a função `create()`:

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

import android.view.ViewGroup
import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter

class PokedexLoadStateAdapter(
    private val tryAgain: () -> Unit
) : LoadStateAdapter<PokedexLoadStateViewHolder>() {

    override fun onBindViewHolder(holder: PokedexLoadStateViewHolder, loadState: LoadState) {
        holder.bind(loadState)
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        loadState: LoadState
    ): PokedexLoadStateViewHolder {
        return PokedexLoadStateViewHolder.create(parent, tryAgain)
    }

}
```

Note que o `PokedexLoadStateAdapter` recebe por parâmetro uma função *callback*, se trata da função que é executada ao clicar no botão de tentar novamente. Estamos a propagando desde a UI até o *ViewHolder* a passando na função `create()` no método `onCreateViewHolder()`.

Estamos quase finalizando, mas antes precisamos fazer uma alteração em `PokedexAdapter`. Da forma como desenvolvemos o *footer* ele será exibido com o tamanho de um único *span*  no *RecyclerView* e como planejamos apresentar os pokémon na forma de um grid de duas colunas, temos que consertar isso.

A lógica é a seguinte: caso o item de visualização seja um pokémon, esse item terá tamanho de *span* um, caso não seja, terá tamanho de *span* dois. Vamos começar sobrescrevendo a função `getViewType()` da `PokedexAdapter`:

```kotlin
override fun getItemViewType(position: Int): Int {
    return if (position == itemCount) {
        NETWORK_VIEW_TYPE
    } else {
        POKEMON_VIEW_TYPE
    }
}
```

Se a posição atual for igual a última, ou seja, igual a `itemCount`, o tipo de item de visualização será `NETWORK_VIEW_TYPE`, isto é, será um item que irá exibir a *progress bar* ou o erro. Caso não seja, será um item do tipo `POKEMON_VIEW_TYPE`, em outras palavras, o item exibirá um pokémon. 

Objeto `Constants` modificado:

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

object Constants {
    const val BASE_URL = "https://pokeapi.co/api/v2/"
    const val POKEMON_STARTING_OFFSET = 0
    const val POKEMON_OFFSET = 20
    const val LAST_OFFSET = 885
    const val LAST_POSITION = 45
    const val PAGE_SIZE = 20
    const val NETWORK_VIEW_TYPE = 2
    const val POKEMON_VIEW_TYPE = 1
}
```

Pronto, já podemos começar a conectar o *adpter* à UI. A principal modifição será no método `setUpPokedexRecyclerView()`. É nele que vamos implementar essa lógica de tamanho de *span*:

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

import ...

private const val SPAN_COUNT = 2

class PokedexActivity : AppCompatActivity() {

    //...    

    private fun setUpPokedexRecyclerView() {
        binding.pokedexRecyclerView.apply {
            val gridLayoutManager = GridLayoutManager(context, SPAN_COUNT)
            gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
                override fun getSpanSize(position: Int): Int {
                    val viewType = pokedexAdapter.getItemViewType(position)
                    return if (viewType == POKEMON_VIEW_TYPE) ONE_SPAN_SIZE
                    else TWO_SPANS_SIZE
                }
            }
            layoutManager = gridLayoutManager
            adapter = pokedexAdapter.withLoadStateFooter(
                footer = PokedexLoadStateAdapter { pokedexAdapter.retry() }
            )
        }
    }

    //...

}
```

Objeto `Constants` modificado:

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

object Constants {
    const val BASE_URL = "https://pokeapi.co/api/v2/"
    const val POKEMON_STARTING_OFFSET = 0
    const val POKEMON_OFFSET = 20
    const val LAST_OFFSET = 885
    const val LAST_POSITION = 45
    const val PAGE_SIZE = 20
    const val NETWORK_VIEW_TYPE = 2
    const val POKEMON_VIEW_TYPE = 1
    const val ONE_SPAN_SIZE = 1
    const val TWO_SPANS_SIZE = 2
}
```

Primeiro nós criamos um `GridLayoutManager`, com *span_count* igual a dois. Depois nós instanciamos uma nova classe de `SpanSizeLookUp` como uma classe `singleton` e sobrescrevemos o método `getSpanSizeLookUp()`.  Dentro desse método, nós obtemos o `viewType` do item atual, caso seja um pokémon, seu *span* terá tamanho um, caso não, seu *span* terá tamanho dois. Após isso, definimos que o `gridLayout` que configuramos será o `layoutManager` do *RecyclerView* e na definição do *adapter* fazemos uma modificação no código: informamos que agora a `pokedexAdapter` terá um *footer*, o `PokedexLoadStateAdapter`. Note que aqui finalmente passamos a função que será executada ao clicar no botão de tentar novamente: `pokedexAdapter.retry()`, essa função tenta recarregar qualquer requisição que falhou durante a paginação.

Para prover essa funcionalidade de `retry()` também no carregamento inicial, modificamos nosso layout da _activity_, inserindo um `MaterialButton` que quando clicado executa a função `retry()`:

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

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

    <TextView
        android:id="@+id/pokedexErrorMessage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/error_message"
        android:textAlignment="center"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/pokedexRecyclerView" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/pokedexTryAgainButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="@string/try_again"
        android:visibility="gone"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/pokedexErrorMessage" />

    <com.google.android.material.progressindicator.CircularProgressIndicator
        android:id="@+id/pokedexCircularProgressIndicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

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

Para relembrar como está o `strings.xml`:

```xml
<resources>
    <string name="app_name">Pokédex</string>
    <string name="error_message">Something went wrong\nCheck your network connection and try again</string>
    <string name="try_again">Try again</string>
    <string name="load_error_message">Something went wrong\nTry again</string>
</resources>
```

Sendo assim, a nossa classe `PokedexActivity` ficará dessa forma:

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

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import androidx.recyclerview.widget.GridLayoutManager
import br.com.pokedex.databinding.ActivityPokedexBinding
import br.com.pokedex.util.Constants.NETWORK_VIEW_TYPE
import br.com.pokedex.util.Constants.ONE_SPAN_SIZE
import br.com.pokedex.util.Constants.POKEMON_VIEW_TYPE
import br.com.pokedex.util.Constants.TWO_SPANS_SIZE
import br.com.pokedex.util.hideView
import br.com.pokedex.util.showView
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel

private const val SPAN_COUNT = 2

class PokedexActivity : AppCompatActivity() {

    private val binding by lazy { ActivityPokedexBinding.inflate(layoutInflater) }
    private val pokedexAdapter by lazy { PokedexAdapter(this) }
    private val viewModel: PokedexViewModel by viewModel()

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

        setUpAdapter()
        setUpTryAgainButton()
        setUpPokedexRecyclerView()
        getPokemon()
    }

    private fun setUpAdapter() {
        pokedexAdapter.addLoadStateListener { loadState ->
            when(loadState.refresh) {
                is LoadState.Loading -> setUpLoadingView()
                is LoadState.Error -> setUpErrorView()
                else -> setUpSuccessView()
            }
        }
    }

    private fun setUpSuccessView() {
        binding.apply {
            pokedexRecyclerView.showView()
            pokedexCircularProgressIndicator.hideView()
            pokedexErrorMessage.hideView()
            pokedexTryAgainButton.hideView()
        }
    }

    private fun setUpErrorView() {
        binding.apply {
            pokedexRecyclerView.hideView()
            pokedexCircularProgressIndicator.hideView()
            pokedexErrorMessage.showView()
            pokedexTryAgainButton.showView()
        }
    }

    private fun setUpLoadingView() {
        binding.apply {
            pokedexRecyclerView.hideView()
            pokedexCircularProgressIndicator.showView()
            pokedexErrorMessage.hideView()
            pokedexTryAgainButton.hideView()
        }
    }

    private fun setUpTryAgainButton() {
        binding.pokedexTryAgainButton.setOnClickListener {
            pokedexAdapter.refresh()
        }
    }

    private fun setUpPokedexRecyclerView() {
        binding.pokedexRecyclerView.apply {
            val gridLayoutManager = GridLayoutManager(context, SPAN_COUNT)
            gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
                override fun getSpanSize(position: Int): Int {
                    val viewType = pokedexAdapter.getItemViewType(position)
                    return if (viewType == POKEMON_VIEW_TYPE) ONE_SPAN_SIZE
                    else TWO_SPANS_SIZE
                }
            }
            layoutManager = gridLayoutManager
            adapter = pokedexAdapter.withLoadStateFooter(
                footer = PokedexLoadStateAdapter { pokedexAdapter.retry() }
            )
        }
    }

    private fun getPokemon() {
        lifecycleScope.launch {
            viewModel.getPokemonFlow().collectLatest { pokemon ->
                pokedexAdapter.submitData(pokemon)
            }
        }
    }

}
```

Finalmente nosso trabalho por hoje acabou! Vamos rodar o app e ver como ficaram as telas.

Tela de erro modificada:

![erro_tela_inicial.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3wmarq2zyt6mrps4iyw5.png)

Tela de pokémon com *footer* de carregamento:

![loading_pokemon.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wohb01lz4bu3pofyj49w.png)

Tela de pokémon com o *footer* de erro:

![erro_pokemon.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bsba47jx0jpxzz1h5thb.png)

## Próximos posts

Nos próximos posts vamos embelezar nossa Pokédex usando *Navigation* e *Material Design*, também vamos salvar os dados dos pokémon em um banco de dados usando *Android Room* e testar nosso app usando *JUnit* e *Mockk*.

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

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

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