Async/Await: Camada Network genérica com Swift 5.5
Receber uma response de um servidor é uma das partes mais legais de quando está aprendendo a desenvolver uma aplicação front-end. É nessa interação que o app começa a ganhar vida.
Esse artigo vai te mostrar como fazer uma camada de Network simples para o seu app de forma genérica, sem que você precise ficar escrevendo um monte de código toda hora que precisar fazer uma request, ou seja, evitando boilerplate.
Caso não domine ainda o conceito de API, recomendo que antes de dar continuidade leia este artigo. Além disso, é importante também ter uma boa noção sobre Protocol Oriented Programming (POP) e este artigo sobre async/await.
TMDB: The Movie Database
Para nos auxiliar neste artigo, iremos utilizar a API do TMDB para realizar algumas requests, as quais você pode ver a documentação clicando aqui. De forma sucinta, o TMDB é um banco de dados que contém diversas informações sobre filmes, TV, séries, etc.
O uso da API é gratuita, porém isso não significa que ela seja completamente aberta. Para fazer as requisições você precisará criar uma conta para ter o seu Access Token. Assim:
1- Acesse o site do The Movie Database: https://www.themoviedb.org/
2- Clique em “Join TMDB”
3- Faça o seu cadastro e depois faça o login.
4- Vá até Settings > API: https://www.themoviedb.org/settings/api
Pronto. No final da página você vai encontrar o seu token na seção API Read Access Token (v4 auth). É algo assim:
Projeto
Para que você possa seguir o artigo, criei este projeto de exemplo que está utilizando a arquitetura MVC. Você pode clonar o repositório e abrir o .xcodeproject
para acompanhar, onde você encontrará estas três pastas onde estão localizadas os principais arquivos que iremos abordar neste primeiro momento.
- Endpoint: Um protocol de configuração de todos os endpoints.
- HTTPClient: Um dos arquivos mais importantes para este artigo. É nele que se encontra o método que executa as requisições genéricas.
- HTTPMethod: Enum com os principais métodos HTTP para realizar as request. Se necessário, você pode adicionar outros, como head, trace, options, etc.
- RequestError: Enum com alguns erros que gerenciaremos. Caso a API que você consuma ou o seu app tenha outros casos particulares, adicione-os neste enum.
- MoviesEndpoint: Enum com os endpoints do serviço Movies da API do TMDB. Para cada case é possível configurar o endpoint específico, determinando as variáveis
path
,method
,header
ebody
. - MoviesService: Struct responsável para realizar as requests do serviço Movies.
- TopRated: Model para fazer o decode da response do endpoint top_rated.
- Movie: Model para fazer o decode das responses que contém filme(s).
- MainViewController: A única Controller do projeto, responsável por fazer a requisição e mostrar a response.
Camada de Network
Antes de chegar no mais legal do artigo (HTTPClient) precisamos passar por outras partes para que você possa entender tudo o que está acontecendo.
O Endpoint é um protocol onde todos os endpoints precisam conformar para informar todos os seus detalhes: scheme
, host
, path
, method
, header
e body
.
Ele possui uma implementação default das variáveis scheme
e host
pois normalmente são iguais para todos os endpoints e, portanto, não é necessário implementar a todo momento. Se necessário, é possível fazer um override dessa implementação.
A variávelpath
será utilizada como complemento da scheme
e host
para formar a URL do endpoint. Funciona assim: scheme
+ host
+ path
. Por exemplo: suponha que a URL do endpoint seja https://api.themoviedb.org/3/movie/top_rated
, então o path
deve ser /3/movie/top_rated
.
A variável header
deve retornar um dicionário com todos as informações de header que a documentação do endpoint pedir, quando necessário. Normalmente é nele que se faz a autenticação por meio do Authorization, que você verá a seguir.
Assim como a variável header
, quando necessário, o body
também é um dicionário que deve conter as informações do body que a documentação do endpoint pedir para que sejam enviadas.
A variável method
é do tipo RequestMethod
, que representa o método HTTP do endpoint, podendo ser: GET, POST, PUT, DELETE, PATCH, HEAD, etc. Para esta API foram colocados apenas os 5 principais métodos, mas você pode adicionar mais caso faça sentido no seu contexto.
O enum RequestError contém alguns possíveis erros que o HTTPClient irá identificar e retornar para que possa ser gerenciado pelo app.
Ele possui a variável customMessage
, que pode ser utilizada para exibir uma mensagem de feedback para o usuário ou para track de erros, por exemplo.
A struct TopRated é um model Codable para realizar o decode da response do endpoint top_rated da API do TMDB. Observe que ela possui o enum CodingKeys que foi utilizado para renomear as entradas Snake case (enviadas pela API, como “total_pages”) para Camel case, que é o padrão mais utilizado em swift. A struct Movie
segue a mesma ideia.
Bom, agora que você já conhece todas as partes necessárias para se entender o HTTPClient, finalmente chegou a parte mais importante deste artigo.
HTTPClient é um protocol com uma implementação default da funçãosendRequest
, responsável por realizar a request para o servidor. Como parâmetro ela recebe o Endpoint e o tipo do model a ser realizado o decode da response da API. Já como retorno ele lança um Result
que pode ser um success com a response decodificada ou então um error que com um dos cases criados no RequestError
. E como você pode perceber, ela está acompanhada da keyword async
, que representa que é uma função assíncrona que irá rodar em uma thread separada e pode lançar o Result
a qualquer momento.
Como foi dito, o protocol Endpoint
contém todas as informações necessárias para consumi-lo. É aqui que todas essas informações serão utilizadas.
O método inicia criando a URL
a ser utilizada com o URLComponents. Aqui é onde usaremos as variáveis scheme
, host
e path
para configurar o objeto urlComponents. Através dele vamos obter a URL para ser utilizada na instanciação da URLRequest e, caso falhe, o erro .invalidURL
será retornado.
Depois isso, precisamos criar uma URLRequest
para o consumo do endpoint. Mas pera aí, o que é essa URLRequest
? A documentação da Apple irá nos ajudar:
URLRequest
encapsulates two essential properties of a load request: the URL to load and the policies used to load it. In addition, for HTTP and HTTPS requests,URLRequest
includes the HTTP method (GET
,POST
, and so on) and the HTTP headers.
Então o que o código faz em seguida é criar uma URLRequest
a partir da URL instanciada. Depois disso faz-se a configuração do httpMethod
e allHTTPHeaderFields
através, respectivamente, das variáveis method
e header
do Endpoint passado por parâmetro.
O httpBody
também é configurado seguindo a mesma lógica, caso haja algum dado a ser enviado para o endpoint.
Agora só falta realizar a request. Estamos perto! Para isso, utilizaremos a URLSession.shared.data
com a nossa URLRequest
que acabou de ser criada. Mas espera aí, o que é URLSession
? De novo, vamos recorrer à documentação da Apple:
The
URLSession
class and related classes provide an API for downloading data from and uploading data to endpoints indicated by URLs.
Em outras palavras, é a classe que abstrai toda a comunicação entre o app e a API. É ela que, com todas as informações necessárias para a realização da request, nos retornará a response da API.
Na linha abaixo é criada a tupla de retorno da função URL.shared.data()
. Perceba que é necessário utilizar um try
pois uma exception pode ser lançada ao rodar esta função (é por este motivo que estamos usando um do/catch) e, além disso, também possui a keyword await
, que fará com que a próxima linha seja executada apenas quando a função URL.shared.data()
finalizar e retornar a tupla.
Agora o trabalho está praticamente finalizado. Se a API retornou alguma response, prosseguimos com a análise do statusCode
e, caso contrário, retornamos o erro .noResponse
do RequestError
.
Dado que a API retornou uma response, a análise do statusCode
é feita em cima dos possíveis status HTTP. Para este artigo foi feita uma triagem de um caso ideal, onde [200, 201, …, 299] seria considerado um success
e portanto podemos tentar fazer o decode da response, transformando o JSON recebido em um model Decodable e finalmente retornando para que o responsável possa utilizá-lo.
O caso 401 (unauthorized), normalmente causado por uma sessão expirada, também foi considerado, a qual cada contexto exige uma ação do app (por exemplo: pedir para o usuário fazer o login novamente).
Uso
Agora que você já entendeu como funciona a camada de Network, é necessário entender como utilizá-la. É fácil, vamos lá!
Para todos os endpoints dentro do serviço Movies, nós utilizaremos o MoviesEndpoint.
O MoviesEndpoint é enum que conforma com o protocol Endpoint
. Ele possui todas as informações necessárias para cada endpoint. Para adicionar um novo endpoint do serviço Movies, basta adicionar um novo case e completar as informações dentro de cada variável: path
, method
, header
e body
.
Finalmente vamos falar do Service, que é a struct responsável por realizar a request.
A MoviesService é a struct utilizada pelo app para fazer as requests. Repare aqui em alguns pontos importantes:
- Ela conforma com o protocol HTTPClient, o que significa que ela possui internamente a função
sendRequest
para realizar as requests e, por isso, não precisa ficar repetindo código para cada request. - Ela conforma com o protocol MoviesServiceable, o que é de suma importância para a realização dos testes e da injeção de dependência. Você verá mais detalhes em breve.
- A declaração das funções possuem as keywords
async
, seguindo a mesma lógica já comentada na HTTPClient, evidenciando que é uma função assíncrona.
Agora que você entendeu como as structs Service funcionam, basta ver como é a sua utilização na MainViewController.
O MoviesService que criamos será utilizado através de injeção de dependência de um objeto que conforme com o protocolo MoviesServiceable. A MainViewController foi instanciada assim:
Essa injeção de dependência poderia ser feita de outra forma, mas foi feita assim para não deixar o artigo muito extenso. Poderia ser utilizado, por exemplo, a arquitetura MVVM e injetado na ViewController um ViewModel que houvesse o service dentro.
Agora só falta de fato fazer a request utilizando o Service injetado. Como estamos utilizando uma função async
, precisamos criar uma Task para rodar o código assíncrono. Veja a documentação da Apple referente à esta função:
Use this function when creating asynchronous work that operates on behalf of the synchronous function that calls it.
O await
fará com que a linha do switch
seja executada apenas quando a requisição retornar um Result<Movie, RequestError>
. Ao retornar, é possível fazer o switch
para gerenciá-lo da forma necessária dentro de cada contexto.
É isso! Agora vamos ver um pouco sobre como podemos testar nossa camada de Network
e utilizar mocks.
Testes
Agora você só precisa saber como fazer testes com a sua camada de network e utilizar mocks. O projeto possui os 4 arquivos abaixos:
- RequestAppTests: Classe responsável por realizar os testes do app.
- Mockable: Um protocolo para converter arquivos JSON em Codable.
- top_rated_response: Arquivo .json com um exemplo de retorno da API topRated do TMDB.
- movie_response: Arquivo .json com um exemplo de retorno da API de detalhe do um filme do TMDB.
O Mockable é um protocolo que evita boilerplate com uma implementação default da função loadJSON, que lê um arquivo interno .json e converte em um determinado model Codable.
Nada demais, certo? Agora veja o código abaixo de como podemos utilizar o protocol Mockable e o MoviesServiceable para criar um mock.
Você se lembra que a nossa classe de Service utilizada no app (MoviesService) também conforma com o protocol MoviesServiceable? Você também se lembra que quando utilizamos a injeção de dependência na ViewController nós injetamos uma instância do tipo MoviesServiceable e não um MoviesService? Dessa forma você pode injetar uma outra instância diferente para poder reutilizar e testar a sua ViewController. Veja o caso do mock abaixo:
Como o função loadTableView é responsável por fazer a request e atualizar a variável movies
, então o movies.count
deve ser igual a quantidade de itens no meu top_rated_response.json, que no caso é apenas 1. Além disso, o código também testa se o título deste item é o mesmo título contido no arquivo JSON.
Também é possível você testar somente o seu mock, assim:
Existem diversas formas de implementar os testes. Por exemplo, poderíamos ter um ViewModel com o Service injetável para testar requests e diversas funções da responsabilidade do ViewModel e separadamente realizar testes de UI com a ViewController.
Para utilizar outros JSONs no retorno do mock poderíamos criar outras classes Mockables & MoviesServiceable com diferentes retornos, ou então tornar a classe MoviesServiceMock mais reutilizável, injetando nomes de arquivos, tipos de falhas, etc.
Também seria possível deixar a camada de network mais robusta com implementação de cache, multipart, gerenciando outros status codes, etc. Além disso, poderíamos injetar o próprio JSONDecoder e URLSession para tornar ainda mais reutilizável e testável.O artigo se baseou em um contexto simples, mas cada contexto exige uma implementação e complexidade específica.
Gostou deste artigo? Me ajude a continuar escrevendo compartilhando com seus amigos, seguindo aqui no Medium e deixando suas palmas 👏 ❤️
Você também pode me adicionar no LinkedIn ;)
Abraços!