Utilizzo “fluent” di Xamarin

Nel momento in cui ci accingiamo a sviluppare un’applicazione mobile, ci troviamo necessariamente a rispondere ad una serie di domande:

• Svilupperò un’applicazione nativa o ibrida?
• La mia applicazione dovrà interagire con entità esterne?
• Avrò un’architettura che mi supporta?

Le risposte ci aiuteranno a capire quali approcci tenere durante la progettazione e lo sviluppo, in modo da aumentare nettamente la probabilità di cadere in piedi.

In questo articolo ci concentreremo su applicazioni sviluppate nativamente con Xamarin.Forms, un framework sviluppato inizialmente dall’azienda statunitense Xamarin e successivamente acquistato da Microsoft, che permette la definizione di interfacce grafiche tramite un metalinguaggio indipendente dall’architettura, chiamato XAML.
XAML è un’estensione di XML che permette di definire in un’unica volta la struttura dell’interfaccia, condividendola fra le varie piattaforme, e riuscendo a renderizzarla con controlli nativi per ogni piattaforma.

Simuleremo un’interazione tra il frontend (la nostra applicazione) ed un backend mocked (che si occuperà di rispondere alle nostre richieste con dei dati statici), permettendo così di concentrarci sullo sviluppo lato client.

Vedremo come, una buona e preventiva gestione delle risorse grafiche, ci permetterà di non dover richiedere all’app modifiche ad icone, immagini e sfondi; il tutto supportato dalle indicazioni ufficiali di Apple e Google e spinto da una marcia in più, grazie ad un’introduzione sull’utilizzo di file vettoriali.

Infine, noteremo che Xamarin.Forms è fortemente propenso ad essere supportato dal famoso pattern architetturale MVVM, in cui la vista (ovvero l’interfaccia grafica) e la sua logica (il comportamento tenuto dall’applicazione di fronte all’interazione con l’utente) possono essere fortemente disaccoppiate.
Adottando dei piccoli accorgimenti il pattern prima citato sembrerà nativamente supportato dal framework.

“L’inizio è la parte più importante del lavoro.”
PLATONE

Json Server come backend per l’interazione Client-Server

Sviluppando un’applicazione Client-Server dovremo probabilmente decidere se occuparci prima del server o del client.
Supponiamo di voler partire dal frontend, scelta tipica se siamo in possesso di un mockup grafico esaustivo e definitivo; arriveremo molto presto a dover interagire con il server per ricevere dati. Invece di creare dei finti dati (semplicemente istanziando una collezione nel codice) facciamo un passo in più nella simulazione provando a chiamare un server mocked.
Utile al nostro scopo è Json Server : un tool che, una volta in running, leggendo una serie di dati da noi definiti in un file .json opportunamente nominato, si mette in ascolto di richieste http su una certa porta del localhost.
È molto facile installare Json Server tramite il package manager npm; in seguito basterà aprire un Command Prompt ed eseguire la seguente richiesta:

 json-server --watch db.json

In cui
json-server è la chiamata al tool installato;
–watch indica al tool di monitorare eventuali modifiche al file di interesse, oltre a rimanere in ascolto di chiamate HTTP;
db.json è il file json preventivamente definito e riempito con i dati da ricevere.
Il tool resterà in ascolto sulla porta di default, ovvero la 3000.

Facciamo un esempio:

Definiamo il file .json di cui sotto, in cui elenchiamo una lista di pazienti ed una lista di esami. Proviamo a far mettere in ascolto json-server sul nostro file tramite il comando indicato in precedenza e vediamo la risposta.

C:\Users\gabrielec\Desktop>json-server --watch db2.json
  \{^_^}/ hi!
  Loading db2.json
  Done
  Resources
  http://localhost:3000/patients
  http://localhost:3000/exams
  Home
  http://localhost:3000
  Type s + enter at any time to create a snapshot of the database 
Watching...

Osserviamo come il tool abbia identificato le due liste nel file json e come le stia trattando come api. Eseguendo una chiamata HTTP (con postman, curl, o con un httpclient nel nostro codice) verso quelle api, il server simulato ci risponderà con dati consoni.

Eseguendo la seguente get su postman, GET /patients/2 HTTP/1.1 Host: localhost:3000, otteniamo un risultato corretto ed una notifica sul command promtp in cui viene evidenziata la chiamata ricevuta, indicando status code e tempi di risposta.

GET /patients/2 200 3.623 ms – 71

Coloro che hanno già un po’ di esperienza in ambito mobile avranno storto il naso leggendo “localhost” come host della chiamata HTTP. Questo perché né i dispositivi mobile, né gli emulatori, riescono a comprendere l’indirizzo di localhost e tantomeno il suo corrispettivo ip 127.0.0.1. Si tratta di un grosso problema se contestualizziamo l’esempio precedente nell’ambito mobile, dunque come fare?
Ci sono vari modi relativamente veloci, da provare sequenzialmente:

  • Cambiare l’host da localhost all’indirizzo ip pubblico della macchina sulla quale è in debug la nostra applicazione. Per fare ciò occorre banalmente eseguire in Command Prompt il comando ipconfig e prendere l’indirizzo ipv4.
C:\Users\gabrielec\Desktop>ipconfig
Windows IP Configuration
Wireless LAN adapter Wi-Fi:
Connection-specific DNS Suffix  . : int.giuneco.com
Link-local IPv6 Address . . . . . : fe80::5832:b9a5:fada:ea1a%13
IPv4 Address. . . . . . . . . . . : 10.111.111.121
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . : 10.111.111.252
  • Se questo non dovesse funzionare, probabilmente a causa di controlli Firewall od impostazioni aziendali della rete locale, è possibile gestire un proxy che permetta di mappare delle chiamate verso un host, da una porta ad un’altra.
    Ci sono vari modi di gestire questo problema ma il più veloce è utilizzare un tool chiamato SharpProxy . Sebbene non sia immediato riuscire ad utilizzare il tool (è necessario scaricare la Solution da GitHub, compilarla in configurazione di Release ed infine eseguire il file eseguibile generato) questo risolverà il problema in maniera veloce ed efficace.
    In particolare, le due porte saranno:
    • Pubblica: la 5000 di default aperta da SharpProxy;
    • Privata: dovrà essere impostata in base a quella scelta da IIS in fase di debugging della WebApi; nel nostro caso sarà la porta di default aperta da Json Server.

Eseguendo, nel nostro caso, chiamate http all’host 10.111.111.121:5000, Json Server ci potrà rispondere.

  • In caso di fallimento ulteriore, possiamo sempre fare appello ad una WebApi sviluppata in locale o pubblicata in ambiente di sviluppo; in particolare per la soluzione in locale si dovrà aprire la porta associata da IIS e modificare il file applicationhost.config (contenuto nella cartella nascosta .vs della solution della WebApi) in modo che la WebApi accetti chiamate dall’esterno. Per fare questo, dovremo cercare il nodo “site” del file .config nominato come il nostro progetto di WebApi e cambiare la binding information nel seguente modo:

Gestione delle risorse grafiche

Affinché sia un buon prodotto, un’applicazione, oltre ad essere funzionale ed efficiente, deve essere anche efficace. L’efficacia, in barba ai poveri sviluppatori, è data in buona parte dalla grafica dell’applicazione. In particolare, nelle applicazioni mobile native, la corretta gestione delle risorse grafiche è fondamentale allo scopo di permettere agli SDK nativi di renderizzarle correttamente sulla maggior parte dei dispositivi fisici esistenti, in base alle relative device metrics .

L’intero framework di Xamarin supporta la gestione nativa di Android e iOS delle risorse locali, dette embedded, tramite un meccanismo di rendering basato su una costante fissa per ogni dispositivo, detta Device Pixel Density, strettamente correlata alla più famosa DPI, Dot per Inch.

In base alla densità dello schermo, i sistemi operativi cercheranno di caricare le immagini di risoluzione più appropriata. Purtroppo questo meccanismo è automatico solo in parte; saremo noi a decidere l’esatto formato delle risorse per tipo di densità di schermo ed ad embeddarle correttamente.

L’embedding di una risorsa grafica in base alla densità dello schermo è molto semplice, in quanto per:

  • Xamarin Android: Visual Studio, in fase di creazione del progetto, aggiunge alla cartella “Resources” delle sottocartelle, rinominate con la seguente regola: drawable-*, dove * sta per la codifica di Android della densità dello schermo, ovvero hdpi, xhdpi, xxhdpi, xxxhdpi (originariamente erano presenti anche le codifiche ldpi e mdpi, ma sono state scartate in quanto fanno riferimento a dispositivi molto vecchi). Dentro queste sottocartelle andranno inserite le varie versioni delle risorse grafiche di interesse, suddivise per risoluzione;
  • Xamarin iOS: Basterà inserire, nella cartella “Resources”, le risorse grafiche con risoluzione adatta, rinominandole secondo la seguente regola: myResource@*, dove * sta per la codifica di iOS della densità dello schermo, ovvero 2x, 3x.

