Jakiś czas temu opbulikowałem wpis, w którym pokazałem Wam jak można zaimplementować wzorzec Model View Controller (MVC) w swoim projekcie. Dzisiaj przyszedł czas na pewną wariację tego rozwiązania czyli pattern o nazwie Model View ViewModel. O MVVM w świece aplikacji mobilnych mówi się już od jakiegoś czasu i to nie tylko w odniesieniu do platformy iOS.

Całkiem niedawno Google za sprawę Architecture Components mianował MVVM podstawowym wzorcem w architekturze aplikacji Androidowych (trochę im zeszło z tym wyborem 😝). Tak naprawdę jednak MVVM nie jest czymś zupełnie nowym, bo sam wzorzec funkcjonuje już od jakiegoś czasu choćby na platformie .NET. Tak się składa, że to właśnie inżynierowie Microsoftu są jego twórcami.

MVVM według wielu jest remedium na przeciążone kontrolery w architekturze opartej o MVC. Moim skromnym (juniorskim 😇) zdaniem, upychanie całości logiki w jednym pliku nie jest winą samego wzorca, a raczej niewłaściwej interpretacji ze strony programistów. Odkładając jednak dywagacje na bok, trzeba przyznać, że MVVM stanowi bardzo ciekawą alternatywę i warto się nad nim pochylić. Nie stanowi on jednak rozwiązania wszystkich problemów i podobnie jak MVC może zostać użyty w niewłaściwy sposób. Czyli jak zawsze, należy zachować zdrowy rozsądek.

 

W wpisie tym pokaże Wam jak krok po kroku zaimplementować wzorzec MVVM korzystając z protokołów oraz RxSwift. Jeżeli biblioteka ta jest Wam zupełnie obca to zapraszam tutaj i tutaj. Pierwszy wpis wprowadza w teorię, natomiast drugi skupia się na praktyce.

Jak zawsze, najlepiej będzie pokazać wszystko na przykładzie, dlatego wszystkie przedstawione tutaj koncepcje będziemy od razu implementować w aplikacji. Kod do projektu startowego możecie pobrać poniżej:

 

CoinsCap MVVM Starter

 

Jedynym zadaniem aplikacji będzie pobieranie danych o aktualnych notowaniach kryptowalut oraz wyświetlanie ich na ekranie za pomocą UITableView:

 

 

Dane będziemy pobierać z API udostępnianego przez Coin Market Cap. Nie musicie jednak zakładać tam konta, bo dane są dostępne publicznie. Wszystkie wymagane biblioteki zostały już dodane, tak aby skupić się na rzeczach najważniejszych. Zanim jednak przejdziemy do kodowania musimy przyjrzeć się MVVM od strony czystko teoretycznej.

mvvm w teorii

 

MVVM składa się z trzech głównych komponentów:

  1. Model – czyli standardowy obiekt określający strukturę danych w aplikacji.
  2. View – obiekty stanowiące zwykle elementy interfejsu użytkownika. W przypadku iOS do tej kategorii zaliczać się będzie również UIViewCotroller, który łączy wszystkie elementy UI w logiczną całość, a także odpowiada za komunikację widoków z obiektem dostarczającym dane. O nim już za chwilę.
  3. ViewModel – obiekt ten dostarcza dane dla poszczególnych widoków, a także zawiera całą logikę potrzebną do pracy z danymi. W ten sposób obiekt UIViewCotroller zostanie w znacznym stopniu odchudzony, bo będzie skupił się jedynie na poprawnym zarządzaniu elementami UI. Cała logika natomiast (w wersji idealnej) zostanie umieszczona w ViewModel. To właśnie w tym obiekcie powinien zostać umieszczony kod do pobierania danych z serwera lub zapisywania ich do bazy danych. Operacje te nie powinny być jednak wykonywane bezpośrednio przez MVVM, a raczej przez wyspecjalizowane obiekty. Podczas tworzenia przykładowej aplikacji dowiecie się jak powinna wyglądać implementacja takich obiektów.

Tak to będzie się prezentowało na diagramie. Zwróćcie uwagę na relacje pomiędzy poszczególnymi obiektami:

 

Źródło

 

W kilku artykułach, które prześledziłem szukając informacji na temat MVVM natknąłem się na stwierdzenie, że każdy widok znajdujący się na ekranie powinien być wspierany przez obiekt ViewModel. Moim zdaniem jest to zbyt duże uogólnienie. Rozumiem sytuacje, w której na ekranie znajdują się dwa widoki UITableView i każdy z nich potrzebuje zupełnie innego zestawu danych. Ww przypadku zwykłych obiektów, takich jak na przykład UILabel, nie widzę potrzeby tworzenia osobnego obiektu ViewModel zawierającego jedynie kod do formatowania tekstu. Możecie spokojnie założyć, że w przypadku interfejsów składających się z pojedynczego widoku UITableView / UICollectionView + kilka widoków pomocniczych jeden dedykowany obiekt ViewModel w zupełności wystarczy.

ViewModel powinien komunikować się z obiektem UIViewController w sposób „reaktywny”, czyli automatycznie wysłać zmiany zachodzące w modelu danych. Ja zdecydowałem się na wykorzystanie biblioteki RxSwift, a dokładną implementację będziecie mogli zobaczyć już niebawem.

Nie oznacza to jednak, że RxSwift jest jedynym możliwym rozwiązaniem. Możecie skorzystać na przykład z Key-Value-Observing, choć tutaj zostaniecie zmuszeni do korzystania z ograniczonego zakresu typów danych (więcej o KVO możecie przeczytać w tym wpisie). KVO sprawia również spore problemy przy współpracy z interfejsami.

Możecie skorzystać także z ciekawego rozwiązania, na które natknąłem się szperając w sieci. Nie będę go tutaj opisywał, ale możecie je sobie sprawdzić pod tym adresem.

Dodawanie do projektu tak dużej biblioteki jak RxSwift tylko w celu wykonywania prostej operacji wymiany danych może wydawać się niepotrzebnym obciążeniem. Mogę się z tym zgodzić, jednak wszystko zależy od oceny potrzeb projektu. Jeżeli planujecie wykorzystać RxSwift także do innych zadań to wybór jest oczywisty. Jeżeli nie, to możecie skorzystać z jednej z możliwości, które wymieniłem powyżej.

 

W tym miejscu warto zaznaczyć jedną rzecz – sposób implementacji wzorca MVVM będzie zależał od indywidualnych potrzeb oraz preferencji. Nie ma „jedynej słusznej” drogi.

 

Tworzymy obiekt mvvm

 

Wystarczy już samej teorii. Teraz sprawdzimy jak to wszystko poskładać w sensowną całość.

Otwórzcie projekt, który pobraliście na samym początku i porozglądajcie się trochę, żeby sprawdzić jak poszczególne elementy komunikują się ze sobą. Pewne funkcjonalności dodałem już za Was, bo sposób ich implementacji nie jest istotny z punktu widzenia tego poradnika (jak na przykład UITableView).

Zaczniemy od utworzenia abstrakcji, na której oprzemy obiekt ViewModel. W folderze o nazwie ViewModel utwórzcie plik ViewModelProtocol i dodajcie do niego poniższy kod:

Importowanie biblioteki RxSwift jest tutaj koniecznie ponieważ będziemy w deklaracji korzystali z obiektu Variable, który wchodzi w jej skład. Obiekt coinsCollection będzie nam służył do dwóch rzeczy – przechowywania kolekcji obiektów Coin oraz komunikacji z obiektem UIViewController poprzez emitowanie nowych danych za każdym razem, gdy te zostaną pobrane z serwera. Coin będzie w naszej aplikacji pełnił rolę Modelu.

Obiekt webService wykorzystamy do pobierania danych z serwera. On również został oparty na abstrakcji (protokół WebServiceProtocol), ale ta została już zaimplementowana w projekcie. Poleganie na abstrakcjach podczas implementacji poszczególnych funkcjonalności pomoże Wam w przyszłości na łatwiejsze pisanie testów jednostkowych.

Funkcja getCurrentCoinsCap() skorzysta z webService, aby połączyć się z serwerem. Do protokołu dodaliśmy również wymóg posiadania przez obiekt ViewModel konstruktora, za pomocą którego będziemy „wstrzykiwali” obiekt WebService (zjawisko określane jako dependency injection).

Teraz zajmiemy się konkretną implementacją. Ponownie w folderze ViewModel utwórzcie plik o nazwie MainViewViewModel. Nazwiemy go tak ponieważ będzie on odpowiadał za komunikację z obiektem MainViewVC (ekran główny aplikacji). Dodajcie wewnątrz pliku poniższy kod:

Tutaj wszystko powinno być jasne. Utworzyliśmy obiekt, który spełnia wszystkie wymogi protokołu ViewModelProtocol. Na tym etapie implementacja funkcji getCurrentCoinsCap() jest jeszcze pusta, ale za chwilę to zmienimy.

Dodajcie teraz wewnatrz funkcji następujący kod:

Korzystając z obiektu webService będziemy pobierali dane o najnowszych notowaniach kryptowalut. Sposób implementacji funkcji getCoinsData() nie jest istotny z punktu wiedzenia naszego przykładu, ale możecie sobie do niej zajrzeć jeżeli jesteście bardzo ciekawi.

