Xamarin.Forms czy Xamarin Native? Moja pierwsza aplikacja
Xamarin to technologia, o której słyszała chyba większość osób związanych w jakiś sposób z programowaniem. Kiedy powstawała jej głównym założeniem było umożliwienie pisania aplikacji na wiodące systemy mobilne przy użyciu jednego języka oraz platformy. Wybór twórców padł na platformę .NET i C#.
Niewątpliwie do wzrostu popularności Xamarina przyczyniła się decyzja Microsoftu o jego zakupie i udostępnianiu go za darmo wszystkim zainteresowanym. Do przejęcia doszło w 2016 roku i mniej więcej wtedy zainteresowałem się Xamarinem bliżej.
Łukasz Soroczyński. Naukę programowania rozpoczął około pięciu lat temu, która z mniejszymi lub większymi przerwami trwa do dziś. Uważa, że najlepszym sposobem na zdobywanie wiedzy w tej branży jest samodzielne stawianie sobie zadań i ich realizowanie. Swoją przyszłość wiąże z branżą IT, a w szczególności z programowaniem.
Spis treści
Dlaczego zainteresowałem się Xamarinem?
Na początku muszę zaznaczyć, że przed zetknięciem się z Xamarinem nigdy wcześniej nie programowałem natywnie na żadne systemy mobilne, nie licząc aplikacji Universal Windows Platform. Było mi po prostu nie po drodze, swoją przygodę z programowaniem zacząłem od C++, następnie przesiadłem się na C#. W tamtym czasie pisałem głównie aplikacje okienkowe z pomocą Windows Presentation Foundation – nie ukrywam tworzenie GUI metodą drag and drop bardzo przypadło mi do gustu, tak samo, jak całe środowisko Visual Studio.
Przyszedł jednak czas, kiedy zacząłem obnosić się z myślą o napisaniu swojej pierwszej mobilnej aplikacji na Androida. Dlatego też, kiedy usłyszałem, że mogę zrobić to przy użyciu mojego ulubionego C# nie czekałem długo i przystąpiłem do instalacji Xamarina. Kiedy po instalacji moim oczom ukazało się okienko służące do utworzenia nowego projektu, stanąłem przed pierwszym poważnym wyborem…
Xamarin.Forms czy Xamarin Native?
Xamarin to jedna technologia, pozwala jednak pisać aplikacje przy użyciu dwóch różnych podejść:
Aplikacje „natywne” – są to aplikacje, które pisze się z przeznaczeniem tylko na jedną konkretną platformę. Dzięki temu, że Xamarin mapuje całe API Androida i innych systemów, programowanie takich aplikacji zasadniczo nie różni się od zwykłego podejścia dla danej platformy. Oczywiście poza tym, że tutaj używa się C#, a nie Javy, czy Objective-C. Jego zaletą jest dostęp do bardzo specyficznych funkcji każdego z systemów.
W tym momencie możecie zapytać, po co pisać w C# aplikacje na Androida skoro jedyną różnicą jest język, w którym piszemy? Powody są co najmniej dwa. Po pierwsze cała logika, którą umieszczamy w aplikacji, może być po prostu przekopiowana do projektu przeznaczonego dla innej platformy. Oczywiście z pominięciem elementów specyficznych dla danego systemu (obsługa GUI, usług, plików ect). Po drugie jeśli znasz C#, ale nie znasz Javy, to nie musisz się jej uczyć. Może powód trochę banalny, bo horyzonty warto poszerzać i jestem tego wielkim zwolennikiem, ale kiedy nie mamy zbyt wiele czasu jest to mały, bo mały, ale jednak atut.
Aplikacje cross-platformowe oparte o Xamarin.Forms – są to aplikacje przeznaczone na kilka różnych platform jednocześnie (Android, iOS, UWP, Windows Phone), co umożliwia biblioteka Xamarin.Forms. W teorii większość kodu odpowiedzialnego za wygląd i logikę powinna być współdzielona pomiędzy wszystkimi projektami w ramach jednego rozwiązania. Dzięki Xamarin Forms wygląd aplikacji piszemy w XAML’u, który z kolei jest „konwertowany” na natywne kontrolki każdej z platform podczas kompilacji. Moim zdaniem Xamarin Forms to główny powód, dla którego warto używać Xamarina. Jak każda technologia nie jest on jednak pozbawiony wad.
Które podejście jest lepsze? To wszystko zależy od tego, co właściwie chcemy stworzyć. Jeżeli ma być to aplikacja, która stawia na multiplatformowość i będzie głównie operować na danych, to Xamarin.Forms wydaje się najlepszą opcją. Pozwoli nam bowiem współdzielić jeden kod pomiędzy wszystkimi platformami, co ułatwia konserwację aplikacji, jak i zaoszczędza masę czasu podczas jej pisania. W końcu tworzymy jeden projekt w jednej technologii, a nie trzy w trzech różnych technologiach. Sytuacja ma się inaczej jeżeli nasza aplikacja będzie operować na czymś ściśle związanym z danym systemem operacyjnym (no nie wiem… na przykład apka do tworzenia partycji swap dla Androida).
Tutaj Xamarin Forms na niewiele się zda, a wręcz może utrudnić sprawę, bo logikę projektu dla każdego systemu trzeba będzie napisać inaczej – może się zdarzyć, że niektórych rzeczy nie da się zrobić na jednym systemie, chociaż drugi na to pozwoli. Takie sytuacje wykluczają wręcz używanie tego podejścia. Niemniej dalsza część artykułu będzie poświęcona właśnie Xamarin Forms, bo moim zdaniem, to technologia mimo wszystko bardzo godna uwagi.
Xamarin.Forms PCL vs Shared Project
Na powyższym podziale jednak się nie kończy. Samo Xamarin Forms również pozwala pisać aplikacje wieloplatformowe na dwa różne sposoby. No dobra, właściwie to na trzy, bo od czasu kiedy pisałem coś w Xamarinie minęło trochę czasu i wprowadzono pewne zmiany.
Portable Class Library (PCL) – w wolnym tłumaczeniu „Przenośna biblioteka klas”. Podejście te polega na tym, że w ramach jednego rozwiązania tworzone są projekty dla każdego systemu z osobna + jeden projekt przenośny (portable). Do tego projektu wszystkie pozostałe posiadają referencję. Podczas kompilacji kod tego projektu jest kopiowany do wszystkich innych i kompilowany. Z tego też powodu zasób klas, z jakich możemy w nim korzystać jest „ucinany” do najmniejszego wspólnego mianownika wszystkich projektów.
Co to właściwie oznacza? Korzystać możemy tylko z klas/bibliotek, których implementacje dostępne są na wszystkie platformy zwarte w „rozwiązaniu”. Jeszcze jakiś czas temu dobrym przykładem mogła być klasa WebClient, która pozwala m.in. na pobieranie plików. Przy okazji pisania tego artykułu zauważyłem, że implementacja klasy WebClient została w końcu wprowadzona do UWP. Jednak kiedy pisałem w Xamarinie swoją pierwszą aplikację, była ona dostępna dla platformy iOS oraz Android, jednak nie dla aplikacji uniwersalnych Windows. W rezultacie nie mogłem użyć jej w projekcie przenośnym/portable. Dlatego, że taki kod po skopiowaniu do projektu UWP, po prostu by nie działał (bo w tamtym czasie taka klasa dla tej platformy nie istniała). Po usunięciu projektu UWP z solucji nie było z tym problemu.
Takich przykładów można by wymienić całą masę, jednak i temu da się zaradzić. W sieci można znaleźć masę bibliotek, które przywracają zagubione funkcjonalności – np. PCL Storage do obsługi plików. W razie potrzeby możemy też sami stworzyć osobny specyficzny kod/klasę dla każdej z platform, który będziemy wywoływać z projektu głównego. Służy do tego mechanizm DependencyService (o którym jeszcze wspomnę). Jakiś czas temu PCL zastąpił .NET Standard. Właściwie zasada działania pozostaje taka sama, z tym, że teoretycznie .NET Standard ustala minimum, które wszystkie platformy muszą implementować. Przynajmniej z tego co udało mi się dowiedzieć, tak właśnie wynika. Swoje zdanie w tym temacie będę musiał sobie jeszcze wyrobić, bo od czasu wprowadzenia tej zmiany niczego w Xamarinie nie pisałem. Szczerze mówiąc jestem ciekawy czy pokrycie dostępnych klas dla wszystkich platform w dużym stopniu polepszyła się po wprowadzeniu .NET Standard.
Shared Project – to drugi rodzaj podejścia. Pozwala na tworzenie w ramach projektu współdzielonego kodu specyficznego dla każdej z platform przy użyciu bloków tego typu:
spółdzielonego kodu specyficznego dla każdej z platform przy użyciu bloków tego typu: #if __ANDROID__ //instrukcje dla androida #endif #if __iOS__ //instrukcje dla iOS #endif //etc
Następnie kompilator kopiuje do każdego projektu kod, który zamknęliśmy w tych blokach. Może takie podejście ma też zalety, ale mi osobiście się nie podoba. Być może sprawdza się przy pisaniu małych projektów, ale w przypadku większych w kodzie zrobi się zwyczajny bałagan… Przynajmniej według mnie.
Co mnie mile zaskoczyło w Formsach, a co… rozczarowało?
Xamarin Forms na pewno przypadnie do gustu osobą, które miały już styczność z technologią Windows Presentation Foundation – a co za tym idzie z C# i przede wszystkim ze XAML’em. Jest to spowodowane tym, że UI aplikacji w Xamarin Forms piszemy właśnie przy użyciu XAML’a. Właśnie dzięki temu, choć nigdy wcześniej nie napisałem niczego na Androida, bardzo szybko odnalazłem się w tym jak pisać design aplikacji i jak go obsługiwać. Chcę użyć wzorca MVVM znanego z WPF? Nie ma problemu! Do tego raz napisane UI wygląda niemal identycznie na wszystkich urządzeniach niezależnie od systemu – no bajka! Nie mogę jednak mówić o tej technologii w samych superlatywach.
Parokrotnie zdarzyło mi się, że jakaś rzecz działała na Androidzie, na komputerze (UWP), ale na telefonie z Windows 10 Mobile już nie… Niemniej prawie zawsze kolejna aktualizacja Xamarina rozwiązywała moje problemy. Cóż… jest to technologia cały czas rozwijana i wpadki mogą się zdarzać. Od mojego ostatniego zetknięcia się z Xamarinem minął ponad rok, więc myślę, że ilość błędów powinna już być znacznie mniejsza. Muszę również dodać, że aplikacje pisane w Xamarin Forms ważą sporo więcej oraz są nieco wolniejsze od ich natywnych odpowiedników – za wygodę pisania cross platformowych apek trzeba płacić. W tym przypadku zapłatą za wygodę jest utrata wydajności. Widać to w szczególności podczas samego uruchamiania skompilowanej aplikacji na telefonie.
To… jak pisze się w tych Formsach?
Stronę wizualną aplikacji pisze się przy użyciu języka XAML. Jeżeli ktoś już wcześniej pisał jakieś aplikacje w technologii WPF to nie powinien mieć żadnego problemu z odnalezieniem się w Xamarinowym XAML’u. Co prawda można dostrzec lekkie różnice, takie jak nazwy niektórych kontrolek, czy nazwy ich właściwości. Nie powinno to jednak sprawiać problemu. Dla przykładu taki kod (MainPage.xaml):
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns_x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns_local="clr-namespace:App2" :Class="App2.MainPage"> <StackLayout> <!-- Place new controls here --> <Label x_Name="myLabel" Text="Witaj w Xamarin.Forms!" HorizontalOptions="Center" VerticalOptions="CenterAndExpand" /> <Button Text="Kliknij mnie!" Clicked="Button_Clicked"/> </StackLayout> </ContentPage>
Reprezentuje taki oto wygląd aplikacji (w przypadku Androida i UWP):
Niestety screenshota z iOS’a nie przedstawię. Nie posiadam bowiem sprzętu, na którym mógłbym skompilować oraz uruchomić aplikację.
Obsługa GUI z poziomu C# wygląda również identycznie, jak w WPF. Przechodząc do pliku MainPage.xaml.cs możemy zapisać funkcję realizującą zmiany w wyświetlanym komunikacie po kliknięciu przycisku:
using System; using Xamarin.Forms; namespace App2 { public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); } int i = 0; private void Button_Clicked(object sender, EventArgs e) { myLabel.Text = "Kliknięto: " + i++ + " razy"; } } }
Oczywiście, tak samo jak w WPF możemy stosować tutaj wzorzec MVVM. Odpowiednie biblioteki ułatwiające implementację tego wzorca są już od dawna dostępne w repozytoriach nugeta. Nie będę się w tym miejscu zatrzymywał na dłużej. Jeżeli kogoś interesuje temat, śmiało może zajrzeć do dokumentacji Xamarina na stronie Microsoftu. Znajduje się tam wiele przydatnych informacji wraz z przykładami dotyczącymi m.in. tworzenia i obsługi interfejsu użytkownika. Materiały dostępne są tutaj.
W związku z tym nie widzę sensu, żebym przepisywał zawarte tam informacje tutaj. Od mojej ostatniej wizyty na tej stronie pojawiło się całe mnóstwo nowych treści. Do tego większość jest teraz dostępna w języku polskim, więc gdyby ktoś miał problem z angielskim to zostaje on rozwiązany. Co prawda tłumaczenia mogą nie być doskonałe, bo większość artykułów tłumaczyła maszyna. Nie powinno to jednak sprawiać dużego problemu.
Mechanizm DependencyService
Jeżeli mówimy już o Xamarin Forms, na chwilę uwagi na pewno zasługuje mechanizm DependencyService. Czasami może dojść do sytuacji, kiedy będziemy potrzebować użyć jakiejś specyficznej funkcji, która jest realizowana w inny sposób na Androidzie, UWP i iOS’ie. Dajmy na to… Na przykład bardziej złożone operacje na systemie plików. Nie wiem czy na ten moment coś się polepszyło, ale kiedy pisałem swoją pierwszą aplikację, to musiałem użyć tego mechanizmu, aby najpierw pobrać, a następnie zapisać plik w pamięci urządzenia. Kod w przypadku Androida/iOS’a i UWP wyglądał zgoła inaczej. Wynikało to głównie z dostępności różnych bibliotek służących do operacji pobierania pliku na tych platformach oraz innego sposobu poruszania się po systemie plików.
Właśnie do rozwiązywania takich problemów powstał DependencyService. Jest to mechanizm udostępniany przez Xamarina umożliwiający implementację funkcji specyficznych dla każdej platformy. Funkcje te z kolei możemy wywoływać z naszego projektu przenośnego (portable) przy pomocy konstrukcji DependencyService.Get<interface>. Całość dość dobrze obrazuje poniższy rysunek:
Używanie tego mechanizmu sprowadza się do utworzenia nowego interfejsu w naszym projekcie współdzielonym/przenośnym. Dajmy na to, na przykład takiego:
using System.Threading.Tasks; namespace App2 { public interface ICrossPlatformDownloadManager { Task<string> DownloadFileAsync(string url, string fileName); } }
Kolejną rzeczą jaką trzeba zrobić, jest stworzenie we wnętrzu wszystkich projektów przeznaczonych na konkretne platformy klas, które będą implementować powyższy interfejs. W przypadku Androida kod może wyglądać na przykład tak:
using System; using System.IO; using System.Net; using System.Threading.Tasks; using App2.Droid;
[assembly: Xamarin.Forms.Dependency(typeof(CrossPlatformDownloadManager))]
namespace App2.Droid { public class CrossPlatformDownloadManager : ICrossPlatformDownloadManager { public async Task<string> DownloadFileAsync(string url, string FileName) { try { string path = Path.Combine(Android.App.Application.Context.GetExternalFilesDir(null).Path, FileName); WebClient webClent = new WebClient(); await Task.Run(() => webClent.DownloadFile(url, path)); return path; } catch (Exception) { return null; } } } }
Należy w tym miejscu pamiętać o linijce siódmej. Ona służy do aktywacji działania DependencyService.
W przypadku UWP kod może wyglądać w taki sposób:
using App2.UWP; using System; using System.IO; using System.Net.Http; using System.Threading.Tasks; using Windows.Storage;
[assembly: Xamarin.Forms.Dependency(typeof(CrossPlatformDownloadManager))]
namespace App2.UWP { public class CrossPlatformDownloadManager : ICrossPlatformDownloadManager { public async Task<string> DownloadFileAsync(string url, string fileName) { try { HttpClient client = new HttpClient(); // Create HttpClient byte[] buffer = await client.GetByteArrayAsync(url); // Download file StorageFile destinationFile = await ApplicationData.Current.LocalFolder.CreateFileAsync( fileName, CreationCollisionOption.ReplaceExisting); using (Stream stream = await destinationFile.OpenStreamForWriteAsync()) stream.Write(buffer, 0, buffer.Length); // Save return destinationFile.Path; } catch (Exception) { return null; } } } }
Wywołanie powyższych funkcji z projektu wspólnego sprowadza się do takiej oto linijki:
string result = await DependencyService.Get<ICrossPlatformDownloadManager>().DownloadFileAsync("http://example.com/", "index.html");
Tylko tyle. Xamarin zadba już o to, żeby odpowiednie umieszczone w pozostałych projektach metody zostały uruchomione na odpowiednim dla nich systemie. Proste, a jakże przydatne. Od razu uprzedzam ewentualne uwagi odnośnie samego przykładu. Kod ten pisałem już spory czas temu. Na dzień dzisiejszy być może nie ma już potrzeby realizowania funkcji pobierania pliku w ten sposób, bo przez czas kiedy ja zajmowałem się innymi rzeczami, Xamarin nieustannie się rozwijał. Chodzi tutaj głównie o pokazanie zasady działania DependencyService. To co siedzi w metodach ma znaczenie drugorzędne.
Moje aplikacje
Jeżeli interesuje kogoś, jak może wyglądać przykładowy kod „pełnoprawnej” aplikacji napisanej w Xamarin Forms zachęcam do przejrzenia kodu mojego pierwszego projektu, który wykonałem w tej technologii. Aplikacja jest również dostępna w sklepie Google Play. Tak samo jak kilka innych pod tym adresem.
Zdaję sobie sprawę, że bardziej doświadczone ode mnie osoby znajdą w kodzie dziesiątki rzeczy, które można było zrobić lepiej. Na swoje usprawiedliwienie muszę powiedzieć, że aplikacja ta powstawała na bieżąco w trakcie poznawania przeze mnie Xamarina. W zasadzie, to cała moja nauka programowania wygląda tak, że piszę projekty tego typu. Dlatego też nie są one pozbawione wad. Myślę również, że wiele osób robi podobnie i osobiście uważam taką formę zdobywania wiedzy za najlepszą.