Pierwsza aplikacja na iOS, czyli z czym można się spotkać, co jest trudne i c...
MvvmCross na przykładach w Xamarin.Android
1.
2. MVVMCROSS / MVVM 101
Na przykładach w Xamarin.Android
PRZEMYSŁAW RACIBORSKI
3. Wyjaśnijmy skrót - Model-View-ViewModel.
Intuicyjny, prawda?
Wzorzec MVVM polega na podzieleniu kodu aplikacji na trzy oddzielne warstwy:
DLACZEGO MVVM TO MVVM?
M/MODEL V/VIEW VM/VIEWMODEL
Klasy opisujące:
• nasz model danych
• dostęp do tych danych
• logikę biznesową, która
przetwarza dane
Widoki, inaczej nazywane
interfejsem użytkownika,
prezentujące już przetworzone dane
Warstwa pośrednia pomiędzy
Widokami (V) a Modelem (M).
4. MvvmCross zawiera wsparcie dla takich platform, jak:
• WPF
• UWP
• Xamarin.iOS
• Xamarin.Android
• Xamarin.Mac
• Xamarin.tvOS
• Aplikacje konsolowe
CZYM JEST MVVMCROSS?
Framework, który ułatwia zastosowanie wzorca MVVM w praktyce.
5. Główne funkcjonalności i założenia MvvmCross to:
• Dependency Injection (IoC) ad-hoc support
• Konfigurowalny system nawigacji pomiędzy widokami
• Wspomniany wcześniej system Bindingów View/ViewModel
• System pluginów – wsparcie dla funkcjonalności specyficznych dla platform jak chociażby obsługa
Lokalizacji, obsługa systemu plików, wykonywania połączeń telefonicznych czy wyboru zdjęć zrealizowana
za pomocą małych nugetowych paczek-pluginów
• Opakowanie oraz ułatwiony dostęp do natywnego API platform, np. MvvmCross Android Support Library
Zazwyczaj, aplikacja napisana przy użyciu MvvmCross składa się z jednego projektu PCL (Portable Class
Library – kod wspólny pomiędzy platformami) oraz n-projektów platform-specific.
6. • Zapoznanie się z dokumentacją i tutorialami przedstawionymi na MvvmCross.com
• Utworzenie projektu za pomocą MvvmCross Template (Xamarin Studio/Visual Studio add-in):
- ten wygeneruje nam podstawowy szkielet aplikacji wraz z podstawową konfiguracją projektu
Kod źródłowy aplikacji przedstawionej na prezentacji jest dostępny na githubie :
MVVMCROSS W PRAKTYCE
https://github.com/thefex/Xamarin-Cracow-Meetup.
7. Po stronie projektu PCL (Portable - kod współdzielony między platformami) potrzebujemy:
• LoginViewModel zawierający:
o Informację o nazwie użytkownika oraz wpisanym haśle (dane przekazane z widoku)
o Command, który powinien wywołać logikę logowania:
• czy wpisano poprawne dane - jeśli tak, przejdź na główny ekran, w przeciwnym wypadku wyświetl dialog
o Informację o tym czy wpisane dane są poprawne (czy możemy wywołać akcję logowania
• w tym przypadku czy przycisk „Sign in” jest klikalny)
• IAuthenticationService, tj. serwis realizujący proces logowania, zawierający:
o Logikę sprawdzającą czy dane logowania są poprawne
o Logikę zapisującą w trwałym kontenerze danych (nie ulegającym „zniszczeniu” po zamknięciu aplikacji) informację o tym, czy
użytkownik jest zalogowany. Skorzystamy z tego, aby przy następnym odpaleniu od razu przenieść użytkownika do głównego ekranu.
• IAuthenticationService zostaje wstrzyknięty do LoginViewModel. Nie tworzymy serwisów ręcznie.
MVVMCROSS W PRAKTYCE - EKRAN LOGOWANIA
8. Do implementacji ekranu logowania po stronie platformy (Android – widoki) musimy dodać LoginActivity.
Każda „strona” w Androidzie jest reprezentowana za pomocą Activity. Widoki w Androidzie są napisane przy pomocy plików .xml.
Przyjrzyjmy się kolejnym krokom:
1. Dodanie dwóch pól EditText – czyli Androidowych TextBoxów.
2. EditTextyy zawierające nazwę użytkownika i hasło powinny zostać podpięte pod pola LoginViewModel: Username i
Password. Zmiana wpisanego tekstu po stronie widoku powinna automatycznie zmieniać zawartość pól ViewModelu (i vice
versa).
3. Dodanie przycisku „Sign in” – kliknięcie na przycisk powinno wywoływać command LoginViewModel: SignIn. Brak wpisanej
nazwy użytkownika lub hasła powinien „zablokować” przycisk.
4. Do podpięcia kontrolek konieczne będzie skorzystanie ze składni MvvmCross „local:MvxBind
NazwaPropertyPoStronieKontrolki NazwaPropertyPoStronieViewModelu” – mechanizm bindingów.
MVVMCROSS W PRAKTYCE - EKRAN LOGOWANIA
9. W jaki sposób możemy zaimplementować wyświetlenie użytkownikowi dialogu w przypadku podania złych danych logowania?
Każda z platform zawiera różne API do wyświetlania dialogów – do żadnego z nich nie mamy dostępu w naszym „Core”
projektu – PCL (Portable Class Library).
Owszem, moglibyśmy w naszym ViewModelu wystawić pole w stylu „IsErrorDialogVisible”, a po stronie widoku moglibyśmy
reagować na zmianę tej właściwości poprzez wyświetlenie dialogu.
Jest to jednak rozwiązanie kiepskie – ogranicza nas do jednego stałego rodzaju dialogu, dodawanie i konfiguracja dialogów jest
na dłuższą metę problematyczna. Co więcej – nie możemy w łatwy sposób wykorzystać raz napisanego kodu wyświetlającego
dialog.
Rozwiązaniem jest skorzystanie z „Messengera” (w MvvmCross jest to plugin MvvmCross.Plugins.Messenger)
implementującego wzorzec EA – Event Aggregation.
MVVMCROSS W PRAKTYCE - EKRAN LOGOWANIA
10. Polega to na tym, że:
• po stronie PCL tworzymy klasy wiadomości zawierające dane opisującą wiadomość – np. DialogMessage zawierająca pola
jak Title czy Content,
• tworzymy Obserwatory obsługujące wiadomość – np. DroidDialogMessageObserver zawarty w projekcie Androida – w
reakcji na wiadomość o typie DialogMessage wyświetl Androidowy Dialog opisany przez DialogMessage,
• rejestrujemy obserwatory (np. po stronie Androidowych widoków) – rejestracja polega na tym, obserwatorzy zaczynają
nasłuchiwać/czekać na wiadomość,
• publikujemy wiadomość za pomocą Messengera (np. DialogMessage) – w chwili publikacji wszyscy aktualnie
zarejestrowani obserwatorzy obsłużą akcje powiązaną z wiadomością. Opublikować wiadomość jesteśmy w stanie w
projekcie Portable – nie zawiera to bowiem żadnego z API powiązanego z platformą.
W przykładowym projekcie dołączonym do prezentacji w ten sposób zaimplementowane jest wyświetlanie Toastów
(SnackBara), Dialogów oraz blokowanie interfejsu użytkownika przy operacji asynchronicznej.
11. Kolejnym z widoków przedstawionym w aplikacji jest widok prostej listy zawierającej dane jak:
• Imię
• Nazwisko
• Avatar
W Xamarinie najłatwiejszym sposobem na zaimplementowanie widoków z listami jest „podpięcie się” pod kolekcję
implementująca interfejs INotifyCollectionChanged (np. ObservableCollection<T>).
Przy zmianie pozycji/usunięciu/dodaniu elementu listy propagowany jest event zadeklarowany w interfejsie
INotifyCollectionChanged (CollectionChanged).
Przy każdej zmianie kolekcji event ten zawiera informację o tym w jaki sposób zmieniła się lista.
Niestety – poprawna implementacja list przy użyciu INotifyCollectionChanged jest problematyczna.
RX+DYNAMICDATA, A OBSERVABLECOLLECTION.
WPROWADZENIE DO LIST DANYCH
12. Wszystkie algorytmy (jak na przykład sortowanie) musielibyśmy napisać samodzielnie - tak aby w poprawny sposób został
propagowany event CollectionChanged.
Dodatkowo – wszystkie operacje na ObservableCollection są wywoływane na głównym wątku interfejsu – gdy spróbujemy to
zrobić na innym wątku aplikacja rzuci nam wyjątkiem.
Z pomocą przychodzi nam RX (Reactive Extensions) oraz biblioteka DynamicData, która jest w stanie wykonać za nas całą
pracę – tj. między innymi posortować, pogrupować dane na innych wątkach.
Temat RX/DynamicData moglibyśmy omawiać tygodniami – toteż jest to tylko krótkie wprowadzenie do tematu.
DynamicData jest biblioteką, która jest w stanie na podstawie: źródła danych i serii zadeklarowanych transformacji (w stylu
posortuj, pogrupuj, „przetransformuj” model) utworzyć ReadOnlyObservableCollection<>, która będzie „propagowała”
eventy CollectionChanged przy każdej z zadeklarowanych transformacji.
Dzięki prawidłowej propagacji eventu CollectionChanged nasze listy mogą reagować na każdą ze zmian animacją.
13. RX+DYNAMICDATA, A OBSERVABLECOLLECTION.
WPROWADZENIE DO LIST DANYCH
Aktualnie jedyną „słuszną” kontrolką do list w Androidzie jest RecyclerView. Listy wykonane przy użyciu RecyclerView
tworzą tylko n widocznych widoków, które są następnie „reużywane” (tzw. View recycling – stąd nazwa RecyclerView).
Dzięki takiemu rozwiązaniu aplikacje oparte na RecyclerView są wydajniejsze, zajmują mniej pamięci.
W świecie „Java-Android” czy Xamarina bez MvvmCross aby wyświetlić listę musimy zaimplementować
tzw. RecyclerAdapter – adapter, który wie skąd pobrać dane, ile ich jest, informujący RecylerView kiedy i gdzie
zmienił się jakiś element na liście.
MvvmCross załatwia to za programistę
wystarczy dodać paczkę MvvmCross.Support.V7.RecyclerView.
Na kolejnych slajdach zostaną przedstawione funkcjonalności RecyclerView opakowane przez MvvmCross.
14. Utworzenie prostej listy z 1 typem elementów wymaga dodania bindingów do źródła danych (ItemsSource)
oraz zdefiniowania atrybutu MvxItemTemplate, który powinien wskazywać na „layout” opisujący element listy.
NAJPROSTSZA LISTA PRZY UŻYCIU MVXRECYCLERVIEW
15. LISTA ZE WSPARCIEM RÓŻNYCH LAYOUTÓW - MVXITEMTEMPLATESELECTOR
Bazując na funkcjonalności znanej z Windows Phone/WPF, swego czasu do MvvmCross dorzuciłem funkcjonalność zwaną
„MvxItemTemplateSelector”. Dzięki temu dla jednej listy jesteśmy w stanie łatwo zdefiniować różne layouty dla jej elementów.
Aby skorzystać z tej funkcjonalności do atrybutu
„MvxTemplateSelector” należy wrzucić string, którego
wartość to: „Namespace.TemplateSelectorClassName,
AssemblyName”
Należy zaimplementować MvxTemplateSelector.
Zaimplementowane metody służą do wyboru layoutu
w zależności od danego elementy listy.
16. GRUPOWANIE A MVXRECYCLERVIEW
Niedawno do funkcjonalności MvxRecyclerView dodałem możliwość grupowania listy. Funkcjonalność jest jak do tej pory dostępna
jedynie w paczkach „unstable”. Aby dodać grupowanie do poprzedniej listy:
Należy dodać atrybut MvxGroupedDataConverter
analogicznie do MvxTemplateSelector. Należy określić
layout „headera” grupy za pomocą atrybutu
MvxGroupSectionLayoutId.
Należy zaimplementować interfejs
IMvxGroupedDataConverter, który na podstawie
elementu listy powinien utworzyć obiekt
MvxGroupedData.
17. HEADER/FOOTER - MVXRECYCLERVIEW
Kolejną z funkcjonalności list, którą dodałem do MvvmCross, jest obsługa Header/Footer list.
Aby dodać header/footer do poprzedniego przykładu:
Header/Footer definiujemy za
pomocą przekazania layoutu
do atrybutów:
• MvxHeaderLayoutId,
• MvxFooterLayoutId.
Dodatkowo możemy wykorzystać:
• MvxHidesHeaderIfEmpty,
• MvxHidesFooterIfEmpty.
To atrybuty, które ukrywają
header/footer, gdy lista jest pusta.