L’esatta dimensione delle risorse grafiche tuttavia non è facile da definire, in quanto sia Google che Apple fanno riferimento solo ad alcuni tipi di risorse, come icone e tab icons, e non dimensioni di background. Con l’esperienza, ho individuato le dimensioni che più si adattavano alle varie situazioni, raggruppandole per tipo di risorsa, come mostrato di seguito.

Possiamo trovare maggiori informazioni sulle risoluzioni in pixel qui:
Apple: https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/app-icon/
Appcelerator: https://docs.appcelerator.com/platform/latest/#!/guide/Icons_and_Splash_Screens-section-src-29004897_IconsandSplashScreens-SplashScreen
Iconhandbook: http://iconhandbook.co.uk/reference/chart/android/

Inoltre, per immagini che devono essere centrate nello schermo, oppure per immagini posizionate in basso o in alto (ancorate al bordo), come calcolare le dimensioni in pixel, se vogliamo produrre immagini customizzate per ogni risoluzione? Dobbiamo effettuare un ragionamento in termini di proporzioni, in modo che larghezza o altezza siano coerenti con i vari schermi.
Facciamo un esempio per quanto riguarda l’ancoraggio in basso: conoscendo la larghezza dello schermo, grazie alla tabella precedente, possiamo applicare una proporzione per l’altezza in modo da avere una serie di immagini che risultino consone con le diverse dimensioni dello schermo. Questo ragionamento è adattabile a tutte quelle immagini di cui dobbiamo calcolare le dimensioni, come mostrato nello schema di seguito: considerando la singola cella come unità di misura, è facilmente calcolabile che, per esempio, l’altezza dell’immagine ancorata in alto è 3/h dove h è l’altezza dello schermo (20 unità di misura nello schema).

Dunque, dopo aver definito tutte le immagini con tutte le dimensioni, a livello di solution otterremo una situazione simile a questa:



Sorge dunque un dubbio: abbiamo sempre disponibile un grafico disposto a fornirci tutte queste risorse grafiche? Non possiamo darlo per scontato.

Un buon compromesso può essere fornito dalle seguenti soluzioni che tuttavia richiedono un piccolo sforzo grafico al fine di definire almeno un unico file per risorsa grafica.


Le soluzioni dipendono dal tipo di file fornitoci:

  • Il file è raster (png, jpg):
    • Se il file è un’immagine di background, può essere salvata tenendo conto delle dimensioni del device da supportare con risoluzione maggiore (ad es. iPhoneX e simili) ed utilizzata in modo che su device più piccoli l’immagine risulti eventualmente ritagliata ma non deformata, come illustrato nell’esempio seguente (dal sito Apple)
  • Il file è vettoriale (svg):
    • Se il file è un’icona, possiamo renderla font tramite la webapp icomoon (https://icomoon.io/). In questo modo riusciamo a gestire l’icona come se fosse una label, potendo scalarla in base al fontsize desiderato, non perdendo qualità dell’immagine;
    • Se il file è un’immagine di background od un pattern, possiamo gestirla direttamente come svg, in modo da non doverci preoccupare di perdere la qualità dell’immagine scalandola. Possiamo gestire questo tipo di file grazie ad una libreria di Daniel Luberda, FFImageLoading (https://github.com/luberda-molinet/FFImageLoading).

Di seguito mostriamo un’immagine che esemplifica le due gestioni, raster e vettoriale: la prima è caricata nativamente da un file .png di dimensioni 128×128 pixel, mentre la seconda è caricata da un file .svg e renderizzata tramite la libreria FFImageLoading. Entrambe le immagini sono forzate ad assumere la massima dimensione possibile all’interno del loro container e, come possiamo vedere, la prima subisce una netta perdita di qualità.

Ovviamente si tratta di un ingrandimento estremo, date le piccole dimensioni della png iniziale presa ad esempio (128×128), che ci serve per chiarire al meglio l’effetto a cui possiamo andare incontro.

Nel caso, invece, la.png iniziale sia già a dimensione massima prevista e venga rimpicciolita secondo necessità, limiteremo l’effetto pixelizzazione ma caricheremo il device di un calcolo in più durante la renderizzazione.

Ricapitoliamo i pro e i contro di queste gestioni.

Il supporto di un vero MVVM

Per chi non sapesse di cosa parliamo, Model-View-ViewModel è un pattern architetturale nato come evoluzione del vecchio pattern Model-View-Presenter , al fine di semplificare ed allo stesso tempo migliorare l’interazione fra vista e logica della libreria client Windows Presentation Foundation (WPF) di Microsoft. In Xamarin, come in WPF, la vista, definita in XAML, è legata ad una classe di code behind, in cui si va a definire l’interazione dei vari controlli grafici con l’utente e la loro logica di risposta. Questo approccio è fortemente sconsigliato ed addirittura viene definito un antipattern, in quanto non permette il disaccoppiamento delle classi (inficiando lo sviluppo e l’integrazione di nuove funzionalità). Inoltre le classi di code behind sono difficilmente testabili. Il pattern MVVM risolve i problemi del code behind delegando la gestione della logica in classi dedicate chiamate ViewModel, facilmente astraibili e testabili.

Il problema è dunque risolto? Non del tutto: ci sono scenari in cui si rende necessaria la scrittura di codice nel code behind, in quanto, tipicamente, il ViewModel non ha un riferimento esplicito alla vista; dunque, tutte quelle funzionalità tipiche della vista, ma invocate da ViewModel, sono necessariamente definite nel code behind. Questo comporta avere porzioni di codice facenti parte di un processo, ma sparpagliate in classi diverse ed accessibili solo tramite l’utilizzo di meccanismi di messaggistica (sebbene integrati in Xamarin).

Un esempio paradossale e fondamentale allo stesso tempo si ha nella gestione del Binding Context e del Data Binding . Il binding in Xamarin è un meccanismo che permette di collegare alcune proprietà dei controlli grafici definiti nello XAML, direttamente con proprietà pubbliche e “notificanti” definite all’interno del ViewModel. Con il termine notificanti si intende proprietà che notificano il loro cambio di stato; ciò è possibile solo se queste fanno parte di una classe che implementa l’interfaccia PropertyChanged.

In Xamarin “vanilla”, il modo più semplice per collegare una vista al suo ViewModel è entrare nel code behind della vista, istanziare il ViewModel ed assegnarlo alla proprietà BindingContext della vista (proprietà ereditata da classi base), dunque almeno una riga di codice nel code behind deve essere inserita. Questo esempio è chiaramente triviale, dato che è possibile definire il binding context della vista direttamente nel suo XAML, ma esemplifica bene che il solo framework Xamarin non è del tutto in grado di gestire un vero MVVM.

Dopo aver spiegato -implicitamente- perché usare MVVM, valutiamo cosa vorremmo che il framework ci offrisse:

  1. Una gestione semplice e veloce del Data Binding;
  2. ViewModel con gestione della navigazione da vista a vista (senza referenziarla esplicitamente);
  3. Controllo dei comportamenti custom e logica di controlli grafici senza passare dal code behind.

Fortunatamente ci vengono in soccorso alcuni framework di supporto a MVVM. Tenendo di conto le tre richieste precedenti, potremmo utilizzare:

  • Fresh MVVM: sviluppato da Michael Ridland, è un framework molto semplice ed intuitivo; offre dei metodi core che permettono la navigazione fra pagine (modali e non), una gestione degli alert, la gestione dello stack di navigazione e metodi di “ingresso” ed “uscita” da pagina a pagina e un’implementazione IoC che permette l’utilizzo della dependency injection e inversion of control, il tutto interamente contenuto all’interno dei ViewModel. Come viene descritto nella documentazione, predilige la convenzione rispetto alla configurazione: per esempio l’associazione della vista con il ViewModel viene effettuata tramite reflection, a patto che venga rispettata la convezione di chiamare le due classi come MyViewPage.xaml e MyViewModelPageModel.xaml;
  • MVVM Cross: Framework nettamente più grande e complicato rispetto al precedente, ma che permette di gestire binding complessi direttamente nello XAML. Si basa sulla configurazione.

La scelta di un framework rispetto ad un altro dipende sicuramente dalla complessità dell’applicazione che si deve implementare e da eventuali funzionalità particolari difficilmente implementabili a mano. Riguardo ai due framework precedenti, il primo non è costantemente manutenuto e possiamo notare che pure essendo snello ha non poche issue aperte su github. Il secondo è supportato da una grossa community ed è tutt’oggi aggiornato.

Conclusioni

Se, in fase preliminare, ci preoccupiamo di gestire in anticipo tutte le potenziali problematiche che possiamo incontrare in corso d’opera (con una corretta gestione delle risorse grafiche, una simulazione dell’architettura scelta più veritiera possibile, ed un framework di supporto che sopperisca a potenziali mancanze della piattaforma di sviluppo scelta) potremo garantirci un buon 20% del lavoro già fatto. Potremo, in questo modo, evitare rallentamenti nello sviluppo, che sempre più di frequente divengono criticità fondamentali in ambito progettuale.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *