All Articles

Refit para Xamarin Forms

¿Qué es el calendario de adviento?

Me gustaría antes de comenzar contaros una historia de donde surge el calendario de adviento y que es exactamente.

El calendario de adviento es una cuenta regresiva. Sirve para ir descontando los días que quedan hasta Nochebuena desde el 1 al 24 de diciembre. Tiene su origen en una tradición alemana del siglo XIX, según la cual los niños encendían una vela en cada día del periodo de adviento para contar cuando días quedaban para navidad. En los años veinte se imprimió el primer calendario de adviento, que incluía tabletas de chocolate para cada día de espera y así endulzar la espera.

Luis Beltrán tuvo la brillante idea de crear el primer adviento de Xamarin para endulzarnos a los Xamarianos la espera del día de navidad. Desde aquí quiero felicitarle por la iniciativa. El adviento de Xamarin la comunidad colabora escribiendo un post o un vídeo desde el 1 al 28 de diciembre. Os dejo un enlace donde podréis consultar todo el contenido que se ha generado aquí.

Un vez hecha la introducción, vamos al lio.

Introducción

Cuando trabajamos con Xamarin Forms por lo general siempre mostramos información que provenga de algún servicio. El objetivo de este post es consumir un servicio RESTful en una aplicación .Net Standard de Xamarin.Forms con Refit.

¿Que es Refit?

Refit es una librería open source creado por Paul Betts y forma parte de mi lista de nuget imprescindibles para desarrollar en Xamarin Forms. La principal razón de que me guste tanto es por que consigo tener un código muy limpio, fácil de entender y de mantener.

Al usar está librería conseguimos conumir servicios RESTful en interfaces vivas, de forma que podemos modelar cualquier servicio que necesitemos y consumirlos a través de atributos. En concreto tenemos 5: Get, Post, Put, Delete y Head.

Para los que han trabajado en android nativo o iOS nativo los equivalentes a Refit sería Retrofit para android y Alamofire para iOS.

Consumir apis REST con Refit

Tenemos que tener Refit instalado tanto en nuestra PCL como en nuestros proyectos de plataforma.

Para esta demostración usare una API que tengo alojada en github (https://github.com/jorgemht/jfilmapi) que nos da acceso a detalles de películas. En este enlace podeís consultar el schema de la base de datos, la entidades y los end-points que están disponibles.

Para consumir está api REST solo necesitamos realizar 2 pasos y pocas líneas de código para que tengamos disponible un servicio en nuestra App que consuma la api REST.

  1. Crear una interfaz con los endpoints API. La librería hará el trabajo por nosotros para atacar esos endpoints.
public interface IFilmService
{
    [Get("/film")]
    Task<IEnumerable<Film>> GetAll();

    [Get("/film?filmId={id}")]
    Task<Film> GetById([AliasAs("id")] int filmId);
    
    [Post("/film")]
    Task AddFilm([Body] Film film, [Header("Authorization")] string token);
}

Cada método de nuestra interfaz debe tener un atributo http definido. También podemos especificar parámetros directamente en el atributo, como hemos definido en el método GetById. Hay que tener en cuenta que siempre necesita comenzar con un ”/” si no queremos encontrarnos con un error.

  1. Con refit llamamos a RestService, que nos dará una implementación de nuestra interfaz:
public class ApiService : IApiService
{
	private string baseUrl = "https://my-json-server.typicode.com/jorgemht/demo/";
	private readonly IFilmService filmService;

	public ApiService()
	{
		filmService = RestService.For<IFilmService>(baseUrl);
	}

	public async Task<IEnumerable<Film>> GetAllFilm() => await filmService.GetAll();

	public async Task<Film> GetFilmById(int id) 
	{
		IEnumerable<Film> films = await filmService.GetById(id);
		return films.ToList().FirstOrDefault();
	}
}

El IApiService lo único que contiene son los métodos que debe implementar nuestro servicio ApiService.

Consumir nuestra API

Por ejemplo, para obtener una lista de películas basta con llamar a nuestro servicio apiService de la siguiente manera:

await apiService.GetAllFilm();

O por ejemplo, para obtener una película basta con llamar a nuestro servicio apiService de la siguiente manera:

await apiService.GetFilmById(id);

Trabajar sin Refit

Si optamos por no usar refit nosotros tendríamos que:

  1. Inicializar nuestro objeto HttpClient:
    HttpClient CreateHttpClient(string token = "")
    {
        if (Connectivity.NetworkAccess != NetworkAccess.Internet)
	        throw new ConnectivityException();

        var httpClient = new HttpClient();
        httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        if (!string.IsNullOrEmpty(token))
        {
	        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        }
        
        return httpClient;
     }
  1. Inicializar nuestro JsonSerializerSettings (también en refit lo podemos hacer nosotros):
JsonSerializerSettings serializerSettings = new JsonSerializerSettings
{
	ContractResolver = new CamelCasePropertyNamesContractResolver(),
	DateTimeZoneHandling = DateTimeZoneHandling.Utc,
	NullValueHandling = NullValueHandling.Ignore
};

serializerSettings.Converters.Add(new StringEnumConverter());
  1. Crear los métodos para realizar las peticiones HTTP que vamos a necesitar:
public async Task<TResult> GetAsync<TResult>(string uri, string token = "")
{
	var httpClient = CreateHttpClient(token);

	var response = await httpClient.GetAsync(uri);

	await HandleResponse(response);

	var serialized = await response.Content.ReadAsStringAsync();

	return await Task.Run(() => JsonConvert.DeserializeObject<TResult>(serialized, serializerSettings));
}
public async Task<TResult> PostAsync<TRequest, TResult>(string uri, TRequest data, string token = "")
{
	var httpClient = CreateHttpClient(token);

	var serialized = await Task.Run(() => JsonConvert.SerializeObject(data, serializerSettings));

	var response = await httpClient.PostAsync(uri, new StringContent(serialized, Encoding.UTF8, "application/json"));

	await HandleResponse(response);

	var responseData = await response.Content.ReadAsStringAsync();

	return await Task.Run(() => JsonConvert.DeserializeObject<TResult>(responseData, serializerSettings));
}
public async Task<TResult> PutAsync<TRequest, TResult>(string uri, TRequest data, string token = "")
{
	var httpClient = CreateHttpClient(token);

	var serialized = await Task.Run(() => JsonConvert.SerializeObject(data, serializerSettings));

	var response = await httpClient.PutAsync(uri, new StringContent(serialized, Encoding.UTF8, "application/json"));

	await HandleResponse(response);

	var responseData = await response.Content.ReadAsStringAsync();

	return await Task.Run(() => JsonConvert.DeserializeObject<TResult>(responseData, serializerSettings));
}
private async Task HandleResponse(HttpResponseMessage response)
{
	if (!response.IsSuccessStatusCode)
	{
		var content = await response.Content.ReadAsStringAsync();

		if (response.StatusCode == HttpStatusCode.Forbidden || response.StatusCode == HttpStatusCode.Unauthorized)
		{
			throw new Exception(content);
		}

		throw new HttpRequestException(content);
	}
}

Gracias a Refit nos evitamos escribir todo ese código y nuestro servicio ApiService se queda más limpio y estructurado.

Al final lo que logramos con Refit es tener estructurados en interfaces los endpoints de la API Rest que vamos a consumir y una clase, por ejemplo, ApiService que será el servicio que usaremos para obtener los datos que necesitemos en nuestra aplicación. Refit hace esa magia, con pocas líneas de código ataquemos a cualquier servicio RESTful y obtengamos un resultado.

Trabajar con Refit

Vamos a analizar el código que hemos visto arriba y ver como Refit trabaja.

Body content

De los parámetros que tenga nuestro método, solo un parámetro puede ser usado como el body de la request usando el atributo body.

[Post("/film")]
Task AddFilm([Body] Film film, [Header("Authorization")] string token);

Request headers

En el método anterior le estamos pasando un objeto de tipo Film y un token para autentificarnos en nuestra API. En este caso el token se lo pasamos en la cabecera de la petición. Para ello se lo establecemos con el atributo header.

Con Refit podemos agregar encabezados a las solicitudes. Tenemos dos maneras, podemos asignársela de manera estática o dinámica.

Static

Si nuestro Header no va a cambiar, podemos configurarlo como estático agregando el atributo [Headers] y pasando los encabezados.

[Headers("Content-Type: application/json")] 
public interface IApiService
{
	Task<IEnumerable<Film>> GetAllFilm();

	Task<Film> GetFilmById(int id);
}

Dynamic

Cuando sea dinámico lo pasaremos como cualquier otro parámetro, solo tenemos que asegurarnos de pasar el atributo [Headers] asignándole el nombre del valor.

[Post("/film")]
Task AddFilm([Body] Film film, [Header("Authorization")] string token);

Query

En el atributo de nuestro método podemos especificar los parámetros de nuestra request.

[Get("/film?filmId={id}")]
Task<Models.Film> GetById([AliasAs("id")] int filmId);

En caso el nombre del parámetro no encaje con el nombre en la URL podemos emplear otro atributo de refit, AliasAs.

Additional configuration

Refit proporciona RefitSettings donde puede especificar configuraciones adicionales como el serializador de contenido. Por defecto, usa las configuraciones globales de JsonNET, los Newtonsoft.Json.JsonConvert.DefaultSettings.

Cuando creamos la implementación de nuestra interfaz usando Refit, podemos opcionalmente asignarle un objeto RefitSettings que nos permite especificar las configuraciones de socialización para ese cliente API en particular:

filmService= RestService.For<IFilmService>(baseUrl, new RefitSettings
{ 
	JsonSerializerSettings = new JsonSerializerSettings
	{
	    Formatting = Formatting.Indented,
	    TypeNameHandling = TypeNameHandling.Objects,
	    ContractResolver = new CamelCasePropertyNamesContractResolver()
	}
 });

Puedes consultar aquí el objeto JsonSerializerSettings.

Exception handling

Refit ofrece dos tipos de excepciones:

  • ApiException: captura cualquier excepción entrante de la API.
  • ValidationApiException: catch BadRequest exceptions.
try
{	
	IEnumerable<Film> films = await filmService.GetById(id);
	return films.ToList().FirstOrDefault();
}
catch (ValidationApiException validationException)
{
}
catch (ApiException exception)
{
}

Herramientas útiles

Quicktype - Nos permite generar instantáneamente clases C# y métodos desde un JSON.

Reto

Implementar la API SWAPI en un nuevo proyecto y obtén una lista de los personajes de las guerras de las galaxias.

Conclusiones

Refit me parece una maravilla por la sencillez que tenemos para consultar nuestros servicios RESTful. Solo necesitamos crear una interfaz con el endpoint de la API y llamar a RestService. Con eso ya obtenemos una implementación de la interfaz. Los atributos que trabaja está librería son bastante simples, son solo los verbos HTTP que desea usar y el head para las cabeceras. Además generará métodos asíncronos, por lo que siempre deberá devolver una Tarea o Tarea .

Más información sobre el proyecto

Por último, no olvides seguirme en GitHub para ver el código de los proyectos que voy subiendo :)

De momento esto es todo. Happy coding Xamarianos.

Más información

Puede explorar más al consultar su repositorio GitHub.