[OLD] ISS, Architecture: ViewModel, LiveData, Retrofit
[This is a post from my old website. Outdated packages and libraries. Viewer discretion is advised ;-)]
W tym artykule stworzymy prostą aplikację na Android wyświetlającą aktualną pozycję Międzynarodowej Stacji Kosmicznej, wykorzystującą ViewModel, LiveData oraz bibliotekę Retrofit. Przy okazji wyjaśnimy jak działają dodane w zeszłym roku komponenty architektury Androida.
Aplikacja wyglądać będzie tak:
{: .align-center}
Komponenty architektury Androida
Architecture Components to biblioteki dla Androida, zaprezentowane przez Google na I/O ‘17. Mają one ułatwić proces projektowania architektury aplikacji. Dotąd zespół Androida nie rekomendował żadnego konkretnego wzorca architektury. A tych popularnych trochę jest, by wymienić same model-view-*.
Dodatkowe komponenty i rekomendowany przepływ danych można zobrazować tak:
{: .align-center}
Warstwa UI oddzielona jest od tej częsci kodu, która jest odpowiedzialna za zdobywanie i przygotowywanie danych przez ViewModel
. Obiekty tej klasy nie są niszczone wskutek zmian konfiguracyjnych aplikacji (np. zmiany orientacji ekranu), są lifecycle-aware, więc nie ma już potrzeby pakowania multum danych w onSaveInstanceState()
. ViewModel przetrwa, jeśli w Activity, w której był przywołany, zostanie użyte onDestroy()
, o ile tylko nie będzie to się wiązało z wywołaniem metody finish()
(więcej tutaj).
Sam ViewModel
również nie ma być odpowiedzialny za operacje na bazie danych albo zewnętrznym API, dostaje takie gotowe dane z Repository
. Sama klasa Repository nie wchodzi w skład bibliotek Architecture Components. Obiekty należące do tej klasy mają pośredniczyć w wymianie danych pomiędzy źródłem danych (API/SQLite/itd.) a ViewModelem.
Biblioteka Room
to warstwa abstrakcji nad SQLite, ułatwiająca (= mniej kodu) dostęp i manipulację danymi z bazy danych. Od strony RESTfulowych usług, pobierania danych spoza naszej aplikacji przy pomocy jakiegoś API, Android nie dostarcza gotowej biblioteki. W dokumentacji natomiast korzysta z Retrofit
, stąd gwiazdka na diagramie :).
Pozostaje kwestia LiveData
. Jest to data holder, który może być obserwowany. Innymi słowy, jeśli nasze dane zostaną przekazane przez ViewModel do warstwy UI jako LiveData, to obserwowanie stanu danych w LiveData wystarczy, by przy zmianie w danych zmianie też uległa wyświetlana dla użytkownika treść na ekranie.
Aplikacja WhereIsSpaceStation
W tym wpisie zajmiemy się “prawą stroną” diagramy. Nie będziemy niczego zapisywać w bazie danych, wykorzystamy LiveData, ViewModel, Repository i Retrofit by wyświetlić na ekranie aktualną pozycję ISS. Po naciśnięciu przycisku informacja na ekranie będzie aktualizowana. W przyszłości dodamy też bazę danych.
Skorzystamy z:
- Android Studio (na dzień pisania wpisu jest to wersja 3.1.1)
- OpenNotify API
- Retrofit
- Biblioteki Architecture Components
Ustawienia projektu
Tworzymy nowy projekt w Android Studio:
{: .align-center}
{: .align-center}
Resztę ustawień pozostawiamy domyślne. Kreator powinien nam stworzyć nowy projekt, z jedną Activity (wedle templatki EmptyActivity).
Nasza aplikacja będzie korzystać z połączenia z internetem, więc w AndroidManifest.xml
musimy dodać:
|
|
Architecture Components na dzień dzisiejszy trzeba ręcznie dodać do projektu. W build.gradle
projektu sprawdzamy, czy wśród repozytoriów znajduje się google():
|
|
A w build.gradle
na poziomie modułu aplikacji dodajemy potrzebne biblioteki. Aktualne wersje bibliotek znaleźć można tutaj:
|
|
Synchronizujemy i budujemy projekt, sprawdzając, czy nie ma błędów kompilacji :).
Resources
Nasza aplikacja będzie bardzo prosta, więc możemy od razu ustawić potrzebne pliki w katalogu res
.
W strings.xml dodajemy kilka Stringów:
|
|
A w pliku XML naszego Activity (MainActivity.xml):
|
|
Na nasze UI składać się będzie (oprócz paska aplikacji): TextView z na sztywno ustawionym tekstem (“Where is the ISS now?”), TextView, w którym wyświetlać będziemy lokalizację ISS oraz Button, którego naciśnięcie zaktualizuje dane o lokalizacji.
Konfiguracja Retrofit
Ustawienie Retrofit jest bardzo proste. Potrzebujemy adresu, pod który wysyłane będzie żądanie, POJO, w których Retrofit będzie zapisywać dane zwrotne i jednego interfejsu.
OpenNotify, API z którego skorzystamy, jest bardzo przyjemne, bo nie wymaga żadnej autoryzacji. Adres, pod którym znajdują się potrzebne nam dane o lokalizacji ISS jest taki: http://api.open-notify.org/iss-now.json
. Jak widać, dane zwrotne są w postaci JSON, będziemy musieli poinformować o tym Retrofit przy konfiguracji.
POJO wraz z anotacjami możemy wykreować automatycznie. W Android Studio istnieje możliwość używania pluginów. Klikamy w Preferences->Plugins, wpisujemy Json2Pojo
, instalujemy i restartujemy IDE.
{: .align-center}
Stworzymy sobie osobną paczkę do POJO. W widoku struktury projektu z lewej strony programu klikamy na główną paczkę naszego projektu prawym przyciskiem myszy, następnie New->Package. Nazywamy ją “pojos
”. Klikamy prawym na pojos, New->Create POJOs from JSON. W Root Class Name wpisujemy nazwę naszego POJO: IssLocationJSON
, a w okienku przeklejamy JSON z OpenNotify. Klikamy OK.
Plugin powinien wygenerować dwie klasy: IssLocationJSON
oraz IssPosition
. Dla porównania, kod dla IssLocationJSON:
|
|
Oraz dla IssPosition:
|
|
W głównym folderze z kodem źródłowym, czyli obok paczki pojos, tworzymy paczkę api. W niej tworzymy interfejs dla Retrofit o nazwie OpenNotifyService
. Ma on tylko jedną metodę abstrakcyjną: getIssLocation()
. Anotujemy ją Retrofitowym @GET
, podając ścieżkę do interesującego nas node’u API (czyli co jest po slashu głównego adresu):
|
|
Mamy już interfejs, POJO i adres.
Później zapiszemy dane zwrotne z lokalizacją w LiveData. LiveData nie ma jednak wbudowanego sposobu na radzenie sobie z żądaniami internetowymi. A co jeśli użytkownik nie będzie mieć połączenia internetowego albo serwer OpenNotify przestanie działać? Zwrócimy null? Dokumentacja sugeruje takie rozwiązanie, ale na nasze minimalne potrzeby wystarczy dużo prostsze. Zrobimy wrapper dla naszych IssLocationJSON, posiadający pole informujące nas o stanie połączenia. Instancje tego wrappera zapiszemy dopiero jako LiveData. Wówczas będziemy mieli dostęp do informacji, czy posiadamy dane o lokalizacji ISS, czy też ich nie mamy, bez problemów z nullami.
W paczce pojos tworzymy klasę IssLocationWrapper
:
|
|
Stworzymy też abstrakcyjną klasę Status
ze statycznymi kodami dla statusu naszego wrappera. Tworzymy paczkę util
i w niej klasę Status
(pewnie bardziej elegancko byłoby użyć enumów, ale dla naszych potrzeb wystarczą zwykłe integery):
|
|
Repository
Możemy już stworzyć instancję klasy Retrofit
, która będzie zczytywać dane o lokalizacji ISS z serwera. Mając w pamięci diagram przepływu danych z komponentami architektury Androida, musimy stworzyć Repository
.
Tworzymy paczkę repository
, a w niej klasę IssLocationRepository
:
|
|
Od razu korzystamy z LiveData (w tym wypadku MutableLiveData
, które różni się tylko tym, że upublicznia nam metody do zapisu danych w LiveData), bo z metody getLocation()
będzie korzystać ViewModel.
W konstruktorze konfigurujemy Retrofit
: podajemy bazowe URL API, informujemy, z jakiego konwertera ma korzystać (GSON w tym wypadku, z racji na stosowany w OpenNotify JSON), no i jakie żądania ma obsługiwać (nasz OpenNotifyService).
Metoda setLocation()
wykonuje żądanie GET asynchronicznie, stąd .enqueue
i CallBack
. Jeśli nie ma połączenia z internetem wywołana zostanie przeciążona metoda onFailure()
. Metoda onResponse()
natomiast zostanie wywołana, jeśli otrzymamy odpowiedź z serwera. Jeśli wartość response.isSuccesful()
jest false, to najwyraźniej dostaliśmy odpowiedź 404, 500 albo inną odpowiedź błędu, ale nie dostaliśmy w odpowiedzi danych o lokalizacji ISS, na których nam zależało. Może też się zdarzyć, że nasze żądanie zostanie przesunięte pod inny adres, ale nie otrzymamy odpowiedzi o błędnym adresie, bo adres działa (nie ma pod nim żadnych istotnych dla nas danych).
We wszystkich tych przypadkach, a także kiedy po prostu otrzymujemy interesujące nas dane (hurra), stosownie konfigurujemy obiekt MutableLiveData<IssLocationWrapper>
.
Nic więcej w naszym Repository nie musimy dodawać.
ViewModel
Konstrukcja i struktura ViewModel w naszym przypadku jest bardzo prosta: tworzymy nową klasę, rozszerzającą klasę ViewModel o nazwie LocationViewModel
:
|
|
Nasz LocationViewModel jest w stanie zwrócić informację o lokalizacji ISS (getLocation()
) oraz wysłać żądanie do repozytorium o ponowne ustalenie lokalizacji.
UI, MainActivity
Jesteśmy gotowi by przekazać dane do warstwy UI. Przypomnijmy, że na layout MainActivity
składają się TextView, w którym wyświetlać będziemy lokalizację ISS oraz Button do odświeżania lokalizacji.
MainActivity:
|
|
LocationViewModel dostarcza nam metoda get()
ViewModelProvidera
(przypisanego do Activity), nie tworzymy ViewModeli ze ‘‘zwykłego’’ konstruktora.
Następnie zaczynamy obserwować (observe()
) dane zwracane z ViewModel w metodzie getLocation()
. Jeśli pamiętamy sprzed chwili, zwraca ona nam MutableLiveData<IssLocationWrapper>
, a więc nasz obiekt z informacjami o lokalizacji ISS oraz stanem połączenia.
Jeśli dane te ulegną zmianie wywołana zostanie metoda onChanged()
obserwatora. Sprawdzany jest status połączenia i stosownie uzupełniany TextView.
Pod koniec metody onCreate()
mamy jeszcze zdefiniowany listener dla przycisku, obsługujący naciskanie przycisku przez użytkownika. Zauważmy, że nie wywołuje on już metody pobierającej dane z ViewModelu, a jedynie metodę aktualizacją lokalizację w samym LiveData w ViewModelu. Jako, że stan tego obiektu obserwujemy, to po zmianie jego zawartości automatycznie ujrzymy zmianę na poziomie UI w TextView.
Finisz
I to tyle, po odpaleniu aplikacji, zakładając dostęp do internetu, ujrzymy obrazek z początku artykułu:
{: .align-center}
Jeśli zaś włączymy w emulatorze/telefonie tryb samolotowy, to ujrzymy:
{: .align-center}
Podsumowując, zbudowaliśmy prostą aplikację, korzystającą z bibliotek Architecture Components oraz Retrofit. Warstwa UI jest odseparowana od reszty poprzez ViewModel. Wyświetlana informacja o lokalizacji ISS pochodzi z obserwowanego z poziomu UI obiektu LiveData przekazywanego przez ViewModel. ViewModel pozyskuje potrzebne informacje z Repository. W Repository wykonujemy połączenie z API przez Retrofit. UI nie wie, jak przetwarzane i skąd źródłowo pochodzą dane do wyświetlenia. ViewModel również na dobrą sprawę nie wie. Repository i ViewModel pozbawione są odwołań do klas związanych z UI. Warstwa UI natomiast pozbawiona jest odwołań do klas bezpośrednio związanych z logiką manipulacji i pozyskiwania danych.