Po pobraniu danych z serwera uruchamiany jest blok kodu, który przekazuje dwa opcjonalne parametry. Pierwszym z nich jest kolekcja obiektów typu Coin (nasz model danych), drugim natomiast jest ewentualna informacja o błędzie. Wewnątrz bloku korzystamy z jednej z możliwości jakie daje nam pattern matching w Swift (więcej na ten temat możecie poczytać w tym miejscu). Za pomocą instrukcji switch sprawdzamy jakie wartości zostały przekazane do bloku.

Jeżeli otrzymaliśmy kolekcję obiektów Coin, oznacza to, że operacja zakończyła się sukcesem i możemy przypisać nową wartość do zmiennej coinsCollection (więcej o niej za chwilę). W przypadku wystąpienia problemów z pobieraniem otrzymujemy obiekt String zawierając informację o błędzie.

Najważniejsza operacja związana jest właśnie z przypisaniem nowej wartości dla zmiennej coinsColletion. Z racji tego, że jej typem jest Variable (pochodzący z bilioteki RxSwift), w momencie ustawienia nowej wartości dla pola value, obiekt coinsColletion wyemituje nowe zdarzenie onNext(), w którym zostanie przekazane cała kolekcja obiektów Coin.

W tej chwili oczywiście nic się nie wydarzy, bo żaden obiekt nie nasłuchuje zmian zachodzących w obiekcie coinsCollection, ale już za chwilę zmienimy ten stan rzeczy dodając subskrypcję w obiekcie MainViewVC.

 

Współpraca z kontrolerem

 

Otwórzcie teraz obiekt MainViewVC. Spora część funkcjonalności została już zaimplementowana. My skupimy się tylko na elementach istotnych z punktu widzenia implementacji wzorca MVVM.

Tuż pod deklaracją stałej disposeBag() dodajcie poniższy kod:

Pole viewModel będzie oczywiście przechowywało obiekt odpowiedzialny za dostarczanie danych dla naszego kontrolera.

Będziemy korzystali z dwóch konstruktorów. Pierwszy będziemy mogli wykorzystać podczas pisania testów jednostkowych dla obiektu MainViewVC. W czasie testów będziemy mogli z łatwością skorzystać z dowolnego obiektu ViewModel, który będzie spełniał wymogi protokołu ViewModelProtocol.

Drugi konstruktor jest tym domyślnym, który będzie uruchamiany przez system jeżeli korzystamy ze Storyboards. W ramach tego konstruktora przekazywana jest domyślna implementacja obiektu MainViewViewModel, do której „wstrzykiwany” jest obiekt WebService (także w postaci domyślnej implementacji).

Tutaj mała uwaga. Zdaję sobie sprawę, że taka konfiguracja konstruktorów nie jest najbardziej elegancka, ale w moim przypadku takie podejście po prostu się sprawdza. Podczas codziennej pracy poszczególne widoki w aplikacji tworzę z poziomu kodu, ale zostawiam sobie puste obiekty UIViewController w Storyboards, aby mieć możliwość szybkiego rozeznania się w nawigacji pomiędzy poszczególnymi ekranami. Koniec małej uwagi 😅.

Teraz jedna z najważniejszych rzeczy w całej aplikacji. Utworzymy subskrypcję dla obiektu coinsCollection znajdującego się w MainViewViewModel, w celu przechwytywania przychodzących zdarzeń. W momencie zakończenia pobierania danych z serwera obiekt MainViewViewModel „poinformuje” obiekt MainViewVC o wyniku operacji, tak aby ten mógł podjąć odpowiednie działania. Na tym właśnie polega komunikacja w MVVM.

Dodajcie teraz poniższy kod w funkcji viewDidLoad() zaraz pod wywołaniem funkcji setupUI():

W pierwszej kolejności przekształcamy obiekt coinsCollection na Observable w celu utworzenia subskrypcji. W tym przypadku interesuje nas wystąpienie samego zdarzenia onNext(), dlatego pomijamy przekazywany parametr (a dlaczego o tym za chwilę). Wewnątrz bloku onNext wywołujemy funkcję updateUIWithCoins(), która odpowiada za odświeżenie widoku UITableView. Zwróćcie uwagę na fakt, że w celu uniknięcia wycieków pamięci korzystamy ze słabej referencji do obiektu self (czyli MainViewVC).

Na samym końcu robimy porządek dodając subskrypcję do obiektu disposeBag, który będzie odpowidzialny za jej usunięcie we właściwym momencie.

Teraz musimy jeszcze upewnić się, że obiekt UITableView otrzymuje właściwe dane do wyświetlenia. Na samym dole pliku MainViewVC znajduje się rozszerzenie, które spełnia wymogi dwóch protokołów: UITableViewDataSource oraz UITableViewDelagate. W funkcji tableView(tableView: numberOfRowsInSection:) zamieńcie domyślną implementację na poniższy kod:

 

 

Ilość wyświetlonych komórek będzie uzależniona od ilość obiektów przechowywanych w kolekcji coinsCollection. To właśnie dlatego tworząc subskrypcję do tego obiektu pomijaliśmy przekazywane elementy – będziemy je pobierać bezpośrednio z obiektu MainViewViewModel. W ten sposób będziemy mogli uniknąć przechowywania modelu (naszych danych) w obiekcie MainViewVC, co jest jednym z głównych celów korzystania ze wzorca MVVM. Dzięki temu pozbędziemy się konieczności zarządzania „stanem” obiektu UViewController.

To samo podejście musimy zastosować w dwóch pozostałych funkcjach znajdujących się w rozszerzeniu. W funkcji tableView(tableView: cellForRowAt:) wstawcie poniższy kod:

Tutaj sprawa jest oczywista. Pobieramy z kolekcji odpowiedni obiekt Coin, a następnie wykorzystujemy go do konfiguracji komórki w tabeli.

Została nam jeszcze funkcja tableView(tableView: didSelectRowAt:):

Ponownie pobieramy obiekt z kolekcji, ale tym razem wykorzystamy go do wstępnej konfiguracji coinsDetailVC, który będzie odpowiadał za ekran wyświetlający informacje o wybranej kryptowalucie. To taki dodatkowy bajer.

Aplikacja jest już praktycznie gotowa. Musimy jeszcze tylko wstawić kod do pobierania danych z serwera. W pierwszej kolejności na samym końcu funkcji viewDidLoad() dodajcie poniższy fragment:

 

 

Następnie dodajcie ten sam kod wewnątrz funkcji refreshCoins():

 

 

Wszystko jest już gotowe. Możecie skompilować projekt i uruchomić go na emulatorze, aby sprawdzić czy wszystko działa jak należy. Jeżeli wszystko pójdzie zgodnie z planem to Waszym oczom powinien ukazać się widok podobny do poniższego obrazka:

 

 

Podsumowanie

 

Żeby ułatwić Wam odrobinę przyswojenie nowych informacji, poniżej umieściłem krótkie podsumowanie całego procesu:

  • Utworzyliśmy abstrakcję dla obiektu ViewModel, tak aby ułatwić testowanie obiektu MainViewVC.
  • Na podstawie abstrakcji stworzyliśmy właściwą implementację obiektu MainViewViewModel.
  • Wykorzystaliśmy obiekt coinsCollection do komunikacji z MainViewVC w sposób „reaktywny”. Oznacza to, że MainViewVC         będzie nasłuchiwał informacji o zakończeniu pobierania danych z serwera i na ich podstawie podejmował odpowiednie działania.
  • Kolekcję obiektów Coin przechowywaną przez obiekt coinsCollection wykorzystaliśmy do zasilenia obiektu UITableView w dane.

A tak przedstawia się podział na poszczególne warstwy:

  • Model – obiekt Coin
  • View – obiekty MainViewVC oraz pozostałe elementy UI
  • ViewModel – obiekt MainViewViewModel

Proces, który Wam przedstawiłem jest esencją wzorca MVVM. Można iść o krok dalej i zapewnić komunikację w drugą stronę – z MainViewVC do MainViewViewModel. Jest to przydatne na przykład w sytuacji, w której chcemy informować na bieżąco obiekt ViewModel o zmianach zachodzących w polu tekstowym UITextField. Implementację tego możecie potraktować jako zadanie domowe 🤓. Ja natomiast niebawem opublikuję wpis, w którym pokaże Wam jak zapewnić dwustronną komunikację z wykorzystaniem bibliotek RxSwift oraz RxCoccoa 🤩.

 

Słowo na drogę

 

W sieci możecie znaleźć bardzo dużo opinii na temat tego jak implementować MVVM w „jedyny właściwy sposób”. Znajdą się również takie osoby, który powiedzą, że MVVM wcale taki fajny nie jest. I jak zawsze prawda leży gdzieś pośrodku. Tak jak wspomniałem na samym początku, wzorzec Model View Cotroller sam w sobie zły nie jest – problemem jest natomiast jego niewłaściwa implementacja ( a nie będę ukrywał, że ja też mam z tym sporo problemów). Podobny los może spotkać także MVVM.

Jedno jest jednak pewne – warto znać MVVM choćby tylko po to żeby mieć jakieś porównanie. Ja przestawiłem się na niego całkowicie i jestem zadowolony. Ale w przyszłości może się sporo zmienić. No i nie należy zapominać, że w wielu ogłoszeniach o pracę MVVM jest umieszczany jako „must have” albo przynajmniej „nice to have” 👍. Do następnego 🧐.

Tutaj macie kod ukończonej aplikacji:

 

CoinsCap MVVM

 

 

Link do obrazka tytułowego


 

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *