Xamarin: Удобный REST для Xamarin-приложений

Slava Chernikoff
Feb 24, 2017 · 4 min read

Пятая статья в цикле “Рецепты для Xamarin-разработчиков”.

Оригинал: https://habrahabr.ru/company/microsoft/blog/310704/


Refit для удобного описания клиента REST API

Самым популярным в настоящее время протоколом для общения мобильных приложений с сервером является REST в связке с Json. Поэтому наше сегодняшнее знакомство начнем с библиотеки Refit.

Refit позволяет описать спецификации для работы с REST-сервисом в виде простого Interface с понятным набором входных и выходных параметров, включая возможность манипулировать HTTP-заголовками для отдельных запросов. Для примера возьмем демо-API сервиса httpbin.org:

[Headers("Accept: application/json")]
public interface IHttpbinApi
{
[Get("/basic-auth/{username}/{password}")]
Task<AuthResult> BasicAuth(string username, string password, [Header("Authorization")] string authToken, CancellationToken ctx);
[Get("/cache")]
Task<HttpResponseMessage> CheckIfModified([Header("If-Modified-Since")] string lastUpdateAtString, CancellationToken ctx);
[Post("/post")]
Task<HttpResponseMessage> FormPost([Body(BodySerializationMethod.UrlEncoded)] FormData data, CancellationToken ctx);
}

После описания данного интерфейса, он подается на вход для Refit:

var client = new HttpClient(new NativeMessageHandler())
{
BaseAddress = new Uri("http://httpbin.org")
};
_httpbinApiService = RestService.For<IHttpbinApi>(client);

Сами данные можно при необходимости (конвертации camel case или snake eyes, преобразование из множества в строковые значения) можно расширить аттрибутами из библиотеки Json.net, так как именно она используется в Refit:

public class AuthResult
{
[JsonProperty("authenticated")]
public bool IsAuthenticated { get; set; }
[JsonProperty("user")]
public string Login { get; set; }
}

В Refit в качестве выходного значения можно получить уже преобразованные объекты DTO или HttpResponseMessage. Последний позволяет получить информацию о запросе и ответе, что может быть полезно при отладке. При желании также может использоваться ModernHttpClient при создании HttpClient. В целом, Refit — достаточно удобный и универсальный инструмент для Xamarin-разработчиков в том числе.

Примечание 1: для установки Refit 3.0 в PCL-проект Xamarin.Forms потребуется перевести проект на .NET Standard.

Примечание 2: в документации Refit нет упоминания на использование CancelationToken для отмены активных операций, но данный механизм работает и описан в тикете.

Polly

При разработке мобильных приложений часто приходится учитывать фактор нестабильности сигнала в сотовых сетях, поэтому при выполнении сетевых запросов часто возникает необходимости делать повторные попытки. Это позволяет не отвлекать лишний раз пользователя просьбой повторить запрос.

Интересный подход по использованию Refit и Polly описал Rob Gibbens в своем блоге (дополнительно там показан пример приоретизацией сетевых запросов с помощью Fusillade).

Вот так мы совершаем запрос:

protected async Task<RequestResult> MakeRequest<T>(Func<CancellationToken, Task<T>> loadingFunction, CancellationToken cancellationToken)
{
Exception exception = null;
var result = default(T);
try
{
result = await Policy.Handle<WebException>().Or<HttpRequestException>()
.WaitAndRetryAsync(3, i => TimeSpan.FromMilliseconds(300), (ex, span) => exception = ex)
.ExecuteAsync(loadingFunction, cancellationToken);
}
catch (Exception e)
{
// Сюда приходят ошибки вроде отсутствия интернет-соединения или неправильной работы DNS
exception = e;
}
//TODO: Обработать исключения или передать их дальше
return result;
}

Вместо loadingFunction необходимо передать ваш код обращения к Refit:

var authToken = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
return await MakeRequest(ct => _httpbinApiService.BasicAuth(username, password, authToken, ct), cancellationToken);

Итак, мы интегрировали Refit, Polly и ModernHttpClient.

Кэш

И в завершении статьи можно рассмотреть использование кэша при работе с сетью. Xamarin-разработчику доступны все возможности целевых платформ, поэтому для реализации кэша можно использовать различные СУБД. Одним из самых популярных кэшеров выступает Akavache, работающий поверх SQLite.

var cache = BlobCache.LocalMachine;
var cachedObjects = cache.GetAndFetchLatest("objects", GetRemoteObjectAsync,
offset =>
{
TimeSpan elapsed = DateTimeOffset.Now - offset;
return elapsed > new TimeSpan(hours: 0, minutes: 30, seconds: 0);
});

Также можно использовать для реализации кэша очень удобную мобильную СУБД Realm. Ниже представлен пример кэшера на базе Realm:

public static class LocalCache
{
private class CachedObject : RealmObject
{
[PrimaryKey]
public string Key { get; set; }
public string Value { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}
private static readonly RealmConfiguration Configuration = new RealmConfiguration("cache.realm", true);
private static Realm Db => Realm.GetInstance(Configuration);
public static async Task WriteToCache<T>(string key, T data, DateTimeOffset timeStamp)
{
if (String.IsNullOrEmpty(key) || data == null || timeStamp == DateTimeOffset.MinValue) return;

var currentValue = Db.All<CachedObject>().Where(o => o.Key == key).ToList().FirstOrDefault();
if (currentValue == null)
await Db.WriteAsync(db =>
{
var newValue = db.CreateObject<CachedObject>();
newValue.Key = key;
newValue.UpdatedAt = timeStamp;
newValue.Value = JsonConvert.SerializeObject(data);
});
else
using (var transaction = Db.BeginWrite())
{
currentValue.Value = JsonConvert.SerializeObject(data);
currentValue.UpdatedAt = timeStamp;
transaction.Commit();
}
}
public static DateTimeOffset CacheLastUpdated(string key)
{
if (String.IsNullOrEmpty(key)) return DateTimeOffset.MinValue;

var currentValue = Db.All<CachedObject>().Where(o => o.Key == key).ToList().FirstOrDefault();
return currentValue?.UpdatedAt ?? DateTimeOffset.MinValue;
}
public static void RemoveCache(string key)
{
if (String.IsNullOrEmpty(key)) return;

var currentValue = Db.All<CachedObject>().Where(o => o.Key == key).ToList().FirstOrDefault();
if (currentValue == null) return;
using (var transaction = Db.BeginWrite())
{
Db.Remove(currentValue);
transaction.Commit();
}
}
public static T GetFromCache<T>(string key)
{
if (String.IsNullOrEmpty(key)) return default(T);

var currentValue = Db.All<CachedObject>().Where(o => o.Key == key).ToList().FirstOrDefault();
return currentValue?.Value == null ? default(T) : JsonConvert.DeserializeObject<T>(currentValue.Value);
}
public static void ClearCache()
{
Realm.DeleteRealm(Configuration);
}
}

Заключение

Итак, сегодня мы рассмотрели использование Refit, Json.net, ModernHttpClient, Polly и Realm при интеграции с REST API.

Binwell

Разработка облачных и мобильных решений под заказ

Slava Chernikoff

Written by

Tech Lead at Binwell / Microsoft MVP

Binwell

Binwell

Разработка облачных и мобильных решений под заказ

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade