“Social Project” – Xamarin Workbook #2 – Refit i Akavache

W prowadzonym projekcie korzystam z biblioteki Refit do komunikacji z Firebase, wspomagając całość biblioteką o nazwie Akavache. Czym jest Akavache i jak może pomóc Ci poprawić wydajność aplikacji przy wykonywaniu żądań sieciowych? Wszystkiego tego zaraz się dowiesz!

Wstęp

Co kryje się pod tą tajemniczą nazwą Akavache? Jest to narzędzie stosowane do cachowania danych. Natomiast Refit jest bezpieczną pod względem typów biblioteką do komunikacji typu REST. Co da mi użycie Akavache? Wyobraź sobie sytuację, że masz dwa ekrany w aplikacji. Na jednym z nich masz listę z danymi, które pobierasz z serwera za każdym razem kiedy wejdziesz na ten widok. Jednocześnie zdajesz sobie sprawę, że ta lista nie zmienia się zbyt często. Poniekąd bez sensu jest pobieranie nowych danych z serwera za każdym razem kiedy ponownie wejdziesz na ten widok, co gorsza jeżeli będzie to częste. I tu znajduje zastosowanie biblioteka Akavache. Kiedy połączymy ją z Refitem. Okaże się, że możemy jej powiedzieć:


“Wrzuć do cache te dane. Ustaw ich ważność na hmm… 10 minut. Jeżeli zapytam znowu przed upływem tego czasu, zwróć mi cache. Inaczej pobierz nowe dane”


Chyba całkiem mądre?

Spójrz w jaki sposób łącze działanie obu bibliotek tak, aby kontrolować żądania sieciowe.

Kilka prostych kroków

Wszystkie klasy oraz interfejsy tworzę w projekcie corowym. Jest to o tyle elastyczne rozwiązanie, że dwa pozostałe projekty platformowe(iOS oraz Android) korzystają z Refita nieświadomie – całość ukryta jest za abstrakcją.

Utworzenie interfejsu o nazwie IApiConnection:

public interface IApiConnectionService
{
    [Get("/users/{userId}.json?auth={token}")]
    Task<UserDTO> GetUser(string userId, string token);

    [Put("/challenges/{userId}/{challengeId}.json?auth={token}")]
    Task<bool> CreateChallenge(string userId, string challengeId, [Body] string challengeConditions, string token);
}

Cała magia Refita dzieje się w tym interfejsie, a dokładnie w dodanych atrybutach metod. Spójrzmy na pierwszą z nich GetUser. Parametr o nazwie Get przyjmuje wartość typu string, a w nim ścieżkę zapytania. Metoda GetUser przyjmuje dwa parametry: identyfikator użytkownika, którego chcemy pobrać oraz wymagany token autentykacji Firebase. Spójrz na wartość ciągu znaków w parametrze. Pomiędzy użytymi tam nawiasami klamrowymi znajdują się identyczne nazwy jak przy parametrach metod. W ten sposób  przekazujemy odpowiednie wartości do zapytania. Prawda, że łatwe?

Druga metoda korzysta z metody Put do dodania nowego użytkownika do bazy. Obiekt do wysłania zaznaczamy za pomocą atrybuty Body. Wystarczy, że umieścisz go obok parametru, który masz zamiar wysłać. Refit zrobi resztę za Ciebie!

Teraz utwórz właściwy serwis, który implementuje powyższy interfejs.

public class ApiConnectionService : IApiConnectionService
{
    private readonly IApiConnection _serviceClient;

    public ApiConnection(IApiConnectionService serviceClient)
    {
        _serviceClient = serviceClient;
    }

    public async Task<UserDTO> GetUser(string id, string token)
    {
        return await FetchOrGetCacheAsync(async () => await _serviceClient.GetUser(id, token),
                                            () => null, $"GetUser{id}", TimeSpan.FromSeconds(1)).ConfigureAwait(false);
    }

    public async Task<bool> CreateChallenge(string userId, string challengeId, [Body(BodySerializationMethod.Json)] string challengeConditions,
                                            string token)
    {
        return await FetchOrGetCacheAsync(async () => await _serviceClient.CreateChallenge(userId, challengeId, challengeConditions, token),
                                            () => false, $"CreateChallenge{userId}", TimeSpan.FromSeconds(1)).ConfigureAwait(false);
    }

