Xamarin – Customers Manager – #3 – Core i Android

Poprzednie części serii

Część trzecia. Po pokazaniu podstaw w poprzedniej części, nadeszła chwila, aby zająć się naszą aplikacją. Nadamy odpowiednią strukturę projektom, zainstalujemy MvvmCross oraz kilka bibliotek. Rozwiniemy projekt Core oraz uruchomimy pierwszą wersję aplikacji na platformie Android.

Foldery i projekt

Zanim zaczniemy programowanko, przyda się nam odpowiednio uformowany projekt. Stwórz kilka folderów jak na zrzucie ekranu poniżej. Zróbmy porządek z klasami, których i tak nie skorzystamy. Usuń, także następujące pliki:
  • CustomerManager.Core
    • Class1.cs
  •  CustomerManager.iOS
    • ViewController.cs
  • CustomerManager.Droid
    • MainActivity.cs
    • Resources → layout → Main.axml
Projekt

Kolejnym krokiem jest dodanie referencji w projektach platformowych do projektu Core. Kiedy to zrobisz, zainstaluj paczkę NuGet MvvmCross, w każdym z trzech projektów. Skorzystaj z najnowszej dostępnej wersji. W moim przypadku jest to 6.2.1.

Instalacja MvvmCross

Czym jest MvvmCross i jaki jest cel jego instalacji?

MvvmCross to kombajn do budowy aplikacji Xamarin. Wspomaga nie tylko użycie wzorca MVVM, ale dostarcza kompletu gotowych mechanizmów do tworzenia aplikacji mobilnej. Nie musisz przejmować się nawigacją, jej domyślna implementacja w MvvmCross pozwala na sterowanie przepływem widoków z poziomu projektu Core. Oczywiście mamy możliwość wpływu na domyślną implementację nawigacji, poprzez użycie  tzw. view presenterów. W nich (już platformowo) dodajemy własne zachowanie. O View Presenterach możesz myśleć jak o moście łączącym widok z modelem widoku. Zrozumiesz to lepiej kiedy po raz pierwszy skorzystamy z MvxNavigationService, czyli serwisu, który zgodnie z nazwą służy do nawigacji pomiędzy widokami.

Czas na kod

Zanim zaczniemy tworzyć jakikolwiek widok czy funkcjonalność, musimy skonfigurować MvvmCross. Dodamy klasę App w projekcie Core. Rozszerza ona klasę MvxApplication, która odpowiada za inicjalizację frameworka m.in. kontenera IoC czy ustalenie punktu startowego aplikacji. To właśnie tutaj będziemy definiować nie zależne od platformy zależności dla kontenera IoC – tak, MvvmCross dostarcza swój, zupełnie wystarczający dla większości aplikacji kontener.  

Dodaj, w głównej gałęzi projektu Core klasę App i umieść w niej następujący kod:

public class App : MvxApplication
{
    public override void Initialize()
    {
        CreatableTypes()
            .EndingWith("Service")
            .AsInterfaces()
            .RegisterAsLazySingleton();

        RegisterAppStart<CustomersListViewModel>();
    }
}

Z pewnością w naszej aplikacji pojawią się serwisy. W zasadzie tylko jeden, ale typowej aplikacji może być ich wiele. Rejestracja ich w kontenerze IoC za każdym razem może być nużąca, dlatego pokaże Ci ciekawe rozwiązanie.

W metodzie Initialize rejestrujemy implementacje wszystkich serwisów za pomocą jednego wywołania. Można opisać to w taki sposób:



Znajdź wszystkie klasy w projekcie, wyfiltruj te, które kończą się na “Service”, weź interfejs, który implementują i go zarejestruj.


Jeżeli będziemy trzymać się odpowiedniej nazewnictwa podczas tworzenia serwisów tj. każdy z nich będzie kończył się postfiksem “Service”, wtedy dla każdej takiej klasy, kontener (korzystając z refleksji) zarejestruje odpowiednią implementację, wykorzystując przy tym interfejs, który jest implementowany przez konkretny serwis (uff, zawiłe). Dzięki temu mechanizmowi, nie musimy rejestrować kolejnych implementacji za każdym razem, kiedy dodamy nowy serwis. Czysta oszczędność czasu oraz ilości kodu. Pamiętaj o tym, że aby instancja została poprawnie zarejestrowana, klasa musi spełnić następujące warunki:

  • musi mieć publiczny konstruktor,
  • nie może być abstrakcyjna,
  • musi posiadać odpowiedni postfiks,
  • implementować interfejs, z którego będziemy poźniej korzystać (wtedy ma sens cała operacja)

Na końcu korzystamy z RegisterAppStart. Jako parametr podany został ViewModel, którego zadaniem będzie obsługa widoku z listą klientów. Chcemy, żeby właśnie widok listy klientów był tym widokiem startowym naszej aplikacji. A to właśnie realizuje RegisterAppStart.

Za każdym razem kiedy będziesz chciał odwołać się do jakiegoś widoku, będziesz to robił poprzez użycie View Modelu. Po połączeniu widoku i modelu widoku, MvvmCross będzie wiedział co konkretnie ma wyświetlić. Już wkrótce dowiesz się jak to zrobić.

Obiekty Customer

Zajmijmy się klasami DTO (Data Transfer Object) – w zasadzie tylko jedną. Z pewnością będzie potrzebna nam taka do przedstawienia encji Klient.  Umieść ją w folderze DTO. Jednocześnie stworzymy klasę CustomerListItemPO reprezentującą podstawowe informacje o kliencie na liście. Będziemy z niej korzystać w modelu widoku. Dzięki takiemu podziałowi, obiekt PO będzie posiadał tylko te właściwości, które są nam potrzebne przy wyświetlaniu danych o kliencie. Zwróć choćby uwagę na właściwość FullName. W kontekście DTO nie ma ona sensu, w tym obiekcie lepiej jest trzymać imię oraz nazwisko oddzielnie. Natomiast w PO, FullName ułatwia zadanie, ponieważ dostajemy już odpowiednio sformatowany model, z którego wkrótce skorzystamy. Umieść CustomerListItemPO w folderze PO.

public class CustomerDTO
{
    public string Id { get; set; }

    public string Name { get; set; }

    public string Surname { get; set; }

    public string Number { get; set; }

    public string StreetName { get; set; }

    public string City { get; set; }

    public string PostalCode { get; set; }

    public string FlatNumber { get; set; }

    public string AvatarUrl { get; set; }
}

public class CustomerListItemPO
{
    public string Id { get; set; }

    public string Name { get; set; }

    public string Surname { get; set; }

    public string Number { get; set; }

    public string AvatarUrl { get; set; }

    public string FullName
    {
        get { return $"{Name} {Surname}"; }
    }
}

Przyszedł czas na obiecany view model. Stwórz CustomersListViewModel w projekcie Core i umieść go we wcześniej przygotowanym folderze ViewModels. Musimy jeszcze dostosować go do zasad MvvmCross, więc dodaj dziedziczenie po klasie MvxViewModel. Teraz umieść jeszcze brakujący using w klasie App. 

public class CustomersListViewModel : MvxViewModel
{
    public CustomersListViewModel()
    {
        CustomerItemClickCommand = new MvxAsyncCommand<CustomerListItemPO>(CustomerItemClickAction);
        AddNewCustomerCommand = new MvxAsyncCommand(AddNewCustomerAction);
    }

    public override async void ViewAppeared()
    {
        base.ViewAppeared();
        await UpdateCustomersList();
    }

    private MvxObservableCollection<CustomerListItemPO> _customers;
    public MvxObservableCollection<CustomerListItemPO> Customers
    {
        get { return _customers; }
        set { SetProperty(ref _customers, value); }
    }

    public IMvxCommand CustomerItemClickCommand { get; private set; }
    private Task CustomerItemClickAction(CustomerListItemPO customer)
    {
        throw new NotImplementedException();
    }

    public IMvxCommand AddNewCustomerCommand { get; private set; }
    private Task AddNewCustomerAction()
    {
        throw new NotImplementedException();
    }

    private Task UpdateCustomersList()
    {
        throw new NotImplementedException();
    }
}

Już tłumaczę co z czym zjeść 🙂 Jak wcześniej napisałem, wymagane jest dziedziczenie po MvxViewModel. Nadpisałem metodę ViewAppeared. MvvmCross wywołuje ją za każdym razem kiedy widok zostanie pokazany użytkownikowi np. kiedy zamknie widok szczegółów klienta i wróci do listy klientów. Wykorzystamy ją do odświeżenia listy klientów. Dalej mamy listę klientów Customers, korzystamy z MvxObservableCollection. Lista ta korzysta z INotifyCollectionChanged do powiadamiania o zmianach w liście np. po dodaniu obiektu czy jego usunięciu. Poźniej zobaczysz to na przykładzie.

Dalej mamy właściwość CustomerItemClickCommand typu IMvxCommand. Jest to standardowy sposób na obsługę kliknięć pochodzących z widoków. Cofnij się do ciała konstruktora i zobacz, że inicjalizujemy ten właśnie command, przekazując jako jeden z parametrów  konstruktora metodę CustomerItemClickAction. Po kliknięciu w konkretnego klienta na liście chcemy wiedzieć jaki klient to był. Tworząc metodę, która przyjmuje jako parametr obiekt klienta i tworząc generyczna wersję MvxCommand tego samego typu, będziemy mogli to zrobić. Korzystamy z wersji asynchronicznej MvxCommand, co pozwala nam na przekazanie metody, która zwraca zadanie (Task). Chcemy tego, ponieważ w ciele metody CustomerItemClickAction będziemy korzystać z asynchroniczności.

Chcemy, także dodawać nowego klienta dlatego kilka linijek niżej dodałem AddNewCustomerCommand. Ten command nie jest już generyczny, ponieważ nie potrzebujemy przesyłać żadnej dodatkowej informacji z widoku. Stwórzmy widoki, a następnie wrócimy do modelu widoku i tchniemy w niego życie.

Android

Potrzebujemy doinstalować dodatkowe paczki NuGet dla projektu Android:

  • Xamarin.Android.Support.Constraint.Layout
  • Xamarin.Support.Design
  • Xamarin.FFImageLoading
  • MvvmCross.Droid.Support.V7.AppCompat
  • MvvmCross.Droid.Support.V7.RecyclerView

Pierwsza z nich to zaawansowany rodzaj layoutu do budowy widoków. Druga paczka zawiera dodatkowe widgety. My skorzystamy z FAB (Floating Action Button), czyli przycisk, który najczęściej znajduje się w prawym dolnym rogu widoku i służy do wykonania jakiejś ważnej akcji w kontekście tego widoku. Podczas instalacji tych paczek środowisko doinstaluje pozostałe wymagane biblioteki. FFImageLoading jest świetną biblioteką do wyświetlania obrazów, umożliwia korzystanie z placeholderów np. podczas wczytywania obrazów, posiada mechanizm cache oraz możliwość podawania obrazu w postaci URL. 

W dalszej części będziemy korzystać z kilku zasobów graficznych. Są one do pobrania tutaj: Zasoby.zip

Dobrze, dodaj do projektu Droid nową klasę o nazwie Setup. MvvmCross domyślnie wykorzysta ją do poprawnej inicjalizacji aplikacji. Dla nas najważniejsza w tym momencie jest przesłonięta metoda CreateApp. To w niej zwracamy nową instancję klasy App, tej samej której definicje dodaliśmy do projektu Core. Jeżeli środowisko nie znajduje jej, sprawdź czy poprawnie dodałeś referencję do projektu Core w projekcie Droid.

public class Setup : MvxAppCompatSetup
{
    protected override IEnumerable AndroidViewAssemblies => new List(base.AndroidViewAssemblies)
        {
            typeof(CoordinatorLayout).Assembly,
            typeof(FloatingActionButton).Assembly,
            typeof(Toolbar).Assembly,
            typeof(MvxRecyclerView).Assembly,
        };

    protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
    {
        MvxAppCompatSetupHelper.FillTargetFactories(registry);
        base.FillTargetFactories(registry);
    }

    protected override IMvxAndroidViewPresenter CreateViewPresenter()
    {
        return new MvxAppCompatViewPresenter(AndroidViewAssemblies);
    }

    protected override IMvxApplication CreateApp()
    {
        return new App();
    }
}    

Zanim przejdziemy dalej dodamy zasoby z kolorami oraz stylami. Stwórz w folderze Resources → values dwa pliki xml

  • styles.xml
  • colors.xml

I umieść w nich następującą zawartość:

<?xml version="1.0" encoding="UTF-8" ?>
<resources>
    <style name="Theme.Splash" parent="android:Theme">
        <item name="android:windowNoTitle">true</item>
    </style>
   
    <style name="MyTheme" parent="MyTheme.Base">
    </style>
    
    <style name="MyTheme.Base" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="windowNoTitle">true</item>
        <item name="windowActionBar">false</item>
        <item name="colorPrimary">@color/primary</item>
        <item name="colorPrimaryDark">@color/primaryDark</item>
        <item name="colorAccent">@color/accent</item>
    </style>
    
    <style name="MyTheme.ActionBar" parent="style/Widget.AppCompat.Light.ActionBar.Solid">
        <item name="elevation">0dp</item>
    </style>
</resources>    
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="primary">#03A9F4</color>
    <color name="primaryDark">#0288D1</color>
    <color name="accent">#5F395A</color>
    <color name="text_color">#555555</color>
</resources>
Magia kolorów

Pobraną wcześniej paczkę zasobów wypakuj i umieść w folderze Resources projektu Android. Każdy z tych plików dodaj do odpowiednich folderów w drzewie projektu. Kiedy to zrobisz znajdź AndroidManifest.xml w folderze Properties. Jeżeli korzystasz z Visual Studio po kliknięciu dwukrotnie uruchomi Ci się edytor. W innym przypadku prawdopodobnie będziesz zmuszony do edycji surowego pliku xml.

Wprowadź zmiany jak na obrazie poniżej. Wprowadź Application icon oraz Application theme. Zauważ, że wybrany motyw to ten sam, który dodaliśmy wcześniej do styles.xml.

 

Android Manifest

Dodajmy jeszcze prosty splash screen. Definicja layoutu oraz code-behind jest poniżej. Parametr MainLauncher oznacza, że będzie on aktywnością, która uruchomi się pierwsza, NoHistory zablokuje możliwość powrotu do splash screena, kiedy będziemy juz na liście klientów. Layout zawiera tylko prosty progress bar informujący o ładowaniu się aplikacji.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background="#9e68ae"
              android:orientation="vertical">

    <ProgressBar
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="center" />
</LinearLayout>
[Activity(MainLauncher = true, 
          NoHistory = true,
          ScreenOrientation = ScreenOrientation.Portrait)]
public class SplashScreen : MvxSplashScreenActivity
{
    public SplashScreen()
        : base(Resource.Layout.splash_screen)
    {
    }
}

Widok listy klientów

W końcu dodamy pierwszy widok. Skorzystamy z CoordinatorLayout. Mówi się o nim jako o FrameLayout na sterydach. Jego głównym atutem jest obsługa zachowań (ang.behaviour). Czym są te zachowania? No to przykładzik.

Powiedzmy, że mamy widok listy klientów oraz wcześniej wspomniany FAB do dodawania nowego klienta. Koniecznie chcemy, aby podczas przewijania listy, nasz przycisk płynnie znikał, ponieważ nie chcemy zakrywać nim informacji o klientach, którzy są w dolnej części ekranu. Natomiast chcemy, aby pojawił się ponownie wtedy, kiedy przewiniemy listę lekko do góry. Wydaje się trudne? Za pomocą zachowań wcale tak nie jest. Wystarczy, że do widgetu, który chcemy, aby był celem dodamy identyfikator odpowiedniego zachowania. Zachowanie będzie realizowało to wcześniej opisane zadanie.

Czy, aby przykład nie pochodzi z naszej aplikacji? 🙂 W jednym z kolejnych części dodamy takie zachowanie dla naszego FABa.

Stwórz nowy layout i nazwij go customers_list. Jego definicja znajduje się poniżej. Zwróć uwagę na FloatingActionButton i na to za pomocą jakich właściwości umiejscowiliśmy go w prawym dolnym rogu ekranu. Jako widgetu listy skorzystaliśmy z MvxRecyclerView (zawdzięczamy go MvvmCross), jest to ulepszona wersja standardowego RecyclerView, posiada kilka funkcjonalności m.in. ułatwiających binding danych, do którego dostęp uzyskasz dzięki przestrzeni app. Za pomocą MvxBind możemy podpiąć się np. do pola Text kontrolki TextView i połączyć je z właściwością w naszym modelu widoku. I o dziwo, wartość tego pola będzie odpowiednio aktualizowana jeżeli zostanie zmieniona w modelu widoku. W naszym przypadku, podpinamy ItemsSource kontrolki MvxRecyclerView do listy naszych klientów w modelu widoku. Jednocześnie podpinamy się pod ItemClick i łączymy do zdarzenie z CustomerItemClickCommand z modelu widoku. Pod spodem MvvmCross przekaże kliknięty obiekt jako wewnętrzny parametr commanda, który my odbierzemy wewnątrz modelu widoku.

Możesz zwrócić uwagę na to, że nigdzie nie deklarujemy, żadnego adaptera dla RecyclerView. W tym przypadku MvvmCross stworzy nam adapter domyślny wykorzystując wartość atrybutu MvxItemTemplate linijkę niżej. Atrybut MvxItemTemplate daje nam możliwość zdefiniowania layoutu na pojedynczego elementu listy. 

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFFF">

    <android.support.constraint.ConstraintLayout
        android:id="@+id/content_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <View
            android:id="@+id/header_top"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:background="@drawable/app_background"
            app:layout_constraintHeight_percent="0.12" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_marginBottom="8dp"
            android:gravity="center"
            android:text="Customers"
            android:textColor="#d9FFFFFF"
            android:textSize="20sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="@+id/header_top"
            app:layout_constraintTop_toTopOf="parent" />

        <MvvmCross.Droid.Support.V7.RecyclerView.MvxRecyclerView
            android:id="@+id/customers"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:MvxBind="ItemsSource Customers; ItemClick CustomerItemClickCommand"
            app:MvxItemTemplate="@layout/customer_item"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/header_top" />
    </android.support.constraint.ConstraintLayout>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/add_customer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|right"
        android:layout_margin="16dp"
        app:MvxBind="Click AddNewCustomerCommand"
        app:layout_anchor="@+id/content_layout"
        app:layout_anchorGravity="bottom|right|end"
        app:srcCompat="@drawable/baseline_add_white_48dp" />

</android.support.design.widget.CoordinatorLayout>    

Dodaj nowy plik customers_item.xml w Resource → layout oraz w folderze Resources → drawable plik app_background.xml. Ten drugi jest definicją tła dla widoków, który m.in. wykorzystywanego na liście klientów.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <gradient
        android:angle="270"
        android:endColor="#44294C"
        android:startColor="#402143" />
</shape>   

Wewnątrz layoutu dla pojedynczego elementu listy klientów, ponownie korzystamy z data bindingu. Pamiętaj, że teraz bindujemy się do obiektu CustomerListItemPO, ważne jest, aby zapisać poprawne nazwy właściwości z tego właśnie obiektu w miejscach bindowania. Użyliśmy kontrolki MvxCachedImageView, pochodzi ona z biblioteki FFImage. Jednocześnie dodaliśmy kilka placeholderów podczas pobierania awataru lub problemu z pobraniem. Wartość ‘res:placeholder.jpg’ oznacza obraz placeholder.jpg w zasobach (Resources) aplikacji Android.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                             xmlns:app="http://schemas.android.com/apk/res-auto"
                                             xmlns:tools="http://schemas.android.com/tools"
                                             android:layout_width="match_parent"
                                             android:layout_height="wrap_content"
                                             android:background="@null">

    <View
            android:id="@+id/separator_left"
            android:layout_width="4dp"
            android:layout_height="0dp"
            android:background="#FF4789"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHeight_percent="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />


    <FFImageLoading.Cross.MvxCachedImageView
            android:id="@+id/avatar"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="16dp"
            android:layout_marginBottom="16dp"
            app:MvxBind="LoadingPlaceholderImagePath 'res:placeholder.jpg';
            ErrorPlaceholderImagePath  'res:placeholder.jpg'; 
            ImagePath AvatarUrl"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintDimensionRatio="1:1"
            app:layout_constraintStart_toEndOf="@+id/separator_left"
            app:layout_constraintTop_toTopOf="parent" />

    <TextView
            android:id="@+id/name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:text="Tomasz jakis"
            android:textColor="@color/text_color"
            android:textSize="16dp"
            android:textStyle="bold"
            app:MvxBind="Text FullName"
            app:layout_constraintStart_toEndOf="@+id/avatar"
            app:layout_constraintTop_toTopOf="@+id/avatar" />


    <ImageView
            android:id="@+id/phone_icon"
            android:layout_width="16dp"
            android:layout_height="16dp"
            android:layout_marginStart="8dp"
            android:src="@drawable/phone"
            app:layout_constraintBottom_toTopOf="@+id/separator_bottom"
            app:layout_constraintStart_toEndOf="@+id/avatar"
            app:layout_constraintTop_toBottomOf="@+id/name" />

    <TextView
            android:id="@+id/telephone"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="4dp"
            android:text="TextView"
            android:textColor="@color/text_color"
            android:textSize="12dp"
            app:MvxBind="Text Number"
            app:layout_constraintBottom_toTopOf="@+id/separator_bottom"
            app:layout_constraintStart_toEndOf="@+id/phone_icon"
            app:layout_constraintTop_toBottomOf="@+id/name" />

    <View
            android:id="@+id/separator_bottom"
            android:layout_width="0dp"
            android:layout_height="1dp"
            android:background="#40333333"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout> 

Mamy już definicję widoku, teraz przejdziemy do dodania code-behind dla niego. Do folderu Views projektu CustomerManager.Droid dodaj klasę CustomersListView. Uruchom aplikację. Powinieneś uzyskać efekt jak na obrazie poniżej.

Zwróć uwagę na atrybut Activity nad klasą. Kiedy piszesz aplikację natywną Android, musisz pamiętać o dodaniu aktywności do pliku AndroidManifest, natomiast przy pisaniu aplikacji Android przy użyciu Xamarina, robisz to przy wykorzystaniu atrybutu Activity. Jest kilka dostępnych parametrów do ustawienia. My pokusiliśmy się do zablokowania rotacji w aplikacji. Teraz powinieneś bez problemu uruchomić aplikację. Jeżeli będziesz miał problem z ustawieniem SetContentView, a dokładnie środowisko nie znajdzie widoku customers_list – przebuduj projekt.

[Activity(ScreenOrientation = ScreenOrientation.Portrait)]
public class CustomersListView : MvxAppCompatActivity
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        SetContentView(Resource.Layout.customers_list);
    }
}
Aplikacja po uruchomieniu

Powrót do projektu Core

Przydałaby się jakaś lista klientów. Musimy jakoś sprawdzić czy przygotowane widoku zadziałają poprawnie. Dodamy serwis, który zwróci nam listę klientów (narazie mock). We wszystkich projektach zainstaluj paczkę NuGet o nazwie AutoMapper. AutoMapper jest standardową biblioteką służącą do mapowania obiektów. Po odpowiednim skonfigurowaniu, biblioteka ta przepisze nam wartości pól jednego obiektu do odpowiadających pól drugiego obiektu. Dzięki niej znacząco zmniejsza się ilość kodu, którego jednym zadaniem byłoby przepisywanie wartości poszczególnych właściwości. Przyda się nam jej pomoc podczas zwracania listy klientów, serwis będzie operował na obiekcie DTO, który w modelu widoku jest niepożądany – tam chcemy mieć CustomerListItemPO

Potrzebna będzie nam wstępna konfiguracja AutoMappera. Zaczniemy od stworzenia tzw. profilu. Profil jest niczym innym jak zbiorem spójnych po względem przeznaczenia mapowań np. CustomerProfile (tak jak u nas), DocumentsProfile (dla mapowania obiektów, które opisują encję dokument). 

Stwórz folder MapperProfiles, w projekcie Core w folderze Infrastructure. Będziemy przechowywać w nim wszelkie profile mapowania. Składnia metody CreateMap jest prosta. Pierwszym parametrem typu jest typ źródłowy, a drugim ten, na który chcemy docelowo mapować obiekty w przyszłości. To wywołanie jest proste, ale AutoMapper umożliwia dogłębne skonfigurowanie takiego mapowania możemy m.in. dodać konwertery wartośći  czy poinformować bibliotekę, że chcemy konkretną właściwość mapować do właściwości o innej nazwie (domyślnie AutoMapper przepisuje wartości po odpowiadających sobie nazwach właściwości).

Natomiast klasę MapperConfigurator umieść bezpośrednio w folderze Infrastructure. Jej zadaniem jest zbudowanie wcześniej deklarowanych profili. 

public class CustomerProfile : Profile
{
    public CustomerProfile()
    {
        CreateMap<CustomerDTO, CustomerListItemPO>();
    }
}

public class MapperConfigurator
{
    public static MapperConfiguration InitializeProfiles()
    {
        return new MapperConfiguration(cfg =>
        {
            cfg.AddProfile(new CustomerProfile());
        });
    }
}

Pozostało nam tylko rejestracja instancji Mappera w kontenerze IoC. Dzięki temu, tam gdzie będziemy chcieli skorzystać z mapowania wystarczy, że w konstruktorze przekażemy IMapper. MvvmCross (konkretnie jego IoCProvider) dostarczy nam odpowiednią instancję.

public class App : MvxApplication
{
    public override void Initialize()
    {
        CreatableTypes()
            .EndingWith("Service")
            .AsInterfaces()
            .RegisterAsLazySingleton();

        Mvx.IoCProvider.RegisterSingleton(MapperConfigurator.InitializeProfiles().CreateMapper());
            
        RegisterAppStart();
    }
}

Przyszedł czas CustomerService. Dodaj go do folderu Services. Pamiętaj, także o stworzeniu interfejsu ICustomerService. Póki co zadaniem serwisu będzie zwracania zamockowanych danych. Dla urozmaicenia awatary klientów będą pobierane z serwisu randomuser.me.  Udostępnia on proste API do pobierania awatarów. Udostępnia prawdopodobnie kilkadziesiąt awatarów. Metoda GetRandomAvatarUrl wylosuje nam indeks, a na jego podstawie odpowiednio utworzy URL do zdjęcia. Taki URL możemy bez problemu zbindować poźniej do awatara klienta już w widoku, ponieważ biblioteka FFImage już sobie z tym poradzi.

public interface ICustomerService
{
    Task> GetCustomersListItems();
}    
    
    
public class CustomerService : ICustomerService
{
    private readonly IMapper _mapper;

    public CustomerService(IMapper mapper)
    {
        _mapper = mapper;
    }

    public async Task> GetCustomersListItems()
    {
        var customersDTO = GetCustomersMock();
        var customersPO = _mapper.Map<List<CustomerListItemPO>>(customersDTO);

        return await Task.FromResult(customersPO);
    }

    private List GetCustomersMock()
    {
        var customers = new List();

        customers.Add(new CustomerDTO()
        {
            Id = "1",
            City = "Poznań",
            FlatNumber = "13",
            Name = "Krzysztof",
            Number = "111111111",
            PostalCode = "50-240",
            StreetName = "ul. Niska 4",
            Surname = "Baranowski"
        });

        customers.Add(new CustomerDTO()
        {
            Id = "2",
            City = "Warszawa",
            FlatNumber = "10",
            Name = "Tomasz",
            Number = "222222",
            PostalCode = "22-151",
            StreetName = "ul. Fiołkowa 34",
            Surname = "Kowalski"
        });

        customers.Add(new CustomerDTO()
        {
            Id = "3",
            City = "Szczecin",
            FlatNumber = "1",
            Name = "Józek",
            Number = "123456789",
            PostalCode = "21-301",
            StreetName = "ul. Kolorowa 50",
            Surname = "Nowakowski"
        });

        for (int i = 0; i < customers.Count; i++)
        {
            customers[i].AvatarUrl = GetRandomAvatarUrl(customers[i].Id);
        }

        return customers;
    }

    private string GetRandomAvatarUrl(string id)
    {
        var random = new Random();

        var idValue = string.IsNullOrEmpty(id) ? 1 : Convert.ToInt32(id);
        var index = random.Next(0, 80) + idValue;

        return $"https://randomuser.me/api/portraits/men/{index}.jpg";
    }
}    

Serwis jest gotowy, teraz już tylko z niego korzystać w modelu widoku. Umieszczę tylko zaktualizowaną metodę UpdateCustomersList. Twoim zadaniem będzie dodanie do konstruktora modelu widoku, parametru ICustomerService i przypisanie go do prywatnego pola. 

private async Task UpdateCustomersList()
{
    var customers = await _customerService.GetCustomersListItems();
    Customers = new MvxObservableCollection(customers);
}    

Uruchom aplikację. Powinieneś uzyskać efekt jak poniżej.

Lista klientów 🙂

Wersja iOS w kolejnej części

I oto dotarliśmy do końca tej części. Udało się sporo zrealizować, mamy już działającą wersję Android i solidne podstawy do tego, aby rozwijać dalej projekt. W kolejnej części zobaczysz, że bez problemu skorzystamy z gotowych już funkcjonalności w wersji iOS. 

Do następnego!

Github: https://github.com/krzbbaranowski/customer_manager_pb

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