Mé začátky s vývojem pro mobilní zařízení

Flutter vs. Xamarin.Forms

Poprvé v životě pracuji na mobilní aplikaci. Protože má appka fungovat jak pro Android tak pro iOS tak ji píšu v Xamarinu, konkrétně Xamarin.Forms.

Jsem oproti Flutteru v nevýhodě, protože Xamarin není tak moc populární, ale nakonec jdu do toho, protože znám dobře C# a znám dobře Visual Studio. Zkusil jsem nanečisto udělat projekt ve Flutteru ale je toho prostě příliš mnoho, co bych se musel naučit a já už bych raději byl produktivní, než abych trávil nejméně jeden či dva měsíce plácáním se s pro mě kompletně neznámým stackem.

Popularita Flutter vs. Xamarin vs. Electron

Microsoft udržuje Xamarin celkem intenzivně naživu a přestože je Xamarinská komunita zjevně mnohem menší než Flutterovská tak rozhodně není až zas tak malá, že by se nevyplatilo do vývoje přes Xamarin investovat.

Android vs. iOS

Flutter ani Xamarin.Forms by nemusel existovat, kdyby neexistoval Apple. Všichni by měli Androida a byl by pokoj.

Vlastně: oni by ty iPhony ani tak nevadily, kdyby se Apple nechoval jak Microsoft v historii se svým Internet Explorerem. Vývoj pro obě platformy naráz – Android a iOS – fakt připomíná dělání webů v minulosti, kdy se prostě muselo při vývoji velmi často zohledňovat, že v prohlížečích fungovalo spoustu věcí jinak a Internet Explorer byla vždycky kategorie sama o sobě.

Microsoft se poučil, Internet Explorer zabil a nahradil jej Edge (což je jen Chrome s jiným oblekem), koupil Github a opensourcoval celou svoji vývojářskou platformu .NET, vyrobil VS Code a Visual Studio nabízí zdarma (pokud nejste větší firma). Microsoft pochopil, že na vývoj v nějaké closed-box platformy mu většina lidí sere a že cloud běží většinou na Linuxech.

Google to zjistil pravděpodobně do určité míry taky, vyvíjet pro Android lze na jakémkoliv OS a Android je vlastně jen upravený Linux, který je stejně jako Linux open-source.

Jenže ne Apple. V Applu furt pracují blbci co žijí v roce 2001. Vyvíjet pro iOS na Windowsech ani na Linuxech nelze. Tedy lze — pokud máte náladu na několika denní sraní se s qemu, Docker-OSX, kvm a podobný hackování — já s tím ztratil po několika dnech trpělivost, aspoň pod Windows 11/WSL2 to neběželo moc dobře (a bez podpory GPU) a nakonec na splátky budeme kupovat Maca, jen abychom vůbec aplikaci pro iOS zbuildili. Koukal jsem ještě na cloudové služby, kdy si za nějaký peníz Maca pronajmete ale vůbec se to nevyplatí, za ty peníze je užitečnější Maca splácet vlastního.

Visual Studio má pro to naštěstí celkem funkční tooling. Stačí, aby Mac byl dostupný v síti, zadá se jeho IP adresa, administrátorský účet s heslem a VS si tam doinstaluje vše potřebné. Při zbuildění iOS projektu se pak VS na Mac samo připojí, přes XCode si mobilní appku zbuildí a obrazovku mobilní appky, která běží nad XCodem v Macu, vám zobrazí vtipně přes vzdálenou plochu.

Vývoj mobilní aplikace

Jakmile má člověk nakonfigurované správně prostředí pro vývoj, běží vše dobře. XAML a MVVM jsem už prakticky zapomněl, naposledy jsem v něm dělal pro jednoho klienta WPF aplikace, ale to bylo ještě v dobách .NET Frameworku 4.6. Pořád jsem si byl ale jistej, že proniknout zpátky do XAMLu pro mě bude pořád efektivnější, než se učit s Flutterem.

Ve WPFku jsou pro mě věci takové intuitivnější. Nikdy jsem nebyl moc frontenďák ale naučil jsem se dělat weby a desktopové aplikace pro Windows přes Winforms a WPF.

Dělat na telefon je něco jiného. Ve Windows máte koncepty oken, modálních oken, okno s focusem. V telefonu ale žádná okna nejsou a místo toho se pracuje s navigačním stackem — okno, které se vám aktuálně zobrazuje, je okno které je umístěné na vrcholu navigačního stacku, do kterého buď okna přidáváte, nebo z něj okna odebíráte.

https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/navigation/hierarchical-images/pushing.png
Přidávání do navigačního stacku (zdroj)
https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/navigation/hierarchical-images/popping.png
Odebírání z navigačního stacku (zdroj)

V Xamarin.Forms je ale ještě jedna vyfikundace navíc – AppShell. Je to taková běžná struktura, kdy máte v mobilní aplikaci navigační panel vlevo a záložky vespod nebo nahoře. Díky AppShellu můžete libovolně kombinovat a zanořovat položky menu a záložky. Zajímavý je, že stránky, na který přes tyhle záložky a položky menu odkazujete, nejsou součástí navigačního stacku — AppShell má svůj vlastní navigační systém, kdy každá stránka vaší mobilní aplikace je identifikovaná nějakou route adresou, na kterou můžete odkazovat.

Navigace přes klasický navigátor

// navigace na stranku MyPage --- vlozi se do navigacniho stacku
await Navigation.PushAsync(new MyPage());

// navigace na predchozi stranku --- aktualni stranka se odebere z navigacniho stacku
await Navigation.PopAsync();

Navigace přes AppShell

// navigace na stranku MyPage
Shell.Current.GoToAsync("mypage");

// navigace na predchozis stranku
await Shell.Current.GoToAsync("..");

API klient – protokol a tooling

Mobilní aplikace většinou volá nějaké API, že jo. Ta naše aplikace není výjimkou.

REST API

V .NET Core se celkem intuitivně nabízí REST přes JSON. ASP.NET to podporuje ve svých knihovnách, stačí napsat Controllery, člověk nemusí řešit žádné ruční serializace a deserializace, můžete rovnou psát své objekty a máte vystaráno.

Pak stačí přidat Swagger nugety a ze svých controllerů máte vygenerovanou celou OpenApi specifikaci a nemuseli jste hnout ani prstem.

Tooling: RestSharp vs HttpClient

Pro C# existuje několik možností ale za zmínku stojí dle mého jen tyto dvě:

openapi-generator umí udělat klienta, který pracuje s RestSharpem zatímco NSwag pracuje přímo s HttpClientem. S HttpClientem se musí pracovat obezřetně – ve výchozí konfiguraci dispose HttpClienta nechává otevřený socket v TIME_WAIT dokud nevytimeoutuje. Používat HttpClient jako singleton je také chyba, protože HttpClient nerespektuje zjevně TTL u DNS záznamu (i když issue na githubu na tohle téma je už uzavřený, těžko říct, jak to funguje třeba v .NET 6, to bych musel otestovat). HttpClient se má používat správně přes HttpClientFactory

Z dokumentace:

Although HttpClient implements the IDisposable interface, it’s designed for reuse. Closed HttpClient instances leave sockets open in the TIME_WAIT state for a short period of time. If a code path that creates and disposes of HttpClient objects is frequently used, the app may exhaust available sockets. HttpClientFactory was introduced in ASP.NET Core 2.1 as a solution to this problem. It handles pooling HTTP connections to optimize performance and reliability.

Zdroj

NSwag a jeho HttpClient je tedy problematický …ale podle mně ne zas tolik. Nejsem si jistý, jestli u mobilní aplikace pro koncového uživatele prakticky vůbec může dojít k vyčerpání socketů, kterých teoreticky může být až 65536 (pro každý port jeden). U mobilní aplikace bych čekal, že OS (ať už Droid nebo iOS) bude na visící timeouty a spojení ve stavech jako TIME_WAIT mnohem agresivnější.

API klient – GRPC

Původně jsem chtěl dělat API a klienta přes GRPC. Věděl jsem, že pro Visual Studio je tooling hotový a skrz nugety lze zprovoznit GRPC endpointy okamžitě.

GRPC má oproti REST obrovskou výhodu: je to binární protokol. Což znamená, že přenos dat, serializace a deserializace je z podstaty věci mnohonásobně rychlejší, než JSON REST.

GRPC jsem nakonec ale nepoužil. API chci totiž provozovat jako WebAppku na AppService na Azure a bohužel, z nějakého důvodu (nejspíš lenost) Microsoft nemá dokončenou implementaci HTTP/2 na Windows Server OS a kvůli tomu GRPC na WebAppkách nefunguje. A to ani když máte Linuxovou AppServicu — což jsem nejdřív upřímně nechápal, pravděpodobně je to ale tím, že každá AppServica či WebAppka ve skutečnosti běží jako ServiceFabric, což je Windows Server.

Mobilní aplikace a unhandled exceptions

Když máte v C# konzolovku nebo nativní, desktopovou aplikaci ať už pro Windows nebo pro Linux, je někdy užitečné napsat kus kódu, který handluje nezachycené exceptiony. V .NETu je tohle triviální.