    private async Task<T> FetchOrGetCacheAsync<T>(Func<Task<T>> fetchFunc, Func<T> fallbackFunc, string cacheKey, TimeSpan expiration)
    {
        try
        {
            return await BlobCache.LocalMachine.GetObject<T>(cacheKey);
        }
        catch (KeyNotFoundException)
        {
            MvxTrace.TaggedTrace("cache", $"Klucz {cacheKey} nie istnieje. Pobierz dane.");
        }
        catch (Exception e)
        {
            MvxTrace.TaggedError("cache", $"Problem z cachowaniem danych o kluczu {cacheKey}, {e}");
        }

        var res = await FetchAsync(fetchFunc, fallbackFunc);

        if (res.Success)
        {
            try
            {
                await BlobCache.LocalMachine.InsertObject(cacheKey, res.Content, expiration);
            }
            catch (Exception e)
            {
                Debug.TaggedError("cache", $"Problem z dodanie danych do cache. Klucz: {cacheKey}, {e}");
            }
        }
        return res.Content;
    }

    private async Task<Result<T>> FetchAsync<T>(Func<Task<T>> fetchFunc, Func<T> fallbackFunc)
    {
        try
        {
            var res = await fetchFunc().ConfigureAwait(false);
            return new Result<T> { Content = res, Success = true };
        }
        catch (Exception e)
        {
            MvxTrace.TaggedError("api", $"Wystąpił błąd {e}");
        }

        return new Result<T> { Content = fallbackFunc() };
    }
}

Pojawiły się magiczne metody. Już tłumaczę. Oprócz implementacji interfejsu dodałem dwie metody: FetchOrGetCacheAsync oraz FetchAsync. Służą one do obsługi cachowania danych. To ich działanie decyduje o tym czy dane zostaną pobrane czy zwrócone z magazynu danych. Przykładowe wywołanie FetchOrGetCacheAsync:

return await FetchOrGetCacheAsync(async () => await _serviceClient.GetUser(id, token),
                                            () => null, $"GetUser{id}", TimeSpan.FromSeconds(1)).ConfigureAwait(false);

Pierwszym parametrem jest delegat, który rzeczywiście odpowiada za pobranie danych. Jest to metoda z naszego interfejsu, która “posiada obsługę” żądań sieciowych. Drugi parametr określa domyślną wartość do zwrócenia w przypadku braku danych. Parametr trzeci jest bardzo ważny, ponieważ jest kluczem używanym przy cachowaniu danych. Ostatni parametr określa czas ważności danych w cache. Kiedy czas minie, a my zażądamy danych zostanie wykonane żądanie sieciowe, a my dostaniemy świeże dane.

Tyle jeżeli chodzi o sam serwis.

HttpClient

Musimy jeszcze zdefiniować klienta Http, którego Refit będzie używał.

var client = new HttpClient(new NativeMessageHandler())
{
    BaseAddress = new Uri("Adres"),
    Timeout = TimeSpan.FromSeconds(10)
};

Mvx.RegisterSingleton(() => RestService.For<IApiConnectionService>(client));

W projekcie korzystam z DI MvvmCross, więc ostatnia linijka służy do dostarczenia implementacji dla RestService. Jest to serwis wykorzystywany przez bibliotekę Refit. Nie korzystam bezpośrednio z ApiConnectionService, ale posiadam osłonowy serwis. Przykładowa metoda wygląda następująco:

public async Task<bool> CreateChallenge(string userId, string challengeId, string challengeConditions)
{
    return await _apiConnection.CreateChallenge(userId, challengeId, challengeConditions, _token);
}

To już wszystko

Przedstawiłem podstawy korzystania z Refit przy użyciu Akavache do cachowania danych. Artykuł miał na celu pokazanie istnienie obu bibliotek oraz ich przykładowego użycia.

Do następnego!

Mam coś dla Ciebie

Zapisz się do mojego newslettera, a ja prześlę Ci zbiór kilkunastu praktycznych wskazówek dla programisty aplikacji mobilnych.

Menu