AppDomain.CurrentDomain.UnhandledException += MyHandler;
TaskScheduler.UnobservedTaskException += MyHandler; //pro zachyceni nezachycenych exception bezicich v tascich

V Xamarinu ale platí úplně jiný režim. Jakákoliv exceptiona shodí aplikaci bez ohledu na to, zdali je nebo není zachycená. Tohle je zjevně rozdíl mezi Flutterem a Xamarinem. Flutterovská aplikace beží totiž ve svém vlastním enginu a každý vygenerovaný pixel patří Flutteru. Takže i exceptiony, které si ve Flutteru vyrobíte, si můžete ve Flutteru také zpracovat.

Xamarin ale kompiluje všechno do nativního kódu takže handlování exception probíhá podle pravidel dané platformy.

AppDomain, TaskScheduler — to všechno sice proběhne, jakmile dojde k vyhození exceptiony, jenže aplikace stejně spadne. Android má zjevně nějaký svůj způsob, jak unhandled exceptiony handlovat. Všechno jsem to ale zkoušel a nic nezabrání tomu, že aplikace kompletně žuchne.

V iOSu to navíc údajně ani nejde, což zatím nemohu otestovat. Swift údajně nemá podporu pro zachycení nezachycených exception a někde jsem našel (teď se mi to nedaří dohledat), že Objective-C při vyhození exceptiony už není v obnovitelném stavu a jediné, co v tomto stavu ještě lze udělat, je zapsat někam lokálně log s chybou.

API klient bez exceptions

Takže v Xamarin prostředí vyhození exceptiony bezpodmínečně vede ke shození celé aplikace. Před shozením aplikace můžu exceptionu zalogovat ale to je vše – aplikace prostě žuchne.

Bomba.

V takovém případě se potřebuji exceptionám vlastně totálně vyhnout. Což je stejně správně. try/catch bloky jsou náročné na výpočetní výkon a navíc v UI aplikaci by se vůbec exceptiony neměly používat. Validace musí být součástí business logiky a jakýkoliv „nevalidní stav“ je něco, co nemělo vyhazovat exceptiony, natož shazovat celou aplikaci.

Průser je v tom, že NSwag vybleje klienta, který chrlí exceptiony jak hovado. openapi-generator používá v tomto ohledu trochu lepší strategii skrz staticky nastavený ExceptionHandler.

Pokud programujete REST api, pak pro každý endpoint vracíte různé HTTP status kódy. 200 pro vytažení dat, 204 pro změny, 400 pro validaci, 403 pro autorizaci atd. apod. Generátory mají tendenci na vše, co není 2xx, vyhodit exceptionu.

Co s tím?

Generované modely, vlastní API klient

Zkoušel jsem různé konfigurace a nastavení i s openapi-generatorem, (který jsem kvůli jiným důvodům nechtěl použít), ale nakonec jsem to vzdal, vždycky jsem narazil na nějaké dost podstatné limitace, které mi vadily a rozhodl jsem se napsat si vlastního klienta.

Modely ale naštěstí psát nemusím. Konfigurace NSwagu obsahuje parametry generateClientClasses a generateClientInterfaces. Pokud tyto parametry nastavím na false tak mi NSwag vygeneruje pouze request/response modely ale žádné klienty mi necpe.

Jednoduchého klienta jsem si napsal takto:

  • Při zavolání jakékoliv API metody dodávám zároveň funkci, která zpracovává validní odpověď (HTTP 2xx) a volitelně funkci, která zpracovává jakoukoliv jinou, než nevalidní odpověď (HTTP 4xx, 5xx).
  • Ve výchozím chování v případě nevalidní odpovědi chci, aby mi to vyplivlo nějaký popup, ve kterém je zobrazená generická zpráva o tom, co se z API vyplivlo. Toto chování ale mohu kdykoliv upravit.

Nemusím se tedy bát, že by mi NSwag vyhodil nějakou exceptionu a ani se nemusím starat o to, jak metody NSwagu wrapovat do nějakého try/catche, kterému se stejně chci v kódu vyhnout, pokud je to možné. Jasně – musím si napsat vlastního klienta ale protože kód pro mobil má sdílenou knihovnu s API tak URL endpointů mohu napsat staticky (parametrizované endpointy v tomhle použití budou muset být metody, což není vůbec hezké ale to jsem schopný zkousnout).

Ukázka volání API:

await Api.Get<GuideRequest>(url: Endpoints.User.GetLastGuideRequest, async (gr) =>
{
	if (gr != null && gr.State == GuideRequestState.Waiting)
	{
		LayoutState = LayoutState.Custom;
		CustomState = "RequestPending";
	}
	else
	{
		LayoutState = LayoutState.Success;
	}
});