Indice
Corso di Android
Primi passi con Android
1. Introduzione: perché Android
Android sta dilagando. Non è più solo questione di smartphone o tablet. Si sta imponendo come sistema operativo in grado di animare qualsiasi dispositivo più o meno mobile tanto da apparire, in prospettiva non troppo futuristica, una presenza sempre più costante nel nostro quotidiano.
Gli è stata attribuita – e probabilmente a ragione – la più veloce diffusione mai vista per un sistema operativo mobile. Ma quali sono i fattori di questo successo e soprattutto perchè tutto ciò sta capitando ad Android? Le motivazioni ipotizzate sono varie e di varia natura. Gran parte del merito è stato attribuito alle sue radici ben salde nel mondo open source. Android, infatti, è figlio di Linux, ha attirato l'interesse di tanti sviluppatori che per anni si sono stretti intorno ai grandi bacini del software libero ed accoglie in sé tutto il meglio di quanto è stato ideato per supportare lo sviluppo del web, desktop e mobile sia in termini di pattern progettuali che di librerie software.
Eppure architettura del sistema ed open source sono aspetti che interessano molto una platea fortemente tecnica come programmatori ed ingegneri informatici. Per decretare un successo tanto ampio è necessario che ci sia anche un forte riscontro di pubblico. Sicuramente un qualcosa che ha contraddistinto questo sistema è stata la sua adozione da parte di dispositivi molto diversi tra loro, non solo per tipologia – smartphone piuttosto che tablet – ma soprattutto per fasce di prezzo, da poche decine di euro fino a cifre piuttosto significative.
Ciò ne ha permesso una diffusione molto diverficata trasversalmente alle diverse categorie sociali, ma ha causato di riflesso una frammentazione notevole dello scenario applicativo costringendo gli sviluppatori ad una particolare cura degli aspetti di adattamento alle caratteristiche del dispositivo ospite. Proprio in questo, Android ha dimostrato la sua grande modernità offrendo tutto il supporto necessario per permettere all'applicazione in esecuzione di adegursi ad ogni circostanza.
Questa guida è dedicata allo sviluppo di applicativi Android. Nella prossime lezioni verrà utilizzata una programmazione in linguaggio Java per realizzare applicazioni in cui non manca niente. Si creeranno interfacce utente interattive, dinamiche e graficamente piacevoli. Si avranno gli strumenti per gestire dati e avviare attività di rete in modo che le proprie app prendano da subito parte a quel grande laboratorio di idee che è Internet e tutti i servizi che ne fanno parte. Ed inoltre si avrà a disposizione multimedialità, funzionalità hardware, comunicazione.
Per iniziare, Android richiede nozioni di programmazione Java, passione, curiosità e nulla più. Infatti gli strumenti che si renderanno necessari, come si vedrà presto, sono totalmente gratuiti. Quindi non ci sono scuse per non cominciare.
2. L'SDK e l'ambiente di sviluppo
Per iniziare a programmare su Android è necessario munirsi innanzitutto dei necessari strumenti software, tutti velocemente reperibili in Internet a costo zero. Necessari sono:
- un JDK, il kit di sviluppo per la tradizionale programmazione Java, visto che questa è la tecnologia con cui realizzeremo i nostri programmi;
- un IDE (ambiente di sviluppo integrato) che includa possibilmente tutti gli strumenti necessari al programmatore. Ne verranno presentati due, le alternative più comuni ed entrambe valide: Eclipse ed Android Studio.
Tra gli strumenti appena citati, di cui a breve verranno illustrate le fasi di download ed installazione, non è stato nominato un elemento fondamentale che merita, però, una menzione speciale: l'Android SDK. Questo è il vero pacchetto di strumenti che ci permetterà di vedere realizzati i nostri programmi per Android. Nonostante l'importanza fondamentale rivestita, il suo utilizzo, inizialmente, può passare un po' inosservato visto che normalmente viene scaricato insieme agli IDE più comuni, Eclipse e Android Studio. Per questo non mancheremo di sottolinearne sin da ora la sua struttura e le funzionalità che ne fanno parte.
Iniziamo, se non o si è già installato, ad insallare Java. È necessario recarsi presso il sito Oracle e scaricarne una versione per il proprio sistema operativo, specificando non solo la tipologia – Windows, Linux, Mac OS o Solaris – ma anche la versione, 32 o, meglio se se ne ha la possibilità, 64 bit. Una volta eseguito lo scaricamento del pacchetto se ne procede all'installazione che non presenta grandi difficoltà in alcuno dei sistemi per cui è disponibile.
Maggiore interesse riveste la scelta dell'IDE. Come detto, le due alternative più praticabili al momento sono Eclipse e Android Studio.
Eclipse
Eclipse è uno strumento gratuito e molto flessibile, ben noto da tanti anni a varie comunità di sviluppatori. In particolare, la sua natura modulare l'ha reso molto ricco di funzionalità mediante vari plug-in installabili al suo interno oltre che utilizzabile nella programmazione con vari linguaggi, primo tra tutti Java, ma anche C/C++, PHP ed altro ancora. Nel caso di Android è disponibile un bundle ossia un pacchetto completo già dotato degli ADT (Android Developer Tools) e con Android SDK già disponibile. Il link da cui è possibile raggiungerlo è https://developer.android.com/sdk/index.html. La pagina che ci accoglie spiega subito che con un singolo download avremo a disposizione Eclipse, gli ADT e Android SDK già pronti per essere usati. Le versioni disponibili, come per il JDK, si riferiscono ad ogni possibile sistema operativo. Dopo il download – di un pacchetto piuttosto corposo di circa 500 MB – è sufficiente procederne alla decompressione.
Android Studio
Un'alternativa giovane (neanche un anno di vita) ma molto promettente è Android Studio, sponsorizzato direttamente da Google e pensato appositamente per Android. Si può ottenere dal seguente link http://developer.android.com/sdk/installing/studio.html e anch'esso è già inclusivo di Android SDK. Nonostante questo interessantissimo strumento non sia ancora arrivato alla versione 1.0 dimostra di accogliere volentieri tutto ciò che di più utile può esserci per lo sviluppatore Android. Gli elementi di Android Studio che spiccano maggiormente sono:
- l'utilizzo di Gradle come strumento di build automation, atto quindi ad accompagnare lo sviluppatore nelle fasi di build, sviluppo, test e pubblicazione della propria app;
- disponibilità di un gran numero di template per la realizzazione di applicazioni già in linea con i più comuni pattern progettuali;
- editor grafico per la realizzazione di layout, molto pratico e dotato di un ottimo strumento di anteprima in grado di mostrare l'aspetto finale dell'interfaccia che si sta realizzando in una molteplicità di configurazioni (tablet e smartphone di vario tipo).
Android SDK
Ultima nota, diamo uno sguardo più ravvicinato all'Android SDK. Come detto esso viene già reperito congiuntamente all'installazione di Android Studio o di Eclipse e sarà reperibile, in entrambi i casi, aprendo la cartella contenente l'installazione dell'IDE. Un aspetto molto importante è che questo SDK è costituito da molti strumenti – programmi, emulatori, piattaforme per ogni versione di Android e molto altro – la cui composizione non è immutabile ma viene gestita tramite il programma Android SDK Manager, avviabile sia da Eclipse che da Android Studio. Grazie al Manager, il programmatore potrà profilare le piattaforme e gli strumenti presenti nel SDK nella maniera più congeniale al proprio lavoro. Maggiori dettagli in merito verranno via via presentati nel corso delle successive sezioni della guida.
Un paio di download, qualche click per installare e scompattare: indipendentemente dal sistema operativo del proprio PC, l'ambiente per lo sviluppo su Android non necessità di grandi operazioni per essere pronto all'utilizzo e alla realizzazione della prima app.
3. Alternative allo sviluppo nativo
L'approccio alla programmazione Android appare del tutto agevole soprattutto grazie a due aspetti fondamentali già evidenziati: strumenti del tutto gratuiti e semplicità nell'apprestamento dell'ambiente di sviluppo. Ed in effetti è così. A volte però l'appassionato di tecnologia che si avvicina a questo mondo rischia di scoraggiarsi facilmente.
Ammesso che si possegga le skill necessarie del linguaggio Java, ci si accorge presto che un'infarinatura di sintassi spesso non è sufficiente. Per sfruttare degnamente le possibilità offerte dal framework è necessario essere dei buoni programmatori, consci delle principali problematiche da tenere sott'occhio in uno sviluppo professionale: ottimizzazione delle prestazioni, salvaguardia delle risorse a disposizione e via dicendo.
Ma allora che fare se non ci si sente attratti dalla programmazione di questo tipo, cosiddetta nativa, e non si vuole comunque rinunciare all'idea di vedere pubblicate le proprie app?
Alternative ce ne sono e consistono in strumenti – comunque validi – per lo sviluppo non nativo, dall'approccio più visuale e spesso familiare a chi proviene dal web design.
Eccone alcuni:
- Apache Cordova e Steroids scaturiscono dal know-how nello sviluppo per Internet. In effetti: se CSS ha reso il Web elegante, Javascript gli ha dato vitalità e HTML5 l'ha rivoluzionato, perché non riproporre questi strumenti nello sviluppo mobile? Cordova è la versione open source del progetto PhoneGap e serve a realizzare le cosiddette app ibride con un'interfaccia realizzata in modalità web ma in grado di interfacciarsi con il sistema operativo mobile mediante un vasto numero di API. Steroids nasce per superare alcuni limiti riscontrati in PhoneGap ma senza “reinventare la ruota”, si basa infatti su Cordova ma approfondisce il legame con lo strato nativo;
- Corona SDK è un ambiente particolarmente versato alla gestione dell'interazione come per i videogiochi. Creato dai Corona Labs non necessità di alcuna conoscenza del linguaggio Java e propone come alternativa lo scripting in LUA, un formalismo dall'approccio molto semplice che permette di personalizzare maggiormente le proprie applicazioni;
- Unity è il più diffuso motore per videogiochi del mondo. Il primato che gli spetta è pienamente meritato in quanto combina editor visuale di altissimo livello ma anche programmazione in tecnologie ad oggetti avanzate come C# oltre che gestione di altri aspetti fondamentali per i videogiochi come grafica, animazione e fisica;
- AppInventor è probabilmente la possibilità più abbordabile per l'appassionato di tecnologia a digiuno di programmazione e che desidera qualche risultato piuttosto rapido. E' stato creato dai Google Labs come strumento per la rapida modellazione di app Android ed è stato successivamente ceduto al prestigioso M.I.T.. Intuitività e semplicità le sue chiavi di lettura principali.
Nativo sì, nativo no. Qual è l'approccio migliore? Sicuramente entrambi hanno i loro pro e contro. Mentre da un lato il nativo offre la possibilità di una gestione totale del dispositivo senza la paura di trovare limiti, d'altra parte richiede spesso una programmazione molto professionale e si concentra esclusivamente su una piattaforma impedendo un'agile riciclo dei propri sforzi su altri mercati del mobile.
Il non-nativo – anche se è impossibile generalizzare data la diversità degli ambienti appena citati – offre vantaggi vari, ascrivibili a volte ad una minore necessità di programmare e molto spesso alla possibilità di creare applicazioni cross-platform distribuibili su sistemi operativi diversi.
4. Gli elementi e il funzionamento di base di un'applicazione
Ogni applicazione Android, indipendentemente dalla finalità che si prefigge, affida le sue funzionalità a quattro tipi di componenti. Si tratta di Activity, Service, Content Provider e BroadcastReceiver ed esistono affinchè la nostra applicazione possa integrarsi alla perfezione nell'ecosistema Android.
Prima di addentrarci nella spiegazione di ognuna di esse, è utile concentrarsi un attimo su due principi ispiratori che, tra gli altri, sono alla base della maggior parte delle scelte progettuali operate dai creatori di Android. Tenerli a mente ci permetterà di comprendere meglio ciò che del sistema verrà illustrato nei prossimi capitoli:
- la salvaguardia delle risorse: essendo progettato per sistemi embedded, storicamente dotati di poche risorse di memoria, Android ha avuto sin da subito uno spirito parsimonioso. Vedremo che, senza far perdere fluidità alla user-experience, Android è particolarmente bravo nel distruggere e ricreare parti dell'applicazione in maniera del tutto impercettibile all'utente. Chi dovrà fronteggiare questo atteggiamento sarà il programmatore, ovviamente. Per fortuna, ciò non costerà grandi fatiche ma solo particolare cura nel prendere determinati accorgimenti da applicare con la necessaria consapevolezza. Vale la pena sottolineare che quando si parla di esiguità di risorse in Android, l'obiezione mossa più comunemente ruota attorno alla recente commercializzazione di smartphone che possono contare su 2 GB di memoria RAM. Ciò è vero ma non bisogna dimenticare che Android si propone lo scopo di animare qualunque dispositivo in cui riesca a vivere. Il sistema vincerà quindi la sfida di sopravvivenza solo se saprà adattarsi anche a contesti che offrono condizioni molto più disagiate di quelle che può prospettare un nuovissimo device Samsung;
- sicurezza: Android è figlio di Linux, come già ricordato, quindi ha nel DNA la ricerca della stabilità. Ogni applicazione è un utente a sé stante e vive in un proprio processo in cui viene allocata una nuova istanza della virtual machine, ciò per evitare che il crash di un'applicazione propaghi instabilità alle altre app in esecuzione. Questa forma di “isolamento” viene riflessa anche sulla memoria di massa in quanto ogni applicazione ha un suo spazio in cui lavorare e custodire i propri dati. In merito, è assolutamente sconsigliata, per non dire vietata, qualsiasi pratica che porti un'app ad invadere lo spazio riservato ad un'altra. Nonostante ciò le nostre applicazioni non sono costrette a vivere in assenza di comunicazione tra loro, anzi Android favorisce un dialogo “sano” tra di esse mettendo a disposizione meccanismi agevoli per la condivisione di contenuti e funzionalità tra componenti del sistema.
È arrivato il momento quindi di presentare più da vicino i blocchi costitutivi di un'applicazione.
Le fondamenta di Android
Un'Activity è un'interfaccia utente. Ogni volta che si usa un'app generalmente si interagisce con una o più “pagine” mediante le quali si consultano dati o si immettono input. Ovviamente la realizzazione di Activity è il punto di partenza di ogni corso di programmazione Android visto che è il componente con cui l'utente ha il contatto più diretto.
Un Service svolge un ruolo, se vogliamo, opposto all'Activity. Infatti rappresenta un lavoro – generalemente lungo e continuato – che viene svolto interamente in background senza bisogno di interazione diretta con l'utente. I Service hanno un'importanza basilare nella programmazione proprio perchè spesso preparano i dati che le activity devono mostrare all'utente permettendo una reattività maggiore nel momento della visualizzazione.
Un Content Provider nasce con lo scopo della condivisione di dati tra applicazioni. La sua finalità richiama quel principio di sicurezza dell'applicazione di cui si è trattato poco fa. Questi componenti permettono di condividere, nell'ambito del sistema, contenuti custoditi in un database, su file o reperibili mediante accessi in Rete. Tali contenuti potranno essere usati da altre applicazioni senza invadere lo spazio di memoria ma stabilendo quel dialogo “sano” cui si è accennato
Un Broadcast Receiver è un componente che reagisce ad un invio di messaggi a livello di sistema – appunto in broadcast – con cui Android notifica l'avvenimento di un determinato evento, ad esempio l'arrivo di un SMS o di una chiamata o sollecita l'esecuzione di azioni. Questi componenti come si può immaginare sono particolarmente utili per la gestione istantanea di determinate circostanze speciali.
Molto importante ricordare che una componente può attivarne un'altra mediante apposite invocazioni di sistema. Questa intenzione viene codificata con un Intent utilizzabile come normale classe Java ma che sottintende un potentissimo strumento di comunicazione di Android. Anche degli Intent faremo uso sin dai prossimi articoli.
5. Il ciclo di vita di un'app Android
Android sa che il fattore fondamentale della sopravvivenza di un sistema mobile è la corretta gestione delle risorse. Pensiamo ad uno smartphone: è un dispositivo che fa una vita difficile al giorno d'oggi. Non solo si occupa di chiamate ed SMS, ma offre pagine web, giochi, comunicazione sui “social” per molto tempo ogni giorno. Inoltre, capita sempre più spesso che non venga mai spento impedendo così una fase molto comune nella vita dei PC: l'arresto del sistema con conseguente liberazione della memoria e pulizia di risorse temporanee assegnate.
Android farà in modo di tenere in vita ogni processo il più a lungo possibile. Ciò non toglie che in alcune circostanze ed in base alle risorse hardware a disposizione, il sistema operativo si troverà nella necessità di dover liberare memoria abbattendo processi.
Sì ma: quale processo abbattere? La discriminante è quanto un'applicazione, candidata all'eliminazione, sia importante per la user experience. Maggiore sarà l'importanza riconosciuta, minori saranno le probabilità che venga arrestata.
Così facendo Android tenterà di raggiungere il suo duplice scopo: preservare il sistema e salvaguardare l'utente.
I processi possono essere classificati, in ordine di importanza decrescente, come:
- Processi in “foreground”: sono quelli che interagiscono direttamente o indirettamente con l'utente. Stiamo parlando delle applicazioni che, ad esempio, contengono l'Activity attualmente utilizzata o i Service ad essa collegati. Questi sono i processi che Android tenterà di preservare maggiormente. Importante notare che, comunque, anche le applicazioni in foreground non sono del tutto al sicuro. Se ad esempio il sistema non disponesse di risorse sufficienti a mantenerli tutti in vita, si troverebbe costretto ad arrestarne qualcuno;
- Processi visibili: non sono importanti come quelli in foreground ma vengono anch'essi grandemente tutelati da Android. Infatti, avendo componenti ancora visibili all'utente anche se non vi interagiscono più, svolgono comunque un ruolo particolarmente critico. Anche in questo caso si tratta di Activity visibili e Service ad esse collegati;
- Processi “service”: contengono dei service in esecuzione che generalmente svolgono lavori molto utili all'utente anche se non direttamente collegati con ciò che egli vede nel display. Il loro livello di priorità può essere considerato medio: importanti sì ma non tanto quanto i processi di cui ai precedenti due punti;
- Processi in “background”: contengono activity non più visibili all'utente. Questa è una categoria solitamente molto affollata composta dal gran numero di applicazioni che l'utente ha usato e messo poi in disparte, ad esempio premendo il tasto Home. Non sono considerati molto importanti e sono dei buoni candidati all'eliminazione in caso di scarsità di risorse;
- Processi “empty”: sono praticamente vuoti nel senso che non hanno alcuna componente di sistema attiva. Vengono conservati solo per motivi di cache, per velocizzare la loro riattivazione qualora si rendesse necessaria. Come ovvio, sono i candidati “numero 1” all'eliminazione da parte del sistema operativo.
Quando, nel corso della guida, esamineremo in dettaglio la realizzazione delle varie componenti di sistema – Activity, Service, ContentProvider e BroadcastReceiver come accennato nel capitolo precedente – vedremo come l'utente alla luce di quanto appena discusso sarà in grado di comprendere il reale funzionamento delle proprie applicazioni e di come esse vengano gestite dal sistema in ogni circostanza, più o meno favorevole.
6. Hello World: creare un'app Android
A questo punto, la filosofia del sistema è stata introdotta, gli strumenti necessari illustrati non resta altro da fare che partire con il primo progetto.
Lo scopo di questa lezione non è tanto quello di scrivere un vero e proprio programma quanto quello di farci accompagnare dall'IDE nella creazione di un progetto Android per poterne vedere la struttura, innanzitutto, e mandarlo in esecuzione in modo da verificare la corretta preparazione della nostra macchina di sviluppo.
Prendiamo contatto con l'IDE: creazione di un progetto
Probabilmente il programmatore novizio di Android sarà già più che svezzato nel mondo Java quindi non dovrebbe avere problemi ad orientarsi in Eclipse. Comunque, riepilogando, per poter testare la propria macchina di sviluppo è necessario innanzitutto creare un nuovo progetto Android:
- assicuriamoci di avere aperto la Perspective di Eclipse relativa a Java (e non a Java EE né a nessun altro tipo di framework). Per farlo, andiamo sul menu Window ? Open Perspective. Se viene visualizzata la voce Java, selezioniamola; altrimenti possiamo procedere con il punto seguente;
- muovendoci nel menu File invochiamo New ? Android Application Project;
- seguiamo la procedura di creazione guidata. La serie di schermate (circa 4 o 5) che si susseguono ci chiede di inserire una serie di impostazioni. L'unica assolutamente obbligatoria è il nome dell'applicazione. Diciamo che il nome scelto per questa prima applicazione sia – nemmeno a dirlo – HelloWorld, tutte le restanti impostazioni al momento possono essere lasciate con i valori di default, saranno comunque modificabili successivamente. Si può concludere la creazione del progetto selezionando sempre il pulsante Next finché non diventerà attivo quello denominato Finish.
L'architettura di progetto così impostata è costituita da un certo numero di file e cartelle.
Tutti sono importanti ma gli elementi tra i quali il programmatore dovrà sapersi muovere al più presto con scioltezza sono:
- la cartella src che conterrà tutto il codice Java che scriveremo;
- la cartella res in cui risiederanno le cosiddette risorse dell'applicazione per la maggior parte configurate in XML ma non solo;
- il file AndroidManifest.xml anch'esso in XML che custodirà configurazioni e ruoli dei componenti della nostra app.
Un progetto creato in questa maniera da Eclipse è funzionante, sebbene non contenga nessuna funzionalità particolare. Al momento, quindi, non modifichiamo nulla e passiamo subito al suo avvio immediato.
Esecuzione del progetto: dispositivo emulato o reale?
Per eseguire il test è necessario che si abbia a disposizione un dispositivo Android attivo. Può trattarsi di un dispositivo reale – tipicamente smartphone o tablet collegato via USB – o di un sistema emulato (tecnicamente un AVD, Android Virtual Device) mediante gli strumenti messi a disposizione da Android SDK.
Partiamo da questo secondo caso. Nel menu Window (attenzione, per vederlo è necessario trovarsi nella prospettiva Java e non Debug) sono disponibili due voci importantissime: “Android SDK Manager” e “Android Virtual Device Manager”. Il primo serve a profilare il nostro SDK richiedendo lo scaricamento di versioni di Android per le quali vogliamo sviluppare o strumenti aggiuntivi come l'utilissimo HAXM di cui si è parlato in un articolo presente nella sezione Mobile. Per il momento la configurazione di un SDK come lo troviamo in un pacchetto Eclipse appena scaricato va benissimo.
Il secondo strumento, Android Virtual Device Manager, è ciò che ci serve per preparare un emulatore, seguendo questi passi:
- cliccare sulla voce Window ? Android Virtual Device Manager;
- nell'interfaccia che si apre (vedere in figura) c'è un'area che ospiterà l'elenco degli emulatori che creeremo. Alla sua destra cliccare il pulsante New.;
- la nuova finestra che si apre, mostrata in figura, permette di configurare un dispositivo emulato semplicemente assegnandogli un nome e la versione di Android che si vuole che esegua oltre ad una serie di impostazioni ulteriori;
- tornando alla finestra presentata al precedente punto 2. dovremmo vedere il nostro emulatore elencato nell'area bianca. Non resta che selezionarlo e avviarlo cliccando il pulsante Start.
Se si vuole utilizzare un dispositivo reale via USB non è richiesto apportare modifiche in Eclipse. Usando Windows sono solitamente necessari dei driver reperiti direttamente dal sistema operativo o scaricati appositamente dal programmatore. Qualora, al contrario, si usasse Linux non è richiesta l'installazione di alcun driver, macchina di sviluppo e Android si interfacceranno direttamente.
Lancio dell'applicazione
Dopo il boot del sistema emulato, potremo lanciare la nostra applicazione che verrà eseguita direttamente sul dispositivo. Ciò può essere fatto in modalità Run o Debug utilizzando uno dei mezzi messi a disposizione dall'IDE (voci nel menu Run, combinazione di tasti o pulsanti sulla barra degli strumenti).
Il risultato dell'esecuzione – visibile in figura – è molto semplice.
Consiste nella sola apparizione della stringa “Hello world!”. Non è molto ma certifica il raggiungimento dei nostri obiettivi: la macchina di sviluppo è pronta per mettersi al lavoro e, seconda cosa, l'impianto di progetto che abbiamo ora a disposizione è funzionante e può essere usato come base per sperimentare tutto ciò che impareremo.
Il cuore di un'app Android
7. Activity, la "prima pagina" dell'applicazione
Il progetto approntato nel capitolo precedente con l'aiuto dell'IDE può essere ora analizzato nel dettaglio. Lo scopo che ci prefiggiamo è quello di osservare da vicino come è fatta un'Activity, il primo dei quattro componenti basilari che troviamo nelle applicazioni Android.
Nel nostro progetto ce n'è una ed è l'interfaccia utente che mostra il messaggio “Hello World”. Nonostante la sua semplicità, mette in luce un aspetto fondamentale. Per creare un'Activity è necessario fare due cose:
- estendere la classe Activity, appartenente al framework Android;
- registrare l'Activity nell'AndroidManifest.xml mediante l'uso dell'apposito tag XML <activity>. Tra l'altro, questo dettame vale per tutte le quattro componenti fondamentali di un'applicazione.
L'Activity nel codice Java
Il codice Java che realizza l'Activity risiede nella cartella src, come spiegato in precedenza. Il contenuto di un tipico “Hello world” potrebbe essere questo:
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
La classe si chiama MainActivity ed estende Activity. Al suo interno viene implementato l'override del metodo onCreate. Per il momento, ci accontentiamo di sapere che questo metodo viene invocato alla creazione dell'Activity. Più avanti scopriremo che si tratta di una tappa fondamentale del ciclo di vita di questo tipo di componenti.
A proposito delle due righe di codice presenti all'interno dell'onCreate:
super.onCreate(savedInstanceState): invoca il metodo omonimo della classe base. Questa operazione è assolutamente obbligatoria;setContentView(R.layout.activity_main): specifica quale sarà il “volto” dell'Activity, il suo layout. Al momento la dicitura R.layout.activity_main può apparire alquanto misteriosa ma lo sarà ancora per poco, fino al momento in cui verrà illustrato l'uso delle risorse. Il suo effetto è quello di imporre come struttura grafica dell'Activity il contenuto del file activity_main.xml presente nella cartella res/layout.
L'Activity nel file Manifest
Il file AndroidManifest.xml che configura questa applicazione appare così:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="esempi.android.helloworld" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Il nodo <application> contiene le componenti usate nell'applicazione. In questo caso, c'è un nodo <activity> che con l'attributo android:name specifica il nome della classe Java che incarna l'Activity. Se, come in questo caso, non viene specificato un package è sottintesa l'appartenenza della classe al package riportato nel nodo <manifest>, la root del file.
Il costrutto intent-filter all'interno serve ad indicare che questa activity è la main activity del progetto, in pratica l'interfaccia che accoglierà l'utente all'ingresso nell'applicazione.
E se volessimo altre activity? È possibile averne? Certamente.
L'importante è che ogni activity venga prodotta seguendo i due passi definiti all'inizio di questa sezione: (1) creare una classe Java che estenda Activity e (2) definire un nodo <activity> in AndroidManifest.xml che riporti nell'attributo android:name il nome della classe corrispondente. A proposito di questo, il sottonodo intent-filter riferito all'action MAIN va indicato solo nell'Activity principale del progetto.
L'utente passerà da un'activity all'altra in maniera simile a come è abituato a fare tra le pagine dei siti Internet ma qui il tutto avverrà mediante il potente meccanismo degli Intent spiegato a breve.
8. Il ciclo di vita di un'Activity
Una delle più note illustrazioni della programmazione Android è questa:
La si può trovare sulla documentazione ufficiale, nelle pagine in cui viene spiegato il ciclo di vita di un'Activity. Si tratta di una serie di stati attraverso i quali l'esistenza dell'Activity passa. In particolare, nell'illustrazione riportata, gli stati sono rappresentati dalle figure colorate. L'ingresso o l'uscita da uno di questi stati viene notificato con l'invocazione di un metodo di callback da parte del sistema. Il codice inserito in tali metodi dovrà essere allineato con la finalità del metodo stesso affinchè l'app possa essere “un buon cittadino” dell'ecosistema Android.
Ad esempio, il primo metodo di callback che viene raffigurato è onCreate() ed è proprio l'onCreate di cui abbiamo fatto l'override nell'implementazione dell'Activity vista nei capitoli precedenti.
Quando un'activity va in esecuzione per interagire direttamente con l'utente vengono obbligatoriamente invocati tre metodi:
- onCreate: l'activity viene creata. Il programmatore deve assegnare le configurazioni di base e definire quale sarà il layout dell'interfaccia;
- onStart: l'activity diventa visibile. È il momento in cui si possono attivare funzionalità e servizi che devono offrire informazioni all'utente;
- onResume: l'activity diventa la destinataria di tutti gli input dell'utente.
Android pone a riposo l'activity nel momento in cui l'utente sposta la sua attenzione su un'altra attività del sistema, ad esempio apre un'applicazione diversa, riceve una telefonata o semplicemente – anche nell'ambito della stessa applicazione – viene attivata un'altra Activity. Anche questo percorso, passa per tre metodi di callback:
- onPause (l'inverso di onResume) notifica la cessata interazione dell'utente con l'activity;
- onStop (contraltare di onStart) segna la fine della visibilità dell'activity;
- onDestroy (contrapposto a onCreate) segna la distruzione dell'activity.
Nel seguito della guida verranno offerti casi pratici di utilizzo ma per il momento ci teniamo su una linea piuttosto teorica. Intanto si consideri che i metodi di callback sono concepiti a coppie (un metodo di avvio con un metodo di arresto: onCreate-onDestroy, onStart-onStop, onResume-onPause) e solitamente il lavoro fatto nel metodo di avvio – in termini di funzionalità attivate e risorse allocate – verrà annullato nel corrispondente metodo di arresto.
La prima situazione che si mostrerà favorevole ad illustrare l'invocazione dei più importanti metodi di callback sarà l'uso degli Intent per passare da un'activity all'altra. Si tratta, in fin dei conti, di una pratica comunissima nella programmazione Android.
9. Gestire le risorse e gli asset
Nelle applicazioni Android il codice Java richiama spesso degli elementi interni al progetto come file XML, stringhe, numeri, immagini ed altro ancora. Il modo migliore per conservare tutti questi “valori” a disposizione dell'applicazione è collocarli all'interno della cartella di progetto denominata res e gestirli mediante l'apposito meccanismo delle risorse.
Il capitolo che inizia si occupa proprio di questo: comprendere bene come gestire le risorse di un'applicazione e come utilizzarle richiamandole nel codice. In conclusione, si vedrà un altro meccanismo di gestione di file all'interno del progetto: gli assets.
Dove si trovano le risorse
Se si dà uno sguardo ad un qualsiasi progetto per Android si può vedere che res conta diverse sottocartelle i cui nomi non sono affatto casuali.
Tra quelle di più comune utilizzo, troviamo:
- layout: conterrà l'architettura grafica dei componenti dell'interfaccia utente. Sono definiti in XML in una maniera simile a come viene usato HTML per strutturare le pagine web;
- values: conterrà stringhe, colori, dimensioni e altre tipologie di valori che potranno essere usate in ulteriori risorse o nel codice Java. Importante notare che questi valori costituiranno il contenuto di appositi tag XML (
<string>,<dimen>, etc.) raggruppati in files dal nome solitamente indicativo: strings.xml, dimens.xml, colors.xml e via dicendo. Tali nomi sono frutto di pura convenzione ma il programmatore può scegliere liberamente come chiamarli; - drawable: sono immagini nei formati più comuni o, cosa che può stupire, configurate in XML.
Come richiamare le risorse
Le risorse vengono compilate in un formato binario ed indicizzate mediante un ID univoco. Tali ID sono conservati in una classe Java, di nome R, autogenerata ad ogni modifica e visibile nella cartella gen del progetto Android. Abbiamo già incontrato la classe R nell'Activity esaminata in precedenza.
Il codice:
setContentView(R.layout.activity_main);
indicava che il layout dell'activity era collocato tra le risorse. In particolare, ogni percorso interno alla classe R rispecchia una collocazione di risorse nelle sottocartelle di res come viene illustrato in figura.
Mediante i loro identificativi, le risorse sono accessibili sia da codice Java che da altre risorse definite in XML:
- in Java: tramite
R.tipo_risorsa.nome_risorsa; - in XML:
@tipo_risorsa/nome_risorsa.
Ad esempio, la risorsa di tipo stringa e nome appname:
<string android:name="appname">Hello world!</string>
potrà essere recuperata, in Java, mediante R.string.appname o dall'interno di altre risorse XML con @string/appname.
Adattamento multipiattaforma delle applicazioni
La frammentazione dello scenario hardware nel mondo Android resta uno degli scogli più ardui da superare per il programmatore. In questo le risorse giocano un ruolo molto importante. Osservando un tipico progetto Android si può vedere che tra le cartelle interne a res (figura sotto) ne appaiono alcune con nomi “canonici” (menu, values, layout) e altre con nomi “modificati” (drawable-hdpi, drawable-mdpi ma anche values-v14, values-v11, etc.).
Questo perchè al nome della cartella si può accodare un suffisso che rappresenta la configurazione del dispositivo con cui potranno essere richiamate le risorse contenute.
Se, ad esempio, res/layout conterrà la struttura grafica delle varie interfacce per una qualsiasi configurazione, res/layout-land conterrà layout utilizzabili solo quando il dispositivo è in posizione landscape. Altri modificatori di una certa rilevanza sono quelli che si riferiscono alla lingua del dispositivo: values-it saranno le risorse per dispositivi in italiano, values-en per quelli in inglese. Di modificatori esiste una collezione grandissima, tutta disponibile sulla documentazione ufficiale.
Un discorso a parte meritano le immagini. I modificatori applicati alle cartelle drawable (ldpi, mdpi, hdpi e via dicendo) sono alcune delle sigle che identificano le densità dei display. Questo concetto di densità rappresenta la quantità di pixel per area fisica dello schermo. Programmando per Android è bene abbandonare l'abitudine di misurare in pixel, utilizzando come unità di misura degli elementi grafici i dp (Density-Indipendent Pixel), una specie di pixel “virtuale” indipendente dalla densità del display che permette di mantenere intatte le proporzioni tra gli elementi del layout al variare delle densità.
Gli assets
La documentazione ufficiale elenca tutte le tipologie di risorse che possono essere usate. Esiste anche un tipo di risorsa “grezza” collocabile nella cartella res/raw. Vi si potrà collocare tutto ciò che non si riesce ad inquadrare in una particolare tipologia. In alternativa alle risorse raw, si possono definire gli assets. Questi esulano dal meccanismo delle risorse e vanno depositati nell'omonima cartella di progetto. Non vengono né compilati in formato binario né etichettati con un ID univoco. La loro fruizione da parte dell'applicazione avverrà mediante uno stream che potrà essere richiesto ad una classe Java di nome AssetManager.
10. Intent e messaggi
Quando si è parlato delle componenti che rappresentano i blocchi costitutivi di un'app, si è accennato al ruolo degli Intent. Approfondendo il discorso possiamo dire che rappresentano una forma di messaggistica gestita dal sistema operativo con cui una componente può richiedere l'esecuzione di un'azione da parte di un'altra componente.
Sono uno strumento molto duttile anche se gli utilizzi più comuni ricadono in queste tre casistiche:
- avviare un'Activity;
- avviare un Service;
- inviare un messaggio in broadcast che può essere ricevuto da ogni applicazione.
Gli Extras
Un altro aspetto molto utile degli Intent è che essi, nel recapitare questo messaggio, hanno a disposizione una specie di “bagagliaio”, in cui custodiscono dati che possono essere letti dal destinatario. Questi valori condivisi mediante Intent vengono generalmente chiamati Extras e possono essere di varie tipologie, sia appartenenti a classi più comuni che ad altre purchè serializzabili. La gestione degli Extras negli Intent funziona in maniera simile ad una struttura dati a mappa: con dei metodi put viene inserito un valore etichettato con una chiave e con i corrispondenti metodi get viene prelevato il valore, richiedendolo mediante la chiave di riconoscimento.
L'esempio: un form di login
Prendiamo a titolo di esempio il caso più comune, solitamente utilizzato presto dal programmatore Android neofita, l'attivazione di un'Activity da parte di un'altra. Osserviamo quanto appare in figura:
Abbiamo due activity:
- MainActivity contiene un semplice form di login. Dopo aver inserito username e password viene controllata la validità dei dati ed in caso positivo viene invocata l'apertura di un'altra activtiy;
- SecretActivity è l'area accessibile solo mediante login e contiene – immaginiamo – dati riservati.
All'ingresso della seconda Activity, l'applicazione vuole dare il benvenuto all'utente ma per farlo ha bisogno di sapere come si chiama.
Tutto ciò che serve è già incluso nel meccanismo degli Intent.
A livello di codice, nella MainActivity, una volta ottenuto il successo nel login troveremo:
Intent i=new Intent(this,SecretActivity.class); i.putExtra("username", account_username); startActivity(i);
Le tre operazioni rappresentano:
- dichiarazione dell'Intent come normale oggetto Java. In questo caso avremo un cosiddetto intent esplicito in quanto appare chiaramente il nome della classe che verrà invocata;
- viene inserito, tra gli Extras, una String, la variabile
account_username, che sarà trasportata con l'Intent fino a destinazione ossia la classe SecretActivity. Lo scopo è inserire in questa stringa il nome dell'utente che ha effettuato il login. Notare che l'extra viene etichettato con una chiave, in questo caso “username”. Ciò perchè possono essere trasportati più Extras per ogni Intent e dovrà essere possibile distinguerli per utilizzarli; - infine il metodo
startActivitydimostra quale azione vogliamo attivare con questo Intent, si tratta, in questo caso, dell'avvio di un'Activity.
Nel metodo onCreate della seconda Activity, quella con il contenuto riservato, troveremo le seguenti righe:
Intent i=getIntent(); String username=i.getStringExtra("username");
Notiamo subito che l'Activity attivata si trova a disposizione, mediante getIntent(), l'Intent che ne ha provocato l'attivazione. Lo può utilizzare per recuperare la stringa passata, contenente il nome utente.
A livello di ciclo di vita, che succede?
Il passaggio da un'Activity ad un'altra coinvolge i cicli di vita di entrambe. La prima, quella messa a riposo, dovrà passare almeno per onPause (cessazione interazione con l'utente) e onStop (activity non più visibile) mentre la seconda percorrerà la catena di creazione onCreate-onStart-onResume.
Ma in che ordine avverrà tutto ciò? La priorità del sistema è il mantenimento della fluidità della user-experience. Per questo la consecutio delle operazioni sarà:
- la prima Activity passa per onPause e viene fermata in stato Paused;
- la seconda Activity va in Running venendo attivata completamente. In tale maniera l'utente potrà usarla al più presto non subendo tempi di ritardo;
- a questo punto, mentre l'utente sta già usando la seconda Activity, il sistema può invocare onStop sulla prima.
L'interfaccia grafica (GUI)
11. Il layout di un'app Android
Un'Activity ha bisogno di un volto, di un suo aspetto grafico. Sempre. Anche nei casi più semplici, come quando si limita a stampare la stringa “Hello World!”.
La struttura grafica di un'Activity prende il nome di Layout ed è una delle prime competenze di cui ha bisogno un neo-programmatore Android.
Abbiamo già incontrato i layout nel corso di questa guida. È successo quando si è parlato della prima Activity, ma anche quando si è illustrato l'organizzazione delle risorse. Ora è arrivato il momento di entrare nel vivo del discorso scoprendone le tipologie più comuni e analizzandole sia in termini di caratteristiche che di finalità.
In Android, comunemente un layout viene progettato in XML, in una modalità che ricorda molto l'uso di HTML per le pagine web. Ciò è particolarmente apprezzato da tutti quei programmatori che provengono da esperienze professionali o percorsi didattici nel settore.
Gli IDE offrono strumenti visuali per disegnare layout con approccio drag-and-drop e visualizzazioni di anteprima molto utili. Nonostante questi strumenti, nel tempo, siano diventati sempre più usabili ed intuitivi, la conoscenza della sintassi XML per le UI e le corrispondenti classi Java restano un fattore imprescindibile.
Tipi di Layout
Nel framework Android sono stati definiti vari tipi di layout ma ce ne sono tre di utilizzo molto comune che permettono di affrontare ogni situazione:
- LinearLayout: contiene un insieme di elementi che distribuisce in maniera sequenziale dall'alto verso il basso (se definito con orientamento verticale) o da sinistra a destra (se ha orientamento orizzontale, il valore di default). È un layout molto semplice e piuttosto naturale per i display di smartphone e tablet;
- TableLayout: altro layout piuttosto semplice, inquadra gli elementi in una tabella e quindi è particolarmente adatto a mostrare strutture regolari suddivise in righe e colonne come form o griglie. È piuttosto semplice da usare e ricorda molto le tabelle HTML nelle pagine web con i ben noti tag <table> <tr> <td> ;
- RelativeLayout: sicuramente il più flessibile e moderno. Adatto a disporre in maniera meno strutturata gli elementi, ricorda un po' il modo di posizionare <div> flottanti nelle pagine web. Essendo “relative” gli elementi si posizionano in relazione l'uno all'altro o rispetto al loro contenitore, permettendo un layout fluido che si adatta bene a display diversi. Rispetto agli altri due è ricco di attributi XML che servono ad allineare e posizionare gli elementi tra loro.
La figura che segue mostra tre semplici esempi realizzati con layout diversi.
In generale, non c'è nessun lavoro precluso ad un particolare tipo di layout. Il programmatore imparerà col tempo e la pratica ad associare la struttura grafica che deve realizzare allo strumento più adatto a progettarla.
12. Definire layout in XML, il markup di base
Dopo aver classificato i principali layout in base a tipologia e finalità, passiamo all'aspetto pratico, il vero e proprio markup XML necessario a definirli.
Elementi comuni nei layout
Prima di passare agli esempi definiamo alcuni elementi che accomunano le sintassi di tutti i layout. Innanzitutto, gli attributi XML utilizzati per la maggior parte proverranno da un namespace avente URI http://schemas.android.com/apk/res/android. Per questo motivo quando definiremo layout in un progetto Android il nodo root che conterrà tutti gli elementi mostrerà al suo interno la dichiarazione
xmlns:android="http://schemas.android.com/apk/res/android"
e darà senso al prefisso android: che verrà usato per tutti gli attributi nel file.
Secondo aspetto comune non solo ai layout ma anche a tutti gli elementi in essi contenuti, la presenza obbligatoria di due attributi: layout_width e layout_height, che definiscono la capacità dell'elemento di estendersi, rispettivamente, in larghezza (width) o altezza (height). Il loro valore può essere una dimensione, espressa in dp, come già spiegato, o una costante da scegliere tra:
- wrap_content: l'elemento sarà alto o largo a sufficienza per includere il suo contenuto;
- match_parent: l'elemento si estenderà in altezza o in larghezza fino a toccare il suo contenitore.
Quando si andrà ad impostare layout_heigth (o layout_width) l'IDE suggerirà un terzo valore possibile, fill_parent. Questo rappresenta un sinonimo di match_parent ma non va usato in quanto ormai deprecato.
Nel prosieguo di questo capitolo, verrà presentata la sintassi di base dei principali layout. Gli elementi posizionati all'interno dei layout potranno essere altri layout annidati o widget, termine con cui si indicano tutti i controlli per interfacce utente. Nell'ultimo esempio faranno la loro comparsa TextView e Button, molto comuni nelle UI Android. Una spiegazione più dettagliata dei widget sarà presentata nei prossimi capitoli.
Sintassi dei layout
Il LinearLayout riceve con l'attributo orientation la sua connotazione principale. Con esso si dichiara in quale senso verranno disposti gli elementi, orizzontalmente (il default) o verticalmente.
Un esempio:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> . . . . . . </LinearLayout>
Il TableLayout viene specificato mediante due tag: TableLayout e TableRow.
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="match_parent"> <TableRow android:layout_width="wrap_content" android:layout_height="match_parent"> . . . . . . </TableRow> . . . . . . </TableLayout>
TableLayout rappresenta la tabella nel suo complesso mentre ogni nodo TableRow contiene tutti gli elementi di una riga. Il concetto di colonna viene reso in automatico, ogni elemento in un TableRow costituisce una colonna.
Il RelativeLayout sfrutta gli attributi per definire posizionamenti. Sono molti ma piuttosto intuitivi.
A scopo di orientamento, segue una tabella riassuntiva che ne raggruppa le diverse categorie in base alla finalità. Il tipo di valore assegnato a questi attributi può essere booleano (true o false) o l'id di un elemento appartenente al layout.
| layout_alignParentTop layout_alignParentBottom layout_alignParentLeft layout_alignParentRight | Allineamento con il contenitore: attributi che definiscono se l'elemento deve allinearsi ad uno dei bordi del proprio contenitore. Il valore di questo attributo è di tipo booleano |
| layout_alignTop layout_alignBottom layout_alignLeft layout_alignRight | Allineamento con altro elemento: attributi che definiscono se l'elemento deve allinearsi ad uno dei bordi di un altro elemento del layout. Il valore di questo attributo sarà l'id dell'elemento con cui allinearsi |
| layout_above layout_below layout_toLeftOf layout_toRightOf | Posizionamento relativo ad un altro elemento: indicano se l'elemento si trova, rispettivamente, sopra, sotto, a sinistra o a destra del componente il cui id è il valore dell'attributo |
| layout_centerHorizontal layout_centerVertical layout_centerInParent | Centramento: rispettivamente rappresentano se l'elemento deve essere centrato orizzontalmente, verticalmente o in entrambe le direzioni. Il valore è booleano. |
Il frammento di XML che segue mostra un esempio di RelativeLayout con una TextView collocata in alto a sinistra ed un Button in basso al centro:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:text="Esempio di Relative Layout"/> <Button android:layout_width="90dp" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:text="Clicca qui!"/> </RelativeLayout>
13. View: le basi dell'interfaccia grafica
Una classe che incontreremo spesso nelle interfacce utente Android è View. Con questo termine intendiamo un qualunque elemento che appare in un'interfaccia utente e che svolge due funzionalità:
- mostra un aspetto grafico;
- gestisce eventi relativi all'interazione con l'utente.
Una classe derivata da View è ViewGroup. Esso è, al contempo, un tipo di View e un contenitore di altre View. Tanto per fare un esempio di ViewGroup pensiamo ai layout, sono tipici raggruppatori di View.
Classificazione di View
Tutto ciò che tratteremo come un controllo utente in Android sarà direttamente o indirettametne discendente di una View.
Nel corso della guida, le tipologie di View da presentare si articoleranno per lo più in tre categorie:
- i layout di cui si è discusso nei precedenti capitoli;
- i widget che in Android rappresentano i controlli form sono delle View. Il loro utilizzo, spiegato a breve, si snoderà tra posizionamento nei layout e configurazione dei loro gestori di evento;
- gli AdapterView sono delle View, generalmente nello stesso package dei widget, che collaborano nella realizzazione del pattern Adapter. Gli AdapterView e gli Adapter saranno illustrati diffusamente nei prossimi capitoli e rappresentano nel percorso di apprendimento un argomento centrale della UI in Android.
View e id
A livello di ereditarietà, View contiene in sé le caratteristiche comuni a tutti gli elementi che popolano le interfacce Android.
Prima di tutto gli id. Degli id ne abbiamo parlato quando si sono spiegate le risorse e la classe R. Sono riapparsi parlando degli attributi del RelativeLayout. In generale può essere necessario etichettare un elemento della UI per potervi fare riferimento nel codice Java o in altre risorse XML.
Nei file di risorse, gli attributi id hanno un valore definito come @+id/identificatore dove per identificatore si intende il nome dell'id scelto dall'utente. Il simbolo + apposto dopo la @ indica che se l'id con quel nome non è stato ancora definito nel sistema sarà definito per l'occasione.
Se troveremo un elemento TextView così configurato:
<TextView android:id="@+id/nome" android:layout_width="wrap_content" android:layout_height="wrap_content" . . . . . . />
significherà che gli si potrà fare riferimento usando il suo id:
- in XML con @id/nome, ad esempio in un attributo del RelativeLayout;
- nel codice Java come R.id.nome.
Gestione degli eventi
La gestione degli eventi viene realizzata con il meccanismo dei listener.
Nell'immagine che segue, uno stralcio di codice Java mostra una View – in questo caso un Button – che si prepara a gestire un evento di click.
È un esempio che rappresenta i tratti salienti di qualunque gestione degli eventi, i numeri raffigurati indicano:
- il recupero di un riferimento alla View della quale verranno monitorati gli eventi;
- la definizione di un oggetto (anonimo in questo caso) che contiene un metodo onClick definito appositamente per gestire l'evento di click;
- la registrazione del listener tramite un metodo setter affinché il pulsante sappia chi è l'oggetto a cui delegare la gestione dei click.
Nonostante l'esempio sia specifico per un tipo di evento, il meccanismo appartiene a tutte le View e potrà essere riprodotto per qualunque evento. I tre punti cardine rimarranno sempre gli stessi.
Oltre al click le View Android sono in grado di gestire eventi di qualsiasi genere:
- cambiamento del focus;
- pressione di una chiave hardware;
- evento di LongClick, il click lungo;
- gestione del touch;
- molti altri ancora..
14. Widget: i componenti interattivi
In questo capitolo faremo conoscenza da vicino con i widget. Che siano tutti discendenti della classe View ormai l'abbiamo imparato. Ma quali sono i widget più comuni in Android? Ne elenchiamo alcuni:
- TextView: la classica label. Serve a rappresentare del testo fisso;
- EditText: corrisponde all'inputbox di altre tecnologie. Viene usata per permettere l'inserimento del testo. Mediante attributi XML può essere adattata alle proprie necessità. Molto utile è android:inputType i cui valori definiscono i formati più comuni di input (date, password, testo, etc.);
- Button: è il pulsante. La casistica più comune comporterà la gestione dell'evento click per attivare una qualche reazione nell'Activity;
- CheckBox: come ci si aspetta definisce un classico flag che può essere attivato o disattivato. Nel codice Java leggeremo il suo stato (checked o unchecked) mediante un valore booleano;
- Radio: esistono i radiobutton come in ogni altra tecnologia per interfacce utente. Vengono solitamente usati per definire valori alternativi tra loro come il sesso di una persona (maschio/femmina), possesso di facoltà (automunito Sì/No), etc.
L'elenco dei widget sarebbe sconfinato. Ne esistono veramente di ogni tipologia per poter aiutare l'utente ad introdurre gli input più variegati. Per ulteriori approfondimenti, fare pure affidamento alla documentazione ufficiale.
Widget al lavoro: un esempio
Nel capitolo relativo agli Intent, si era immaginato un form di login.
Vediamo come potrebbe essere nel dettaglio.
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TableRow android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Username:"/> <EditText android:layout_width="@dimen/width" android:layout_height="wrap_content" android:inputType="text" android:id="@+id/username" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Password:"/> <EditText android:layout_width="@dimen/width" android:layout_height="wrap_content" android:inputType="textPassword" android:id="@+id/password" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="wrap_content"> <Button android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:onClick="login" android:text="Login"/> <Button android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:onClick="cancel" android:text="Cancella"/> </TableRow> </TableLayout>
Notiamo gli aspetti salienti:
- come layout è stato usato un TableLayout particolarmente adatto alla realizzazione di form che spesso mostrano una struttura a griglia molto regolare;
- tra i controlli usati alcuni mostrano l'attributo android:id. Serve per lo più alle EditText usate per inserire username e password dell'utente che sta tentando il login. L'id sarà indispensabile per recuperare dal codice Java i valori inseriti;
- i due widget Button presentano l'attributo onClick usato per definire quale metodo dell'Activity si occuperà della gestione del click sul pulsante. Rappresenta una scorciatoia rispetto all'uso dei listener.
Il primo Button presenta questa valorizzazione di onClick:
android:onClick="login"
Ciò comporta che nell'Activity dovremo trovare un metodo della seguente forma:
public void login(View arg0)
dove il nome del metodo corrisponde al valore dell'attributo onClick e il parametro View in input rappresenta il widget che ha sollevato l'evento, in questo caso il Button.
Uno sguardo da vicino al metodo login nell'Activity ci mostra come i widget vengono richiamati nel codice Java.
public void login(View v) { EditText username=(EditText) findViewById(R.id.username); EditText password=(EditText) findViewById(R.id.password); String account_username=username.getText().toString(); String account_password=accounts.get(username.getText().toString()); . . . . . . }
Il metodo findViewById viene usato per recuperare il controllo corrispondente all'id.
Anche in questo caso R.id.username corrisponde all'attributo android:id=“@+id/username” che è stato assegnato all'EditText. Dando uno sguardo alla documentazione dei widget, si dovrà di volta in volta riconoscere quei metodi che servono a recuperare il valore del controllo.
In elementi come TextView e EditText rivolti al trattamento del testo, con un metodo getText() si potrà leggere il contenuto e gestirlo come una stringa.
15. Creare un menu
Le applicazioni per Android fanno largo uso di menu per offrire un'interazione con l'utente più vicina a quella dei tradizionali programmi per desktop.
Le tipologie di menu più comuni in Android sono due:
- Options menu: è il menu principale dell'applicazione e contiene voci riguardanti operazioni di interesse generale nella vita dell'app. Pensando ai programmi per desktop può essere paragonato al menu principale contenuto nella barra del titolo;
- Context Menu: è un menu invocabile su un singolo componente dell'interfaccia utente. Le voci richiamabili serviranno ad avviare operazioni sull'elemento su cui è stato richiesto il menu. Normalmente un menu contestuale viene attivato con un click lungo su un componente del layout. Ha le stesse finalità del menu che nei programmi per desktop viene richiamato con il classico “click” sul pulsante destro del mouse.
Definire la struttura del menu
Il primo passo per aggiungere un menu di qualsiasi tipo alla nostra Activity è crearne la struttura. In proposito, va sempre tenuto a mente che i menu sono risorse. Quindi il loro layout va definito nella sottocartella res/menu. Questo è il punto di partenza della creazione di un menu.
Il seguente codice mostra un layout di menu:
<menu xmlns:android="http://schemas.android.com/apk/res/android" > <item android:id="@+id/MENU_1" android:title="Nuova nota"/> <item android:id="@+id/MENU_2" android:title="Elenco note"/> </menu
Assumiamo che il nome del file sia main.xml. Come si può vedere la sintassi necessaria non è molto articolata. Per poter creare un menu minimale, sono sufficienti due tag: <menu> che definisce il menu nel suo complesso e <item> che dichiara la singola voce del menu. Gli attributi impiegati nella configurazione sono due:
- id che, come vedremo, saranno molto importanti nella gestione delle voci del menu ;
- title che contiene una stringa che dà il titolo alla voce di menu.
Il risultato è visibile in figura:
Il menu apparirà cliccando l'immagine cerchiata in rosso apposta sulla barra dell'activity.
Attivare il menu nell'activity
Affinchè il menu venga collegato all'Activity è necessario fare override di un metodo denominato onCreateOptionsMenu. Quella che segue è l'implementazione utilizzata nell'esempio:
@Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater=getMenuInflater(); inflater.inflate(R.menu.main,menu); return true; }
Questo metodo, richiesto nell'Activity che desidera il menu, prende come argomento un riferimento ad un oggetto Menu che non dovremo mai preoccuparci di istanziare in quanto sarà già preparato dal sistema. Ciò che resta da fare è configurarlo assegnandogli il layout che abbiamo predisposto nelle risorse. Questo sarà compito delle tre righe:
MenuInflater inflater=getMenuInflater(): recupera un riferimento ad un inflater di Menu ossia un servizio del sistema in grado di modellare la struttura dell'oggetto Menu in base alle direttive impostate in XML;inflater.inflate(R.menu.main,menu): questo è il momento in cui l'azione dell'inflating viene veramente svolta. Il metodo inflate richiede due parametri: la risorsa contenente il layout del del menu e l'oggetto Menu da configurare;return true: solo se il valore booleano restituito da onCreateOptionsMenu sarà true il menu sarà attivo.
Da ricordare che onCreateOptionsMenu verrà invocato una sola volta, al momento della creazione del menu, cosa che avverrà contestualmente alla creazione dell'activity.
Gestire le voci del menu
Per poter usare il menu manca solo la gestione del click. Questo viene fatto mediante il metodo onOptionsItemSelected.
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
int id=item.getItemId();
switch(id)
{
case R.id.MENU_1:
/*
Codice di gestione della voce MENU_1
*/
break;
case R.id.MENU_2:
/*
Codice di gestione della voce MENU_2
*/
}
return false;
}
Come si può vedere nello stralcio di codice, il parametro in input nel metodo è di classe MenuItem e rappresenta la singola voce selezionata. La prima cosa da fare è recuperare l'id della voce, così come è stato impostato nel menu ed in base al suo valore attivare la gestione corretta.
Creare un Context Menu
Finora la trattazione ha riguardato esclusivamente i menu Options. O almeno così sembra. In realtà i concetti finora espressi vengono applicati anche ai menu contestuali. Infatti un Context Menu viene creato in maniera del tutto simile ad un menu Options.
Le operazioni da effettuare sono le seguenti:
- definizione della struttura del menu contestuale in un apposito file della cartella res/menu;
- predisposizione del metodo
onCreateContextMenuin cui verranno eseguite le medesime operazioni svolte nell'onCreateOptionsMenu; - definizione delle azioni di risposta al click sulle voci mediante
onContextItemSelected; - effettuazione di un'operazione molto importante che non è, viceversa, usata negli OptionsMenu: la registrazione dell'elemento che vuole usare il menu contestuale.
Visto che il menu contestuale viene richiamato con click lungo su un elemento del layout, si deve segnalare all'activity quale elemento sarà dotato di questa caratteristica. Per fare ciò si invoca il metodo registerForContextMenu(View v), solitamente nell'onCreate dell'activity, e la View che viene passata come parametro di ingresso è proprio il riferimento all'elemento sul quale può essere attivato il menu contestuale.
16. ActionBar
Iniziando a sperimentare esempi di codice, uno degli aspetti maggiormente evidenti dal punto di vista grafico, è la spessa fascia scura che si trova nella parte superiore dell'applicazione. Il suo nome è ActionBar e la sua introduzione ha rappresentato un elemento fortemente innovativo a partire dalla versione 3 (HoneyComb) di Android.
Non si tratta solo di un “bordo”. In realtà, l'ActionBar può essere definita una “cornice“ programmabile destinata ad ospitare opzioni di navigazione e di interazione di utilità più o meno comune all'intera applicazione, tra cui:
- actions ossia pulsanti cliccabili per attivare azioni. Altro non sono in realtà che voci dell'Options Menu collocate sull'ActionBar;
- navigazione con “tab”;
- navigazione con menu a tendina;
- campi di ricerca;
- molto altro ancora.
In questo capitolo, si prenderà confidenza con l'ActionBar iniziando a sperimentarne funzionalità utili e già integrabili con quanto si è appreso sinora. Come di consueto, la documentazione ufficiale offrirà quanto necessario ad ulteriori approfondimenti.
Avere l'ActionBar disponibile
Essendo entrata a regime in Android 3, l'ActionBar è un elemento un po' di confine.
Se si sta programmando per API di livello 11 o superiori quindi per Android 4.x.x (nell'AndroidManifest.xml i valori degli attributi targetSdkVersion o minSdkVersion dovranno essere impostati almeno a 11) l'ActionBar sarà sempre disponibile purchè si abbia un tema “olografico”, quindi nel file manifest l'attributo android:theme dovrà essere impostato a Theme.Holo o un suo discendente.
Per avere l'ActionBar in applicazioni destinate anche a versioni di Android con API minori di 11 (quindi anche Android 2.x.x) si dovrà collegare il proprio progetto alla libreria di supporto appcompat v7 ed inoltre:
- l'Activity dovrà estendere ActionBarActivity;
- il tema dell'Activity dovrà essere Theme.AppCompat o un derivato.
Comandi nell'ActionBar
L'immagine (fonte: documentazione ufficiale Android) mostra una tipica ActionBar popolata con gli elementi più comuni:
- icona dell'applicazione e titolo;
- due actions;
- action overflow che ospita altre actions che non hanno trovato posto sull'ActionBar.
Icona dell'applicazione e titolo sono configurabili già dal manifest. Aprendo il file AndroidManifest.xml vediamo che il nodo <application> ha un attributo android:icon e android:label:
<application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" . . . . . .
Servono proprio a definire l'icona ed il titolo per l'applicazione. Questi attributi sono presenti anche nei nodi <activity> che permettono pertanto di adottare un'icona e un titolo per la singola Activity.
Le actions, come già accennato, non sono altro che i comandi che abbiamo imparato a gestire nel capitolo riguardante i menu. Infatti nella visione più moderna della programmazione Android, l'ActionBar tende ad assorbire parte del ruolo degli Options Menu dando la possibilità di ospitarne le voci.
Per farlo si dovrà solo mettere mano al layout del menu (reperibile nella cartella di risorse res/menu) impostando opportunamente il valore dell'attributo showAsAction, tra i seguenti:
| ifRoom | mostra le icone sull'ActionBar compatibilmente con lo spazio disponibile |
| never | non mostra le voci del menu sull'ActionBar ma solo nell'Options Menu |
| always | il layout viene forzato a mostrare le voci in ActionBar. E' un valore sempre sconsigliabile, meglio optare per ifRoom |
| withText | Oltre all'icona viene mostrato in ActionBar anche il testo, solitamente collegato all'attributo android:title |
Se si ha necessità di combinare più valori per showAsAction lo si può fare sfruttando un OR (|), ad esempio ifRoom|withText.
Lo stralcio di XML che segue mostra un OptionsMenu che collocherà entrambe le sue voci sull'ActionBar.
<menu xmlns:android="http://schemas.android.com/apk/res/android" > <item android:id="@+id/MENU_1" android:showAsAction="ifRoom" android:title="Nuovo" android:icon="@android:drawable/ic_menu_add"/> <item android:id="@+id/MENU_2" android:showAsAction="ifRoom" android:title="Elenco" android:icon="@android:drawable/ic_menu_agenda"/> </menu>
Eventuali voci del menu che non troveranno posto nell'ActionBar verranno ospitate nell'action overflow.
Navigazione "all'indietro"
Altra funzionalità che può essere introdotta da subito nella configurazione dell'ActionBar è il supporto alla navigazione all'indietro. L'immagine mostra, cerchiata in rosso, una piccola freccia verso sinistra accanto all'icona dell'applicazione.
Questo elementino di interazione dovrebbe prendere il posto del pulsante hardware comunemente chiamato “Back” o “Indietro”.
Questa tecnica verrà per lo più usata nelle Activity secondarie per tornare a quella precedente. Per impostarla sarà necessario seguire due passi:
- nell'AndroidManifest.xml, esattamente nel nodo
<activity>dell'Activity che dovrà mostrare la frecciolina, si specificherà la parent Activity ossia l'Activity a cui si dovrà tornare:<activity android:name=".SecondActivity" android:label="Seconda Activity" android:parentActivityName=".MainActivity"/>
- nell
'onCreatedell'Activity che mostra la frecciolina, si inserirà la seguente riga di codice:getActionBar().setDisplayHomeAsUpEnabled(true);
17. Le notifiche in Android
Gli utenti Android sono abituati a ricevere notifiche. Si manifestano con una piccola icona che appare nella cosiddetta “Notification Area” e se ne può consultare il contenuto aprendo il “Notification drawer”, una zona “a scomparsa” sul display. Oltre alla semplicità comunicativa che le contraddistingue e alla familiarità dell'utente con questo meccanismo, vale la pena trattarle in un corso di questo tipo perché offrono un esempio di segnalazione che esula dall'interfaccia dell'applicazione.
Se si osserva la figura (dalla documentazione ufficiale Android) si possono riconoscere i vari elementi che costituiscono una comune notifica. Facciamoci guidare dai numeri indicati:
- titolo della notifica (content title);
- icona grande (large icon);
- contenuto della notifica (content text);
- informazioni accessorie (content info);
- icona piccola (small icon) che di norma appare anche nella barra del display;
- ora della notifica (when) impostata dal programmatore o di default dal sistema.
La prima notifica
Visto che le notifiche appaiono in zone del display non gestite dall'applicazione, dovremo interagire con il sistema mediante un apposito servizio: il NotificationManager. Ne recuperiamo un riferimento:
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Nonostante la molteplicità di aspetti che contraddistinguono una notifica, ve ne sono tre assolutamente obbligatori:
- l'icona piccola;
- il titolo;
- il contenuto.
Questi saranno configurati, rispettivamente, con i metodi setSmallIcon, setTitle e setContentText.
Vediamo subito un esempio:
NotificationCompat.Builder n = new NotificationCompat.Builder(this) .setContentTitle("Arrivato nuovo messaggio!!") .setContentText("Autore: Nicola Rossi") .setSmallIcon(android.R.drawable.ic_dialog_email); NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); notificationManager.notify(0, n.build());
La creazione della notifica ha seguito due fasi:
- è stata costruita mediante il Builder incluso nella classe NotificationCompat. I vari metodi setter permetteranno di configurarne i molti aspetti. In questo caso, si è provveduto al minimo indispensabile;
- finita la fase di build, la notifica verrà pubblicata dal NotificationManager mediante invocazione del metodo
notify.
La notifica ottenuta nell'emulatore – con il Notification drawer aperto – è mostrata in figura:
Collegare un'azione alla notifica
Far apparire notifiche non soddisfa le necessità dell'utente però. Egli è abituato a cliccarvi sopra per ottenere una reazione da parte dell'applicazione. Noi, proseguendo l'esempio, faremo in modo che il click sulla notifica provochi l'apertura di un'altra Activity denominata MessageActivity.
La preparazione dell'Activity non verrà ripetuta in questa sede considerato lo spazio dedicatole nei capitoli precedenti. Ricordiamo comunque che si dovranno seguire due step fondamentali: creare la classe MessageActivity estendendo Activity oltre all'eventuale layout ed inserire un nodo di configurazione del nuovo componente nell'AndroidManifest.xml.
L'apertura dell'Activity avverrà mediante Intent ma non sarà attivata subito con il metodo startActivity bensì sarà predisposta per “usi futuri” mediante la classe PendingIntent. Si tratta di una classe che, per così dire, conserva l'Intent e la descrizione dell'azione che esso porta con sé per poterlo attivare successivamente. Ciò che faremo sarà:
- predisporre un normale
Intentper l'apertura dellaMessageActivity; - creazione del
PendingIntentsfruttando l'Intentdel punto precedente; - assegnazione del
PendingIntentalla notifica mediante il metodosetContentIntentdelNotificationCompat.Builder.
Di seguito le modifiche da apportare al codice precedente:
Intent i=new Intent(this,MessageActivity.class); PendingIntent pi=PendingIntent.getActivity(this, 0, i, 0); NotificationCompat.Builder n = new NotificationCompat.Builder(this) . . . . . . .setContentIntent(pi) .setAutoCancel(true); NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); notificationManager.notify(0, n.build());
Ora, dopo l'apertura del “Notification drawer”, si potrà cliccare sulla notifica e ciò comporterà l'esecuzione dell'azione contenuta nel PendingIntent con conseguente avvio della MessageActivity. La notifica scomparirà dalla barra dell'applicazione non appena selezionata, merito dell'invocazione al metodo setAutoCancel().
Notifiche con avviso sonoro
Le notifiche che appaiono sui dispositivi spesso attirano la nostra attenzione con segnalazioni sonore. Per aggiungere questa ulteriore funzionalità all'esempio, dobbiamo recuperare l'Uri del suono che desideriamo:
Uri sound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
e collegarlo alla notifica in costruzione:
NotificationCompat.Builder n = new NotificationCompat.Builder(this) . . . .setSound(sound);
Da questo momento la notifica sarà anche sonora.
18. Notifiche: Toast e Dialog
Una delle fasi più comuni dell'interazione app-utente è la notifica di messaggi mediante le cosiddette finestre di dialogo. Tra l'altro, la presenza costante delle AlertBox, MessageBox e dialog nelle diverse tecnologie web e desktop di ogni epoca ha reso questa forma di comunicazione particolarmente familiare all'utente.
Toast
Utilizzando un dispositivo Android, una tipologia di notifica che si incontra presto è il cosiddetto Toast. Si tratta di una piccola forma rettangolare nera che appare nella parte bassa del display contenente un messaggio con il testo bianco. La sua visibilità dura poco, qualche secondo, e le sue apparizioni improvvise dal basso gli hanno donato questo nome che richiama letteralmente il modo in cui il pane salta fuori dai tostapane.
Il Toast è la forma di notifica più immediata che esiste e realizzarlo è molto semplice:
Toast.makeText(this, "Ciao a tutti!", Toast.LENGTH_SHORT).show();
Si fa uso della classe omonima che espone un metodo statico makeText che prepara il messaggio. I tre parametri richiesti sono:
- un riferimento al Context dell'app. In questo caso, come spesso avviene, si risolve passando un riferimento this all'Activity stessa;
- il testo che si vuole mostrare. Ovviamente sarebbe il caso di sostituirlo con apposite risorse stringa;
- la durata del messaggio utilizzando come valori uno di quelli messi a disposizione delle costanti della classe Toast: LENGTH_LONG e LENGTH_SHORT.
Da non dimenticare, l'invocazione del metodo show() senza la quale il Toast non apparirà.
Il risultato è visibile in figura:
Le Dialog
Immediatezza comunicativa e rapidità di implementazione sono i vantaggi principali del Toast ma ciò che offre spesso non basta. Arriva presto il momento di utilizzare delle vere finestre di dialogo.
Android offre la possibilità di avere Dialog grezze da configurare o in alternativa alcuni tipi già pronti che rispecchiano gli utilizzi più comuni: AlertDialog per gli avvisi, ProgressDialog per mostrare barre di progresso ed altre ancora.
Dialog è la superclasse di tutte le finestre di dialogo e ne rappresenta il tipo più duttile ma che lascia più lavoro al programmatore. L'esempio seguente mostra codice che può essere eseguito all'interno di un metodo dell'Activity:
Dialog d=new Dialog(this); d.setTitle("Login"); d.setCancelable(false); d.setContentView(R.layout.dialog); d.show();
Ciò che succede è descritto qui di seguito:
- istanziamo un oggetto di classe Dialog passando un riferimento al Context;
- impostiamo un titolo, già proprietà della finestra stessa;
- definiamo la finestra come modale richiedendo la sua non-cancellabilità mediante
setCancelable. Ciò impedirà che toccando il display al di fuori della finestra di dialogo essa si chiuda; - assegnamo un layout alla finestra di dialogo come faremmo per un'Activity. Il layout si trova , come tutti i layout, in res/layout e contiene un normale form. Per l'esempio si può usare lo stesso visto per i widget;
- ultimo ma non meno importante, invochiamo il metodo
show()senza il quale la finestra di dialogo non apparirà.
Questo il risultato:
Il click sui pulsanti sarà gestito mediante click listener. Allo scopo è importante dotare, nel layout, i pulsanti di appositi id. Immaginando che il pulsante con l'etichetta “Login” abbia come id R.id.login:
final Dialog d=new Dialog(this); /* * OMISSIS: configurazioni varie della finestra di dialogo come le precedenti * */ Button b=(Button) d.findViewById(R.id.login); b.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { Toast.makeText(d.getContext(), "cliccato", Toast.LENGTH_LONG).show(); } }); d.show();
L'utilizzo del codice non stupisce ma si notino comunque due particolarità:
- il Context necessario nel Toast è stato recuperato dal Dialog stesso;
- il Dialog dichiarato, a differenza dello snippet precedente, è stato etichettato come final in quanto, per una regola propria del linguaggio Java, dichiarato nello stesso metodo in cui viene istanziato l'oggetto anonimo che lo usa.
Dialog già "pronte"
Come si è visto, l'uso della classe Dialog non è proibitivo ma richiede comunque alcune operazioni. Parallelamente, Android offre tipi di finestre di dialogo, molto comuni, praticamente pronte all'uso.
I principali:
- AlertDialog: la più adattabile. Sfrutta una classe interna detta Builder per configurare i vari aspetti:
AlertDialog.Builder builder=new AlertDialog.Builder(this); builder.setTitle("Attenzione!"); builder.setMessage("Operazione non valida!"); builder.show();
L'esempio di codice porta alla realizzazione di una finestra di dialogo che semplicemente mostra un titolo ed un messaggio: praticamente una normalissima AlertBox. AlertDialog inoltre ha già tre pulsanti innestati denominati PositiveButton, NegativeButton e NeutralButton per i quali è necessario solo definire le azioni di gestione del click. Infine piuttosto che richiamare lo show direttamente sul Builder, come mostrato, si può ottenere un riferimento alla finestra di dialogo prodotta tramite il metodo createDialog.
- ProgressDialog è un derivato della AlertDialog pensato per lo più per ospitare indicatori di progresso sia in stile orizzontale (barra) che spinner (circolare). Può essere utilizzato in maniera molto agevole grazie alla versione statica del metodo show:
ProgressDialog progress = ProgressDialog.show(this, "Attendere", "Scaricamento in corso...", true);
- DatePickerDialog e TimePickerDialog, specializzati nella selezione di date e orari, mostrano controlli adatti e opportuni metodi per impostare le informazioni temporali oltre che rilevare gli eventi di selezione.
19. Visualizzare pagine Web: WebView
Una WebView è un tipo di View che permette di visualizzare pagine web. La sua utilità principale è quella di permettere di integrare una web application o più in generale un sito web nella propria applicazione. Il motore della WebView risiede nella libreria WebKit già inclusa all'interno di Android per questo possiamo parlare di questo componente come di un browser vero e proprio in grado di eseguire Javascript e mostrare layout nella maniera più completa possibile.
Nella figura viene mostrato un semplicissimo esempio in cui il layout dell'Activity è costituito esclusivamente dalla WebView e la si è usata per visualizzare direttamente il contenuto della pagina http://www.html.it.
Per ottenere questo risultato si sono compiute tre semplici operazioni:
- si è creato un layout come il seguente:
<WebView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/webview" android:layout_width="match_parent" android:layout_height="match_parent" />
- si è richiesto alla WebView, già nell'onCreate dell'Activity, di caricare l'indirizzo remoto:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); WebView myWebView = (WebView) findViewById(R.id.webview); myWebView.loadUrl("http://www.html.it"); } - si è provveduto ad impostare nell'AndroidManifest.xml, all'interno direttamente del nodo
<manifest>, la permission per il collegamento ad Internet:<manifest xmlns:android="http://schemas.android.com/apk/res/android" ... ... <uses-permission android:name="android.permission.INTERNET"/> <application ... ...
Per la prima volta in questo corso, si incontra una permission. Rappresenta un aspetto di sicurezza che deve essere garantito ogni volta che l'applicazione vuole intraprendere attività di comunicazione o di interazione particolari. Le permission devono essere accettate dall'utente al momento dell'installazione e non nell'esecuzione a runtime. In questo caso, la nostra applicazione vuole accedere ad Internet per mezzo della WebView e per questo deve dichiararlo, semplicemente inserendo l'apposita permission nel manifest.
WebView e Javascript
Non si può parlare di Web al giorno d'oggi senza considerare Javascript. Di default, il suo uso in una WebView non è attivo. Per abilitare Javascript è sufficiente inserire queste righe di codice:
WebView myWebView = (WebView) findViewById(R.id.webview); WebSettings webSettings = myWebView.getSettings(); webSettings.setJavaScriptEnabled(true);
Come si vede si è fatto uso delle WebSettings di WebView. Si tratta di un insieme di configurazioni che possono essere impostate per regolare tutti i comportamenti del componente quando si trova a fronteggiare le classiche problematiche da browser:
- cache;
- font e layout;
- privacy e sicurezza.
La WebView può fare anche in modo che Javascript interagisca con il codice Java presente nell'applicazione. Per fare questo è necessario:
- creare il codice Java che vogliamo richiamare in Javascript apponendo l'annotazione
@JavascriptInterfacesui metodi che devono essere visibili “da Javascript”:public class WebAppInterface { ... ... @JavascriptInterface public void executeJavaCode() { /* Codice da attivare DA JAVASCRIPT */ } }
- registriamo presso la WebView il nostro oggetto Java, assegnandogli un'etichetta di nostra scelta, in questo caso AndroidObject:
WebView myWebView = (WebView) findViewById(R.id.webview); myWebView.addJavascriptInterface(new WebAppInterface(), "AndroidObject");
Dopo questi passi, da Javascript potremo invocare il metodo executeJavaCode sull'oggetto Java che sarà stato registrato nella WebView con l'etichetta AndroidObject. Ad esempio, nella pagina web che richiameremo dalla WebView potremo usare il seguente snippet Javascript:
<script type="text/javascript"> function interactWithAndroid() { AndroidObject.executeJavaCode(); } </script>
20. ListView e GridView
Tra le tante View di cui dispone Android, ne esiste una categoria particolarmente importante. Costituisce buona parte delle interfacce di cui sono dotate le app che usiamo. Stiamo parlando delle AdapterView e del loro rapporto con gli Adapter.
Adapter e AdapterView
Finora abbiamo apprezzato due modalità diverse per realizzare le varie parti delle nostre app:
- design in XML, per layout e risorse, dall'approccio piuttosto visuale ma orientato alla definizione di parti statiche;
- sviluppo in Java, fondamentale per le funzionalità dinamiche ma meno pratico per il disegno di porzioni di layout.
Ma se dovessimo realizzare una “via di mezzo”: una visualizzazione iterativa di contenuti archiviati in strutture dati, potenzialmente variabili, come dovremmo comportarci? In fin dei conti, è un caso comunissimo, pensiamo alle app che mostrano liste di messaggi di vario genere, elenchi di notizie o risultati di una query su database.
Tutti questi lavori vengono affrontati con Adapter e AdapterView dove:
- Adapter è un componente collegato ad una struttura dati di oggetti Java (array, Collections, risultati di query) e che incapsula il meccanismo di trasformazione di questi oggetti in altrettante View da mostrare su layout;
- AdapterView è un componente visuale che è collegato ad un adapter e raccoglie tutte le View prodotte dall'adapter per mostrarle secondo le sue politiche.
Passiamo subito ad un esempio pratico.
ListView, un AdapterView molto comune
Il primo esempio su questo argomento fondamentale viene fatto usando la più comune degli AdapterView, la ListView, ed il più immediato degli Adapter, l'ArrayAdapter.
Il problema è: abbiamo un array di oggetti String e vorremmo che, iterativamente, una View ci mostrasse tutte le stringhe disposte in righe.
Agiamo così:
- otteniamo un riferimento alla struttura dati, in questo caso l'array di String;
- istanziamo un ArrayAdapter assegnandogli, via costruttore, l'array che fungerà da sorgente dei dati e il layout da usare per ogni singola riga;
- recuperiamo la ListView predisposta nel layout e le assegnamo il riferimento all'adapter che sarà il suo “fornitore” di View.
Creiamo due layout:
- uno per l'Activity, nel file activity_main.xml, costituito solo dalla ListView:
<ListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/listview" android:layout_width="match_parent" android:layout_height="match_parent"/>
- l'altro per la singola riga, in row.xml, contenente solo la TextView che mostrerà il testo di ogni riga:
<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"/>
Il collegamento dinamico tra struttura dati/Adapter/Listview viene realizzato nell'onCreate dell'activity:
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
String[] citta=new String[]{"Torino","Roma","Milano","Napoli","Firenze"};
ArrayAdapter<String> adapter=new ArrayAdapter<String>(this, R.layout.row,citta);
ListView listView = (ListView) findViewById(R.id.listview);
listView.setAdapter(adapter);
}
L'immagine seguente mostra il risultato:
L'ArrayAdapter è il caso più semplice di Adapter ma è comunque di uso molto frequente. Nella documentazione ufficiale è possibile trovare molti altri adapter già inclusi nel framework facilmente integrabili nelle proprie applicazioni.
GridView
Dire che la ListView nell'esempio precedente ha fatto molto poco non è del tutto sbagliato. La logica che trasforma gli oggetti in View è totalmente incluso nell'adapter. La ListView, o in generale gli AdapterView, si limitano in molti casi a recuperare View dall'adapter e a mostrarle nel layout.
Se volessimo sostituire la ListView con un altro AdapterView, diciamo la GridView specializzata in griglie, dovremmo compiere molto lavoro? Decisamente no.
Sarà sufficiente:
- nel layout, sostituire la ListView con la GridView, assegnando un numero di colonne alla griglia:
<GridView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/gridview" android:numColumns="3" android:layout_width="match_parent" android:layout_height="match_parent"/>
- nel codice Java semplicemente sostituire l'uso della classe ListView con GridView:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); String[] nomi=new String[]{"Torino","Roma","Milano","Napoli","Firenze"}; ArrayAdapter<String> adapter=new ArrayAdapter<String>(this, R.layout.row,nomi); // sostituiamo ListView con GridView GridView gridView = (GridView) findViewById(R.id.gridview); gridView.setAdapter(adapter); }
La figura mostra come le View non saranno più mostrate in lista ma in una griglia a tre colonne.
Gestione degli eventi
Gli AdapterView hanno un altro ruolo molto importante: la gestione degli eventi. Come per tutte le View viene effettuato mediante il meccanismo dei listener. Un caso molto comune è la gestione del click su un elemento della lista, ciò che viene normalmente interpretato come selezione.
Tornando all'esempio della GridView, se volessimo far visualizzare un Toast che notifica quale elemento è stato selezionato dovremmo inserire il seguente codice:
GridView gridView = (GridView) findViewById(R.id.gridview); gridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> av, View v, int pos,long id) { Toast.makeText(getApplicationContext(), "Selezionato "+citta[pos], Toast.LENGTH_LONG).show(); } });
La classe OnItemClickListener viene utilizzata allo scopo di gestire il click ed il suo metodo onItemClick conterrà il vero codice da attivare ad ogni selezione di elemento. I suoi parametri in input conterranno tutte le informazioni utili tra cui:
- l'elemento su cui si è cliccato (il secondo, di classe View);
- la posizione che ricopre nella struttura (il terzo parametro, di tipo int). All'interno del metodo onItemClick si vede come l'informazione della posizione è stata sfruttata per recuperare dalla struttura dati l'oggetto (citta[pos]).
Proseguire lo studio di AdapterView e Adapter
Quello che è iniziato con questo capitolo è un argomento importante e molto articolato. Rappresenta un blocco fondamentale della UI Android.
Lo studio dell'argomento dovrà proseguire innanzitutto scoprendo i vari tipi di AdapterView disponibili nel framework. Nel prosieguo della guida ne verranno presentati altri tra cui, a brevissimo, lo Spinner ma per il resto la documentazione ufficiale rimarrà una fonte inestimabile di informazioni.
Altrettanto importante sarà apprezzare le potenzialità degli Adapter. In particolare, sarà fondamentale imparare a realizzare un Adapter Custom in cui andremo a definire una logica personalizzata di trasformazione degli oggetti in View. Ciò sarà trattato in un articolo successivo di questa guida.
21. Spinner (menu a tendina)
Lo spinner è un altro widget molto comune, è il classico menu a tendina. In Android viene realizzato come AdapterView e tanto basta per farci comprendere il modo in cui dovremo usarlo.
Si è visto nelle lezioni precedenti che tutti gli AdapterView vengono grosso modo usati alla stessa maniera. È sufficiente collegare loro un Adapter che incapsula la logica di produzione delle View.
Per il resto l'AdapterView si occuperà di gestire gli eventi.
Spinner con valori fissi
Comunque lo Spinner trova la sua utilità anche in contesti meno complessi in cui si può usare come normale campo form per selezionare un valore in un dato insieme.
Pensiamo ad un form in cui si inseriscono i dati di una persona. Al momento di definire lo stato civile, la scelta ricade su un set di possibilità prestabilite: coniugato/a, divorziato/a, celibe/nubile, separato/a.
Il controllo ideale per effettuare questa scelta è senza dubbio lo Spinner . In questo caso, si potrebbe sentire meno il bisogno dell'Adapter in quanto la sorgente dati non cambierà più visto che vengono annoverati già tutti gli stati civili possibili.
In questo caso, si può procedere agendo solo tra risorse XML:
- si crea un array di risorse stringa in un file della cartella res/values e lo si completa con tutti i valori necessari:
<string-array name="staticivili"> <item>Divorziato/a</item> <item>Separato/a</item> <item>Coniugato/a</item> <item>Celibe/Nubile</item> </string-array>
La risorsa sarà accessibile in XML mediante
@array/staticivili - nel file di layout in cui si trova lo Spinner si effettua una modifica. Si aggiunge l'attributo
android:entriese gli si assegna la risorsa di stringhe a cui accedere:<Spinner android:id="@+id/spinner" android:layout_width="wrap_content" android:layout_height="wrap_content" android:entries="@array/staticivili"/>
Seguendo questi due semplici passi, nel layout troveremo lo spinner già popolato dei valori. Non è stato necessario apportare alcuna modifica nel codice Java e tantomeno istanziare direttamente un adapter.
Spinner con Adapter
Il comportamento dello Spinner legato ad un adapter è in buona parte uguale a quello della ListView e della GridView.
Vedremo ora un esempio che mostra un uso congiunto di Spinner e ListView in cui:
- lo Spinner mostra un elenco di Paesi;
- la ListView visualizza un elenco di città appartenenti tutte al Paese selezionato nello Spinner.
L'esempio ha anche il pregio di riepilogare molti concetti visti sinora nello studio delle GUI quindi lo si consideri un esercizio di validità generale.
L'immagine seguente mostra le varie fasi di funzionamento come appaiono in un emulatore.
La fonte dei dati sarà una classe Java, molto semplice, che con liste e mappe fornirà i dati necessari all'esempio:
public class CountryList { private HashMap<String,ArrayList<String>> list; public CountryList() { list=new HashMap<String, ArrayList<String>>(); ArrayList<String> cities=new ArrayList<String>(); cities.add("Roma"); cities.add("Torino"); cities.add("Firenze"); list.put("Italia", cities); cities=new ArrayList<String>(); cities.add("Parigi"); cities.add("Lione"); cities.add("Marsiglia"); list.put("Francia", cities); cities=new ArrayList<String>(); cities.add("Madrid"); cities.add("Barcellona"); list.put("Spagna", cities); } public Collection<String> getCountries() { return list.keySet(); } public Collection<String> getCitiesByCountry(String c) { return list.get(c); } }
Il layout dell'Activity è molto semplice (file: res/layout/activity_main.xml), un RelativeLayout che mostra entrambe le View:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <Spinner android:layout_width="@dimen/body_width" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_marginTop="@dimen/margin_top_1" android:id="@+id/countries" /> <ListView android:layout_width="@dimen/body_width" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_below="@+id/countries" android:layout_marginTop="@dimen/margin_top_2" android:id="@+id/cities"/> </RelativeLayout>
mentre la forma che avrà la singola riga dello Spinner e della ListView sarà la seguente (file: res/layout/row.xml):
<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="5dp" android:textSize="25sp" android:id="@+id/rowtext" />
Tenere presente che quando nel codice dell'Activity richiameremo l'id R.id.rowtext ci riferiremo alla TextView compresa in questo layout.
Il codice dell'Activity non offre grandi sorprese:
public class MainActivity extends Activity { private CountryList countries=new CountryList(); private ArrayAdapter<String> listviewAdapter; private ArrayAdapter<String> spinnerAdapter; @Override protected void onCreate(Bundle savedInstanceState) { // assegnazione del layout all'Activity super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // preparazione della ListView per l'elenco delle città ListView lv=(ListView) findViewById(R.id.cities); listviewAdapter=new ArrayAdapter<String>(this, R.layout.row); lv.setAdapter(listviewAdapter); // preparazione dello Spinner per mostrare l'elenco dei Paesi spinnerAdapter=new ArrayAdapter<String>(this, R.layout.row); spinnerAdapter.addAll(countries.getCountries()); Spinner sp=(Spinner) findViewById(R.id.countries); sp.setAdapter(spinnerAdapter); sp.setOnItemSelectedListener(new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) { TextView txt=(TextView) arg1.findViewById(R.id.rowtext); String s=txt.getText().toString(); updateCities(s); } @Override public void onNothingSelected(AdapterView<?> arg0) { } }); } private void updateCities(String city) { ArrayList<String> l=(ArrayList<String>) countries.getCitiesByCountry(city); listviewAdapter.clear(); listviewAdapter.addAll(l); } }
Da notare comunque che:
- essendo entrambi AdapterView, si sono svolte le stesse operazioni per Spinner e ListView. In entrambi si è preparato un Adapter che gli è stato collegato con il metodo
setAdapter; - la gestione degli eventi è stata usata solo per lo Spinner. Avviene nella maniera classica illustrata nel capitolo delle View;
- all'interno del metodo
onItemSelectedche gestisce la selezione di una voce dello Spinner viene invocato il metodoupdateCitiesche aggiorna mediante l'adapter della ListView l'elenco delle città. Viene fatto in maniera molto semplice ricaricando una nuova lista.
22. Stili e temi
Cos'è che trasforma questa lista:
in quest'altra ?
Risposta: l'applicazione di uno stile.
Tra le due immagini non ci sono differenze “strutturali”. Si tratta della medesima combinazione di ListView e ArrayAdapter usata nei capitoli precedenti.
Il layout usato per raffigurare la singola riga è il seguente:
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"/>
Quindi anche questo molto elementare.
In questo capitolo impareremo ad usare gli stili nelle interfacce Android. A livello concettuale, non si tratta altro che del porting dei CSS all'interno del framework e solitamente questo è ancora un elemento che fa contenti gli sviluppatori web.
Definire uno stile
Uno stile è una risorsa. Pertanto deve riuscire a trovare il suo posto all'interno della cartella res. Normalmente gli stili vengono ospitati nella sottocartella res/values e codificati mediante il tag XML <style>.
Lo stile che ha trasformato la lista delle figure precedenti è questo:
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <style name="rowstyle"> <item name="android:textColor">#FFFFFF</item> <item name="android:textSize">25sp</item> <item name="android:textStyle">bold</item> <item name="android:background">@drawable/row_background</item> <item name="android:paddingLeft">15dp</item> </style> </resources>
È contenuto nel file res/values/row_style e come si vede dimostra che la creazione di uno stile è abbastanza intuitiva:
- si crea un nodo
<style>; - si assegna un attributo name per il nodo
<style>e questo diverrà il nome dello stile nel progetto; - per ogni aspetto dello stile che si vuole curare si aggiunge un sottonodo
<item>in cui l'attributo name definirà l'aspetto cui assegnare un valore ed il contenuto del nodo costituirà il valore assegnato.
Nell'esempio ciò che interessava era:
- rendere il testo di colore bianco, in grassetto e leggermente più grande del normale:
<item name="android:textColor">#FFFFFF</item> <item name="android:textSize">25sp</item> <item name="android:textStyle">bold</item>
- distanziarlo a sinistra dal bordo aggiungendo un po' di padding che potremmo definire un margine interno all'elemento:
<item name="android:paddingLeft">15dp</item>
- dotare ogni TextView di angoli leggermente arrotondati e di un colore di sfondo sfumato dal celestino chiaro ad un azzurro non troppo scuro:
<item name="android:background">@drawable/row_background</item>
Per quanto riguarda i primi due punti non c'è molto da dire. Infatti in questi casi si deve solo cercare nella documentazione il nome dell'attributo che regola un aspetto e dimensionarlo appositamente, ad esempio textColor rappresenta il colore del testo e gli assegnamo il valore esadecimale che rappresenta il bianco. Da notare che come unità di misura del font non è stato usato il dp ma sp. Il concetto alla base di sp è identico a quello dei dp ma è più rispettoso delle problematiche dei font.
Ciò che è particolare è il terzo punto, la creazione dello sfondo. Come si vede rimanda ad un'altra risorsa, di tipo drawable, e di nome row_background. Per fare ciò creeremo un file in una cartella drawable, ad esempio: res/drawable-mdpi/row_background.xml.
Ecco il suo contenuto:
<shape xmlns:android="http://schemas.android.com/apk/res/android" > <gradient android:startColor="#2669DE" android:endColor="#99ADD1" android:angle="90"/> <corners android:radius="5dp"/> </shape>
Contiene un nodo <shape> che, anche se può sembrare strano, serve a disegnare in XML. Uno shape è una forma, di default rettangolare, che può essere configurata mediante i suoi sottonodi.
Questo shape ha due sottonodi:
<gradient>che realizza la sfumatura definendo i codici esadecimali dei colori di partenza e di arrivo;<corners>indica di quanto devono essere arrotondati gli angoli.
Questo del disegno in XML è uno dei settori più vasti in assoluto tra le risorse Android quindi è necessario studiare la documentazione per ulteriori approfondimenti. Ciò che conta qui è dimostrare come la creazione di uno stile possa essere rapida.
Appena creati questi due file (res/values/row_style.xml e res/drawable-mdpi/row_background.xml) si può applicare il nuovo stile, di nome rowstyle, alla TextView del layout precedente semplicemente assegnando il nome dello stile all'attributo style:
<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" style="@style/rowstyle"/>
Ereditarietà tra stili
Un aspetto importante degli stili Android è l'ereditarietà. Non dobbiamo necessariamente partire da zero nel creare uno stile ma possiamo renderli, per così dire, uno il derivato di un altro.
L'esempio precedente potrebbe essere rivisto così:
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <style name="rowstyle" parent="@style/rowbackground"> <item name="android:textColor">#FFFFFF</item> <item name="android:textSize">25sp</item> <item name="android:textStyle">bold</item> </style> <style name="rowbackground"> <item name="android:background">@drawable/row_background</item> <item name="android:paddingLeft">15dp</item> </style> </resources>
Esistono ora due stili:
rowbackgroundche gestisce solo il background;rowstyleche contiene le proprietà relative al testo ma con l'attributo parent dichiara di essere “figlio di rowbackground”. Per questo rowstyle includerà in sé tutte le valorizzazioni espresse nello stile-padre.
Il vantaggio di ciò è che si può stratificare la creazione di stili evitando di ripetere configurazioni simili in stili diversi. Potremmo, ad esempio, creare un altro stile per il testo ma sempre figlio di rowbackground. Questo nuovo stile creerebbe testo diverso da rowstyle ma con lo stesso sfondo.
Temi
Per tema, si intende in Android nient'altro che uno stile applicato ad un'Activity o all'intera applicazione. Il modo in cui si fa questo consiste nell'inserire l'attributo android:Theme nel file AndroidManifest.xml all'interno di un nodo <activity> o <application>.
Il valore di android:Theme sarà l'id di una risorsa stile definita come precedentemente spiegato.
23. Creare Custom Adapter
L'Adapter è un pattern già presentato nel corso di questa guida. È il meccanismo per impostare agevolmente la visualizzazione di oggetti Java su un layout di un'app Android.
Finora le accoppiate AdapterView-Adapter utilizzate negli esempi sono state ListView-ArrayAdapter o GridView-ArrayAdapter.
Di Adapter ne esistono molti nel framework. La documentazione offre tutti i dettagli in merito ma spesso capita di aver bisogno di creare una visualizzazione personalizzata.
In questi casi, si può realizzare un Adapter in versione custom ed in questa lezione vedremo come. Studiare questa casistica fornisce il programmatore non solo di uno strumento utilissimo, ma anche di un'esperienza formativa molto significativa che permette di osservare il funzionamento di un Adapter “dall'interno”.
Nell'esempio che andremo ad utilizzare, un'Activity mostra un elenco di articoli. Potrebbe essere l'interfaccia di un NewsReader ma qui verrà trattata in maniera simulata. Gli oggetti Java che rappresentano articoli, i cui dati provengono dalle recenti pubblicazioni di HTML.it, sono prodotti da un metodo di supporto interno all'Activity.
Ogni articolo, nel progetto, è rappresentato da un oggetto di classe ArticlesInfo i cui membri rappresentano, rispettivamente, il titolo dell'articolo, la categoria tematica di appartenenza e la data di pubblicazione:
public class ArticleInfo { private String title; private String category; private Date date; /* * OMISSIS: la classe possiede tutti i setter e i getter * per gestire i membri privati * * */ }
Affinchè ognuno di questi oggetti, per così dire, si trasformi in una riga della ListView contenuta nell'Activity prepariamo subito un layout nel file res/layout/listactivity_row_article.xml::
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="200dp" android:background="@drawable/row_background" android:descendantFocusability="blocksDescendants" android:padding="10dp"> <LinearLayout android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:orientation="vertical" android:id="@+id/ll_text" android:layout_toLeftOf="@+id/btn_bookmark"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" style="@style/big_textstyle" android:id="@+id/txt_article_description"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" style="@style/small_textstyle" android:id="@+id/txt_article_url"/> </LinearLayout> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLength="5" style="@style/small_textstyle" android:layout_alignParentRight="true" android:layout_alignParentTop="true" android:id="@+id/txt_article_datetime"/> </RelativeLayout>
Nel layout appena riportato sono presenti riferimenti a stili e drawable di sfondo di cui non viene offerto qui il listato ma che ricalcano perfettamente ciò che è stato spiegato in merito in questa guida. Importante ricordare che questo layout, rappresenta la forma di una singola riga che apparirà nell'AdapterView.
L'elemento che si occuperà della trasformazione di ogni oggetto ArticlesInfo in una View sarà proprio l'Adapter: ma non uno standard, uno personalizzato creato da noi.
Per fare questo dobbiamo:
- Creare una classe che chiameremo
ArticlesAdapter, estensione diBaseAdapter; - Fare in modo che la classe possieda un riferimento alla struttura dati da visualizzare, magari passato tramite costruttore. Nel nostro caso sarà una List
<ArticlesInfo>; - Implementare obbligatoriamente i metodi astratti di BaseAdapter:
getCount(): restituisce la quantità di elementi presenti nella struttura dati;getItem(int position): restituisce un Object che rappresenta l'elemento della struttura dati nella posizione position;getItemId(int position): restituisce un id univoco per l'elemento in posizione position;getView(int position, View v, ViewGroup vg): restituisce la View che costituirà una riga della ListView. La View sarà struttura secondo il layout listactivity_row_article e popolata con i dati dell'elemento in posizione position.
Vediamo il codice dell'Adapter:
{ private List<ArticleInfo> articles=null; private Context context=null; private SimpleDateFormat simple=new SimpleDateFormat("dd/MM",Locale.ITALIAN); public ArticlesAdapter(Context context,List<ArticleInfo> articles) { this.articles=articles; this.context=context; } @Override public int getCount() { return articles.size(); } @Override public Object getItem(int position) { return articles.get(position); } @Override public long getItemId(int position) { return getItem(position).hashCode(); } @Override public View getView(int position, View v, ViewGroup vg) { if (v==null) { v=LayoutInflater.from(context).inflate(R.layout.listactivity_row_article, null); } ArticleInfo ai=(ArticleInfo) getItem(position); TextView txt=(TextView) v.findViewById(R.id.txt_article_description); txt.setText(ai.getTitle()); txt=(TextView) v.findViewById(R.id.txt_article_url); txt.setText(ai.getCategory()); txt=(TextView) v.findViewById(R.id.txt_article_datetime); txt.setText(simple.format(ai.getDate())); return v; } }
Come si può vedere, i metodi non sono particolarmente complicati ma su getView vale la pena soffermarsi un attimo.
Al suo interno, per prima cosa, viene controllato se la View passata in input è nulla e solo in questo caso viene inizializzata con il LayoutInflater. Questo aspetto è molto importante ai fini della salvaguardia delle risorse infatti Android riciclerà quanto possibile le View già create. Il LayoutInflater attua per i layout quello che abbiamo già visto fare per i menu con il MenuInflater. In pratica la View da creare verrà strutturata in base al “progetto” definito nel layout XML indicatogli.
Dopo il blocco if, la View non sarà sicuramente nulla perciò procederemo al completamento dei suoi campi. I dati verranno prelevati dall'oggetto ArticleInfo di posizione position recuperato mediante getItem, già implementato. Al termine, getView restituirà la View realizzata.
Questo Adapter incarnerà tutta la logica di trasformazione infatti per il resto l'Activity è molto semplice. Tra l'altro, estende la classe ListActivity che ha un layout costituito da una ListView e alcuni metodi per la sua gestione:
getListView()per recuperare un riferimento alla ListView;setListAdapter()egetListAdapter()per ottenere accesso all'Adapter.
public class MainActivity extends ListActivity { private ArticlesAdapter adapter=new ArticlesAdapter(this, generateNews()); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getListView().setPadding(10, 10, 10, 10); setListAdapter(adapter); } private List<ArticleInfo> generateNews() { ArrayList<ArticleInfo> list=new ArrayList<ArticleInfo>(); Calendar c=Calendar.getInstance(); ArticleInfo tmp=new ArticleInfo(); tmp.setTitle("WordPress: integrare un pannello opzioni nel tema"); tmp.setCategory("CMS"); c.set(2014,3,23); tmp.setDate(new Date(c.getTimeInMillis())); list.add(tmp); /* * OMISSIS: il codice crea altri oggetti "fittizi" da visualizzare * */ return list; } }
Come si è visto la realizzazione di un Adapter personalizzato non presenta eccessive difficoltà ma offre grandi potenzialità. Nel layout che struttura il singolo item dell'AdapterView si può inserire qualunque controllo visuale e ciò evita che il programmatore si trovi costretto a scendere a compromessi per la realizzazione della propria interfaccia utente.
Infine, nell'esempio si è fatto uso di una ListView ma un Adapter custom può lavorare con qualunque AdapterView.
24. Fragment in Android
I Fragment costituiscono senz'altro uno dei più importanti elementi per la creazione di una interfaccia utente Android moderna. Il loro ruolo a partire da Android 3.0 è diventato preponderante tanto che ormai rappresentano una delle conoscenze più importanti per il programmatore.
Un Fragment è una porzione di Activity. Ma si faccia attenzione a comprenderne bene il ruolo. Non si tratta solo di un gruppo di controlli o di una sezione del layout. Può essere definito più come una specie di sub-activity con un suo ruolo funzionale molto importante ed un suo ciclo di vita.
Fragments e Activity
Definiamo subito il rapporto tra Fragments e Activity.
Prima di tutto un Fragment non può vivere senza un'Actvity. Tipicamente nei nostri programmi creeremo più Fragments che si alterneranno nel layout mentre di Activity ne sarà sufficiente una (ma possono essere anche di più).
Come detto il Fragment ha il suo ciclo di vita fortemente collegato con quello dell'Activity di appartenenza.
La figura qui riportata mostra la sequenza di stati che scandiscono la vita del Fragment.
Come si vede ricordano molto quelli dell'Activity.
La fase più variegata è l'inizializzazione del fragment:
- onAttach: segnala il momento in cui il Fragment scopre l'Activity di appartenenza. Attenzione che a quel punto l'Activity non è stata ancora creata quindi si può solo conservare un riferimento ad essa ma non interagirvi;
- onCreate: è la creazione del Fragment in quanto componente;
- onCreateView: il programmatore vi lavorerà spesso. È il momento in cui viene creato il layout del Fragment. Solitamente qui si fa uso del LayoutInflater;
- onActivityCreated: segnala che la creazione dell'Activity è stata completata, vi si può interagire in tutto e per tutto.
Gli altri metodi di callback del ciclo di vita vengono chiamati in corrispondenza degli omonimi metodi dell'Activity.
Hello Fragment!
Come abbiamo fatto per le Activity, anche per i Fragment inizieremo con un “Hello World”. In questo caso, lo scopo dell'esempio non sarà, ovviamente, tanto l'apparizione del messaggio di saluto quanto osservare le fasi che portano alla creazione di un Fragment e del suo innesto all'interno di un layout.
Per raggiungere lo scopo, seguiremo questi step che tratteranno sia XML, i primi due, che Java, i secondi due :
- creeremo il layout per l'Activity in cui ricaveremo il posto per alloggiare il nostro Fragment;
- definiremo il layout del Fragment che conterrà il vero aspetto dell'Activity e, di conseguenza, anche la stringa “Hello World”;
- definiremo la classe Fragment che essenzialmente servirà a caricare il layout di cui al punto precedente;
- creeremo la nostra Activity che svolgerà per lo più il ruolo di bacino di Fragment.
Il primo frammento di codice mostra il layout del Fragment (file: res/layout/fragment_main.xml).
Come si vede, se fosse stato destinato ad un'Activity sarebbe stato identico. Quindi la novità architetturale dei Fragment non influenza il layout.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="@dimen/activity_vertical_margin"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello_world" /> </RelativeLayout>
Il layout dell'Activity è il seguente (file: res/layout/activity_main.xml):
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent"/>
Tutto lo spazio disponibile verrà riempito da un layout che non abbiamo mai usato sinora: il FrameLayout.
Viene utilizzato quando vi si deve ospitare un unico elemento, in questo caso il Fragment. Fondamentale definire l'id in quanto questo Layout svolgerà il ruolo di contenitore del Fragment e pertanto verrà invocato dal codice Java.
La classe Fragment mostra evidenti alcune caratteristiche:
- estende la classe Fragment del framework;
- presenta metodi propri del ciclo di vita dei Fragment. Come già accennato, sarà frequente l'ovverride del metodo
onCreateViewin quanto è il momento in cui viene allestita l'interfaccia utente mostrata dal Fragment. Non ci sorprendono (ormai) le operazioni svolte al suo interno: assegnazione di un Layout mediante LayoutInflater.
public class HelloFragment extends Fragment { public HelloFragment() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main, container, false); return rootView; } }
Il codice dell'Activity contiene solo il metodo onCreate. Al suo interno, vengono svolte nelle prime due righe le operazioni consuete ma questa volta il caricamento del layout con setContentView non basta. Infatti questo porterà a display solo il FrameLayout ancora vuoto.
Per aggiungere il Fragment, si procederà per via dinamica richiedendo al FragmentManager l'avvio di una transazione add che aggiungerà il nuovo Fragment di classe HelloFragment al layout identificato da R.id.container.
public class MainActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .add(R.id.container, new HelloFragment()).commit(); } } }
L'operazione add fa parte delle FragmentTransactions. Le useremo anche nel prossimo capitolo ma intanto si pensi ad esse come delle operazioni che devono essere svolte dal FragmentManager sui Fragments amministrati.
Le FragmentTransactions più comuni sono:
add | Aggiunge un Fragment all'Activity |
remove | Rimuove un fragment precedentemente aggiunto |
replace | Sostituisce un Fragment con un altro |
hide | Nasconde un fragment |
show | Mostra un fragment precedentemente nascosto |
Argomenti e dettagli ulteriori sono disponibili a sufficienza nella documentazione ufficiale.
Notare ancora che, come avviene per le transazioni nei database, le operazioni iniziano con un beginTransaction e vengono definitivamente salvate con un commit.
25. Layout adattabili Smartphone/Tablet in Android
I Fragments possono essere visti come blocchi componibili che permettono di rendere il layout adattabile al dispositivo. Se la frammentazione dei dispositivi rappresenta una problematica di rilievo per i programmatori Android, i Fragments rappresentano in buona parte una soluzione.
La figura (fonte: documentazione ufficiale Android) mostra due dispositivi di tipo diverso ed in configurazioni differenti:
- uno smartphone in portrait. Immaginiamolo con uno schermo piccolo, anche 3 pollici;
- un tablet, quindi schermo almeno da 7 pollici, posizionato in landscape.
I layout presenti su entrambi sono costituiti da due fragments, gli stessi due Fragment: FragmentA e FragmentB.
Con adeguate configurazioni delle risorse e qualche aggiunta al codice visto nel capitolo precedente possiamo creare anche noi un layout adattabile che riesca a mostrarsi in one-pane su smartphone e two-pane su tablet in landscape.
Configurazione delle risorse
Parlando delle risorse, avevamo accennato alla loro gestione multipiattaforma. È arrivato il momento di vederla al lavoro, costituirà il punto di partenza del nostro layout adattabile.
Creiamo due cartelle di risorse layout:
- layout-large-land che verrà usato solo per dispositivi con display large in posizione landscape;
- layout, la cartella di default. Verrà chiamata in causa per tutte le altre situazioni.
La configurazione multipla ha successo se in entrambe le cartelle mettiamo il file di layout con il medesimo nome, activity_main.xml.
Da questo momento, l'Activity cercherà sempre la risorsa R.layout.activity_main ma questa, in base alla configurazione del dispositivo, corrisponderà ora al file res/layout-large-land/activity_main.xml ora al file res/layout/activity_main.xml.
Vediamo entrambi i file di layout.
File 1: res/layout/activity_main.xml:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent"/>
Questo primo layout è identico a quello visto nel capitolo precedente. È un FrameLayout che ospiterà un fragment singolo assegnato dinamicamente con FragmentTransactions.
File 2: res/layout-large-land/activity_main.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment android:name="it.html.guida.gui.fragments.CountryFragment" android:id="@+id/countryfrag" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1"/> <fragment android:name="it.html.guida.gui.fragments.CityFragment" android:id="@+id/cityfrag" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2"/> </LinearLayout>
Nel layout two-pane, i Fragments appaiono insieme sin dall'inizio mentre le FragmentTransactions non dovranno più intervenire. Come segnaposti, abbiamo i tag <fragment>. Il loro attributo android:name indica quale tipo di Fragment dovrà posizionarsi in ogni collocazione.
Comunicazione tra Fragments
L'Activity svolge il ruolo di snodo funzionale e di comunicazione tra i due Fragments sia che essi appaiano contemporaneamente sia che si alternino sul display.
Affinchè i Fragments siano riutilizzabili in più contesti è necessario che non si conoscano tra loro né che conoscano l'Activity alla quale, comunque, devono essere collegati. “Massima coesione, minimo accoppiamento”: questo potrebbe essere lo slogan dei Fragments.
All'interno dei Fragment non verrà mai menzionata esplicitamente la classe di appartenenza dell'Activity .
Piuttosto verrà definita un'interfaccia che sarà implementata dall'Activity. Questa interfaccia costituirà il “protocollo” di comunicazione Fragment-Activity.
L'esempio: Paesi e città
L'esempio mette in pratica il classico modello master/detail. Il master è un fragment, classe CountryFragment, che mostra una lista di Paesi. La scheda detail invece è un CityFragment che mostra un elenco di città appartenenti al Paese selezionato nel master.
In base alle premesse iniziali, vogliamo che i Fragments si presentino accoppiati su schermi large in landscape e si alternino in tutti gli altri casi.
A scopo di esempio, la sorgente dati è fittizia. È totalmente contenuta in una classe CountryList, già usata nel capitolo sugli Spinner. Si invocheranno i metodi:
Collection<String> getCountries(): restituisce l'elenco dei Paesi;Collection<String> getCitiesByCountry(String country): restituisce un elenco di città situate nel Paese.
Selezionando un elemento nella lista dei Paesi, è necessario che l'elenco di città presente nell'altro fragment venga aggiornato. In tutto questo, l'Activity svolgerà il ruolo di mediatore.
public class CountryFragment extends ListFragment { interface OnFragmentEventListener { void selectCountry(String c); } private OnFragmentEventListener listener=null; private CountryList l=new CountryList(); private String[] countries=null; public CountryFragment() { countries=new String[l.getCountries().size()]; l.getCountries().toArray(countries); } @Override public void onAttach(Activity activity) { super.onAttach(activity); listener=(OnFragmentEventListener) activity; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { ArrayAdapter<String> adapter=new ArrayAdapter<String>((Context) listener,android.R.layout.simple_list_item_1,countries); setListAdapter(adapter); return super.onCreateView(inflater, container, savedInstanceState); } @Override public void onListItemClick(ListView lv, View v, int position, long id) { listener.selectCountry(countries[position]); } }
Il CountryFragment mostra alcune particolarità:
- contiene un'interfaccia OnFragmentEventListener. Se si osserva il metodo
onAttachsi vede che tale interfaccia verrà usata per riferirsi all'Activity senza dover usare esplicitamente la sua classe; - è stato esteso ListFragment che funziona in maniera simile al ListActivity. All'interno dell
'onCreateViewviene impostato l'Adapter; onListItemClickrappresenta il momento in cui si notifica all'Activity che è stato selezionato un Paese.
Il codice dell'Activity è il seguente:
public class MainActivity extends ActionBarActivity implements OnFragmentEventListener{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (findViewById(R.id.container) != null) { // Se è presente il FrameLayout con id container, // vuol dire che siamo in SINGLE-PANE perciò // è necessario aggiungere il Fragment con la transazione. // Se savedInstanceState non è nullo, non siamo alla // prima visualizzazione perciò non serve aggiungere il Fragment. if (savedInstanceState != null) return; getSupportFragmentManager().beginTransaction() .add(R.id.container, new CountryFragment()).commit(); } } @Override public void selectCountry(String c) { CityFragment cityFrag = (CityFragment) getSupportFragmentManager().findFragmentById(R.id.cityfrag); if (cityFrag != null && cityFrag.isInLayout()) { // Il Fragment delle città è già nel layout quindi // ne chiediamo solo l'aggiornamento. cityFrag.onSelectedCountry(c); } else { // Siamo in SINGLE-PANE, quindi le FragmentTransaction // operano lo switch tra Fragment. CityFragment frag= new CityFragment(); Bundle b=new Bundle(); b.putString("country", c); frag.setArguments(b); FragmentTransaction ft=getSupportFragmentManager().beginTransaction(); ft.replace(R.id.container, frag); ft.addToBackStack(null); ft.commit(); } } }
L'Activity si troverà esplicitamente a dover gestire l'esatta composizione del layout. Se si notano i commenti nel codice si vede come si procede:
onCreate: se nel layout è reperibile il layout con idR.id.containersi è per forza in one-pane;selectCountry: se si trova presente nel layout un CityFragment allora siamo in two-pane altrimenti si fa uso delle FragmentTransaction per reperirlo.
L'ultimo codice da mostrare è il CityFragment:
public class CityFragment extends ListFragment { private ArrayAdapter<String> adapter=null; private CountryList l=new CountryList(); public CityFragment() { } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); adapter=new ArrayAdapter<String>(getActivity(),android.R.layout.simple_list_item_1); setListAdapter(adapter); Bundle b=getArguments(); if (b!=null) { String c=b.getString("country"); onSelectedCountry(c); } } public void onSelectedCountry(String country) { adapter.clear(); adapter.addAll(l.getCitiesByCountry(country)); } }
Al suo interno, la richiesta di aggiornamento del layout viene fatta mediante l'invocazione di onSelectedCountry. Notare inoltre l'utilizzo degli arguments per passare valori nella comunicazione Fragment-Activity.
Conclusioni
Sicuramente questo è un esempio molto basilare ma che vuole fornire i rudimenti per poter creare applicazioni più complesse rese flessibili dall'attuazione delle regole viste sinora:
- Fragments collegati tra loro il minimo possibile per permetterne un maggior riutilizzo;
- Activity usata per lo più come snodo di comunicazione tra Fragments;
- risorse in grado di saparare all'origine i layout distinguendo con id diversi i contenitori dei Fragments;
- ogni classe Fragment deve avere sin dalla nascita uno scopo ben preciso e contenere in sé tutta la logica necessaria per raggiungerlo in maniera indipendente.
Lo storage
26. Gestire i file
Tutte le applicazioni Android hanno la possibilità di interagire con il filesystem delle memorie di massa installate nel dispositivo, spesso costituite da SD card.
In questo capitolo vedremo come possono essere gestite sia nel caso di supporti fissi che rimovibili. Il requisito fondamentale per il loro utilizzo è una buona conoscenza del sottosistema di I/O del linguaggio Java, specialmente del suo concetto fondante, gli Stream.
Se il lettore ha già confidenza con questi strumenti non troverà grosse difficoltà nel seguire il discorso ed i principali aspetti di novità che potrà riscontrare saranno gli accorgimenti presi per l'adattamento di queste tecniche alla filosofia del sistema Android.
Distinzione basilare: Internal Storage vs. External Storage
La prima distinzione da imparare è quella tra lo spazio interno all'applicazione e quello esterno. Parte di questa distinzione verte su quel concetto di riservatezza dei dati propri di un'applicazione di cui si è discusso al principio di questa guida.
Ogni app ha a disposizione uno spazio disco, detto Storage interno (o Internal Storage) che risiede in una parte del filesystem e a cui solo l'applicazione dovrebbe accedere. Il percorso in Android che porta in questa posizione è /data/data/package_java_della_applicazione/files.
Il percorso è in formato Linux quindi lo '/' iniziale indica la root del filesystem. Notare che in /data/data lo spazio dell'applicazione prende il nome dal package Java. Se ad esempio la nostra app avesse package Java it.html.guida, i file salvati nello storage interno sarebbero collocati in: /data/data/it.html.guida/files.
Per External Storage, Android sceglie una posizione in cui collocare tutto un insieme di risorse che sono di utilità e consultazione generale nel dispositivo (musica, suonerie, film e così via). Solitamente dovrebbe essere collocato su una SD card esterna al sistema ma non è detto che sia un supporto rimovibile, potrebbe essere interno al dispositivo e fisso.
Ora si entrerà nei dettagli di entrambe le tipologie di storage.
Storage interno
Per accedere allo storage interno si usano per lo più due metodi, entrambi appartenenti al Context:
FileInputStream openFileInput(String filename): apre un file in lettura. Come parametro in input viene passato il nome del file. Non serve specificare il percorso in quanto sarà obbligatoriamente quello messo a disposizione dallo storage interno;FileOutputStream openFileOutput(String filename, int mode): apre uno stream in scrittura anche questo nello storage interno. Per il nome del file, vale quanto detto per l'input. A proposito del secondo parametro, si può impostare alla costante Context.MODE_APPEND per concatenare i nuovi contenuti a quelli già esistenti nel file.
Notare che sono disponibili anche due costanti MODE_WORLD_READABLE e MODE_WORLD_WRITEABLE che servono a rendere il file accessibile anche al di fuori dello storage interno. Questi valori sono stati deprecati in quanto non in linea con il principio di riservatezza dei dati interni all'applicazione.
Una volta ottenuto uno Stream, in input o in output che sia, va gestito come normale classe Java per procedere alle operazioni, rispettivamente, di lettura o scrittura.
Storage esterno
Anche per lo Storage esterno, le operazioni su filesystem si svolgono mediante Stream e le consuete classi Java. L'accesso avverrà mediante la classe Environment.
La prima operazione da svolgere è controllare lo stato del supporto. Lo si fa con il metodo statico String Environment.getExternalStorageState().
La stringa restituita può avere una molteplicità di valori, tutti associati a costanti della classe Environment. Evitando di elencarli tutti, teniamo presente solo che due valori in particolare ci dicono che il supporto può essere usato:
- Environment.MEDIA_MOUNTED: il caso migliore. Supporto disponibile in lettura/scrittura;
- Environment.MEDIA_MOUNTED_READ_ONLY: il supporto è disponibile ma solo in lettura.
Tutti gli altri valori indicano situazioni problematiche da valutare a seconda delle circostanze.
Una volta controllato lo stato del supporto e del relativo filesystem, è arrivato il momento di lavorarci direttamente. L'accesso alla cartella root dello Storage esterno primario si ottiene con il metodo statico: File getExternalStorageDirectory().
Dal riferimento all'oggetto File ottenuto, è possibile, secondo le procedure Java, leggere i contenuti, lavorare sui dati e via dicendo.
È sconsigliabile salvare file direttamente nella cartella principale dello storage esterno, normalmente esso contiene delle cartelle associate alle principali tipologie di contenuti:
- Alarms, per i suoni da abbinare agli allarmi;
- Download, per i file scaricati;
- Movie, per i film;
- Music, per i file musicali;
- Notifications, per i suoni delle notifiche;
- Pictures, per le foto;
- Podcasts, per i file di podcast;
- Ringtones, per le suonerie.
Ciò non impedisce ovviamente che ve ne possano essere altre.
Quella della gestione dei file è il primo caso di persistenza che incontriamo in questa guida. Eppure per il programmatore riveste una grande importanza, soprattutto perchè serve a gestire foto, musica, suonerie e tanti altri dati dalla forte connotazione personale destinati ad intrecciarsi indissolubilmente con la vita dell'utente-tipo.
27. Memorizzare informazioni con SharedPreferences
L'utilizzo dei file come collocazione di dati persistenti è molto duttile. La possibilità di salvare dati grezzi mediante Stream apre la strada a moltissime possibilità. Si possono salvare file binari, con codifiche proprie, serializzare strutture dati Java o ricorrere a formati “a caratteri” come CSV o altro ancora.
A volte però, si ha solo bisogno di salvare dati in locale, magari di tipo primitivo, come password, indirizzi IP, numeri o altre informazioni di configurazione piuttosto elementari. In questi casi più che confrontarsi con la varietà offerta dai file farebbe comodo una specie di mappa in cui salvare coppie chiave/valore con la possibilità di renderla persistente su disco.
Tutto ciò è disponibile e prende il nome di SharedPreferences.
I dati collocati nelle SharedPreferences vengono salvati in un file XML contenuto nello storage interno, precisamente nella cartella shared_prefs. Il file in cui sono contenute può avere un nome di default o assegnato dal programmatore pertanto di potrà accedere alle Preferences in due modi:
- nella modalità di default con il metodo
getPreferences(). Questo metodo è concepito per salvare dati privati dell'Activity. Il nome di default prodotto per il file richiamerà infatti il nome dell'Activity; - specificando un nome di file con
getSharedPreferences(String filename, int mode)dove il primo parametro indica il nome che si vuole dare al file di preferenze mentre il secondo sono i priviligi da concedere al file.
Entrambi i metodi restituiranno un oggetto SharedPreferences sul quale si potrà agire come su di una mappa.
Si noterà subito che un oggetto SharedPreferences contiene tipici metodi di lettura come:
contains(String key)che verifica se una proprietà con una certa etichetta è già stata salvata;- una serie di metodi getter come
getInt,getString,getFloate via dicendo con i quali si potrà recuperare i dati salvati per una certa chiave.
Tra gli altri, si vedrà anche un metodo edit() che restituisce un oggetto di tipo Editor. Questa classe è il meccanismo di modifica delle SharedPreferences.
Dall'Editor si avrà accesso a molti metodi put che permettono di modificare le proprietà. Al termine delle modifiche è molto importante che si richieda il salvataggio delle stesse invocando il metodo apply(), anch'esso dell'oggetto Editor.
Semplicità delle mappe, utilità della persistenza, se vi si aggiunge la frequenza in cui si necessita di salvare dati così semplici si comprende perchè le SharedPreferences siano considerate uno strumento importantissimo per il programmatore.
28. Database e SQLite
Il salvataggio di dati su file – visto nei capitoli precedenti – potrebbe essere sufficiente in molti casi. In fin dei conti le API di I/O fornite dal linguaggio Java permettono di trattare dati binari e testuali, salvare strutture dati serializzate ed altro ancora.
Qualcosa però a cui il programmatore è particolarmente abituato è il database relazionale e l'interazione mediante linguaggio SQL tanto da sentirne il bisogno anche in Android. L'evidenza di soddisfare questa necessità richiedeva che venisse individuato un prodotto dotato di determinati requisiti: open-source, ampiamente diffuso, mantenuto e documentato da una comunità prospera, efficiente e soprattutto che non richiedesse l'esecuzione di un servizio continuo in background. La soluzione esisteva già nel mondo del software libero e risiedeva in SQLite.
SQLite
SQLite è considerato il motore di database più diffuso al mondo. Rispetta tutti i requisiti di efficienza e disponibilità di cui si è detto.Si tratta, in realtà, di una libreria software che permette di gestire in un unico file un database relazionale.
Oltretutto è un progetto in continua espansione che mette a disposizione molti aspetti dei moderni DBMS: View, Trigger, transazioni, indici oltre al comunissimo e comodissimo interfacciamento con linguaggio SQL.
Nota: per chi non lo sapesse, SQL è il linguaggio per inviare comandi ad un database ed estrapolarne dati. È il formalismo con cui vengono realizzate le ben note query. In questa sede non ci si dilungherà sull'argomento ma se ne darà per assodata la conoscenza da parte del lettore. Qualora così non fosse, si ritiene opportuno un approfondimento su specifica documentazione.
Database nelle proprie App
Per avere un database SQLite nella propria App Android, non è necessario scaricare né installare niente: semplicemente basta chiedere. La libreria SQLite infatti è già inclusa nel sistema operativo e le API disponibili nel framework offrono tutto il supporto necessario.
Questi i passi:
- creare la struttura del database. Il programmatore dovrà preparare uno script SQL che crei la struttura interna del database (tabelle, viste ecc.). Nel realizzarla, potrà procedere nella maniera che gli è più congeniale scrivendola a mano o aiutandosi con uno strumento visuale come Sqliteman. L'importante è che alla fine di questa fase abbia una stringa contenente i comandi di creazione;
- creare una classe Java che estenda
SQLiteOpenHelper. Questa classe, che nel seguito chiameremo helper, servirà a gestire la nascita e l'aggiornamento del database su memoria fisica e a recuperare un riferimento all'oggetto SQLiteDatabase, usato come accesso ai dati; - creare una classe per l'interazione con il database. Solitamente questa ha due caratteristiche: (1) contiene un riferimento all'oggetto helper definito al punto precedente, (2) contiene i metodi con cui, dalle altre componenti dell'app, verranno richieste operazioni e selezioni sui dati.
Puntualizziamo che i tre step appena enunciati non sono assolutamente obbligatori, esistono infatti modalità alternative di azione. Sono tuttavia una prassi molto comune e funzionale per l'approntamento di un database a supporto di un'app. Tanto verrà dimostrato con l'esempio a seguire. Se ne consiglia perciò l'osservanza.
Esempio pratico
Verrà creata un'Activity che gestisce un piccolo scadenziario. I dati inseriti saranno costituiti da un oggetto, un testo che costituisce il vero promemoria ed una data.
Mettiamo subito in pratica i primi due step: creazione del database e della classe helper.
public class DBhelper extends SQLiteOpenHelper { public static final String DBNAME="BILLBOOK"; public DBhelper(Context context) { super(context, DBNAME, null, 1); } @Override public void onCreate(SQLiteDatabase db) { String q="CREATE TABLE "+DatabaseStrings.TBL_NAME+ " ( _id INTEGER PRIMARY KEY AUTOINCREMENT," + DatabaseStrings.FIELD_SUBJECT+" TEXT," + DatabaseStrings.FIELD_TEXT+" TEXT," + DatabaseStrings.FIELD_DATE+" TEXT)"; db.execSQL(q); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }
Per questioni “organizzative” del codice, i nomi dei campi e della tabella sono stati definiti in costanti nella seguente classe:
public class DatabaseStrings { public static final String FIELD_ID="_id"; public static final String FIELD_SUBJECT="oggetto"; public static final String FIELD_TEXT="testo"; public static final String FIELD_DATE="data"; public static final String TBL_NAME="Scadenze"; }
Notiamo che per prima cosa viene creato un costruttore al cui interno si invoca quello della classe base:
super(context, DBNAME, null, 1);
Tra gli argomenti passati ne notiamo due in particolare:
- il nome del database: è il secondo parametro, di tipo String, valorizzato con la costante DBNAME. Questo è il nome che il database avrà nello spazio disco dell'applicazione;
- la versione del database: è il quarto argomento, di tipo intero e valore 1.
Inoltre è stato fatto l'override di due metodi:
onCreate: viene invocato nel momento in cui non si trova nello spazio dell'applicazione un database con nome indicato nel costruttore. Da ricordare che onCreate verrà invocato una sola volta, quando il database non esiste ancora. Il parametro passato in input è un riferimento all'oggetto che astrae il database. La classe SQLiteDatabase è importantissima in quanto per suo tramite invieremo i comandi di gestione dei dati. Il metodo onCreate contiene la query SQL che serve a creare il contenuto del database. Questo è l'applicazione del primo step, enunciato prima. Notare che al suo interno non c'è alcun comandoCREATE DATABASEin quanto il database stesso è già stato creato dal sistema. Il comando SQL di creazione verrà invocato medianteexecSQL;onUpgrade: viene invocato nel momento in cui si richiede una versione del database più aggiornata di quella presente su disco. Questo metodo contiene solitamente alcune query che permettono di adeguare il database alla versione richiesta.
La classe in cui gestiremo il database prende il nome di DbManager, ne vediamo subito il codice:
public class DbManager { private DBhelper dbhelper; public DbManager(Context ctx) { dbhelper=new DBhelper(ctx); } public void save(String sub, String txt, String date) { SQLiteDatabase db=dbhelper.getWritableDatabase(); ContentValues cv=new ContentValues(); cv.put(DatabaseStrings.FIELD_SUBJECT, sub); cv.put(DatabaseStrings.FIELD_TEXT, txt); cv.put(DatabaseStrings.FIELD_DATE, date); try { db.insert(DatabaseStrings.TBL_NAME, null,cv); } catch (SQLiteException sqle) { // Gestione delle eccezioni } } public boolean delete(long id) { SQLiteDatabase db=dbhelper.getWritableDatabase(); try { if (db.delete(DatabaseStrings.TBL_NAME, DatabaseStrings.FIELD_ID+"=?", new String[]{Long.toString(id)})>0) return true; return false; } catch (SQLiteException sqle) { return false; } } public Cursor query() { Cursor crs=null; try { SQLiteDatabase db=dbhelper.getReadableDatabase(); crs=db.query(DatabaseStrings.TBL_NAME, null, null, null, null, null, null, null); } catch(SQLiteException sqle) { return null; } return crs; } }
Prima cosa da notare: la classe contiene un riferimento al DbHelper.
I metodi che vengono implementati mostrano tre operazioni basilari da svolgere sulla tabella del db: save per salvare una nuova scadenza, delete per cancellarne una in base all'id, query per recuperarne l'intero contenuto.
Da questi metodi, emerge un modus operandi comune. Infatti per lavorare su un oggetto SQLiteDatabase, la prima cosa da fare è recuperarne un riferimento. Lo si può fare con i metodi di SQLiteOpenHelper, getReadableDatabase() e getWriteableDatabase() che restituiscono, rispettivamente, un riferimento al database “in sola lettura” e uno che ne permette la modifica.
Sull'oggetto SQliteDatabase recuperato, si svolge una delle quattro operazioni CRUD, le azioni fondamentali della persistenza (Create, Read, Update, Delete).
Nelle API Android per Sqlite esiste almeno un metodo per ogni tipo di azione:
query: esegue la lettura sulle tabelle: mette in pratica il SELECT sui dati. I suoi svariati overload predispongono argomenti per ogni parametro che può essere inserito in una interrogazione di questo tipo (selezione, ordinamento, numero massimo di record, raggruppamento, etc.);delete: per la cancellazione di uno o più record della tabella;insert: per l'inserimento. Riceve in input una stringa che contiene il nome della tabella e la lista di valori di inizializzazione del nuovo record mediante la classe ContentValues. Questa è una struttura a mappa che accetta coppie chiave/valore dove la chiave rappresenta il nome del campo della tabella;update: esegue modifiche. Il metodo associa i parametri usati nell'insert e nel delete.
Tutti questi metodi non richiedono un uso esplicito di SQL. Chi ne avesse bisogno o preferisse per altre ragioni scrivere totalmente i propri comandi e query può utilizzare metodi di SqliteDatabase come execSQL e rawQuery.
Vale anche la pena sottolineare che i metodi appena indicati offrono una versione “parametrica” delle condizioni di selezione dei record (la classica clausola WHERE di SQL che spesso è indispensabile in selezioni, cancellazioni e aggiornamenti). Ciò è visibile nella classe DbManager, nel metodo che si occupa della cancellazione:
db.delete(DatabaseStrings.TBL_NAME, DatabaseStrings.FIELD_ID+"=?", new String[]{Long.toString(id)})>0
In questi casi, la classe SQLiteDatabase vuole che una stringa raccolga la parte fissa del contenuto della clausola WHERE sostituendo le parti variabili con punti interrogativi. Gli argomenti attuali verranno passati ad ogni invocazione in un array di stringhe. Nell'esecuzione della query ogni punto interrogativo verrà, in ordine, sostituito con un parametro dell'array.
Altra classe cui fare attenzione, è Cursor. Rappresenta un puntatore ad un set di risultati della query. Somiglia a quell'elemento che in altre tecnologie prende il nome di RecordSet o ResultSet. Un oggetto Cursor può essere spostato per puntare ad una riga differente del set di risultati. Ciò viene fatto con i metodi moveToNext, moveToFirst, moveToLast e così via.
Una volta che il cursore ha raggiunto la riga desiderata si può passare alla lettura dei dati con metodi specifici in base al tipo di dato (getString, getLong ecc.) indicando il nome del campo.
Ad esempio, se l'oggetto crs di classe Cursor punta ad un insieme di righe della tabella Scadenze, una volta indirizzato sulla riga desiderata si potrà leggere il campo relativo all'oggetto con:
crs.getString(crs.getColumnIndex(DatabaseStrings.FIELD_SUBJECT))
Con getColumnIndex viene trovato l'indice del campo.
L'Activity ed il CursorAdapter
L'interfaccia utente che si occuperà di interagire con il db è molto semplice.
Costituita da un form per l'inserimento di nuove scadenze e da una ListView sottostante che mostra i record presenti nel db, permette tuttavia di sperimentare le funzionalità sinora descritte.
public class MainActivity extends Activity { private DbManager db=null; private CursorAdapter adapter; private ListView listview=null; private OnClickListener clickListener=new View.OnClickListener() { @Override public void onClick(View v) { int position=listview.getPositionForView(v); long id=adapter.getItemId(position); if (db.delete(id)) adapter.changeCursor(db.query()); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); db=new DbManager(this); listview=(ListView) findViewById(R.id.listview); Cursor crs=db.query(); adapter=new CursorAdapter(this, crs, 0) { @Override public View newView(Context ctx, Cursor arg1, ViewGroup arg2) { View v=getLayoutInflater().inflate(R.layout.listactivity_row, null); return v; } @Override public void bindView(View v, Context arg1, Cursor crs) { String oggetto=crs.getString(crs.getColumnIndex(DatabaseStrings.FIELD_SUBJECT)); String data=crs.getString(crs.getColumnIndex(DatabaseStrings.FIELD_DATE)); TextView txt=(TextView) v.findViewById(R.id.txt_subject); txt.setText(oggetto); txt=(TextView) v.findViewById(R.id.txt_date); txt.setText(data); ImageButton imgbtn=(ImageButton) v.findViewById(R.id.btn_delete); imgbtn.setOnClickListener(clickListener); } @Override public long getItemId(int position) { Cursor crs=adapter.getCursor(); crs.moveToPosition(position); return crs.getLong(crs.getColumnIndex(DatabaseStrings.FIELD_ID)); } }; listview.setAdapter(adapter); } public void salva(View v) { EditText sub=(EditText) findViewById(R.id.oggetto); EditText txt=(EditText) findViewById(R.id.testo); EditText date=(EditText) findViewById(R.id.data); if (sub.length()>0 && date.length()>0) { db.save(sub.getEditableText().toString(), txt.getEditableText().toString(), date.getEditableText().toString()); adapter.changeCursor(db.query()); } } }
L'Activity gestisce l'interazione con il database appellandosi all'oggetto DbManager istanziato. Il metodo salva viene invocato al click del pulsante del form mentre l'oggetto OnClickListener serve ad ogni pulsante di cancellazione presente sulle righe della ListView.
Fin qui niente di particolarmente sorprendente. L'elemento di maggiore novità è l'Adapter che è stato usato: il CursorAdapter. Il suo scopo è trasformare ogni riga del risultato della query in una View.
Nell'esempio, il layout usato per mostrare la singola riga è il seguente:
<RelativeLayout android:layout_height="wrap_content" android:layout_width="400dp" xmlns:android="http://schemas.android.com/apk/res/android"> <LinearLayout android:layout_height="wrap_content" android:layout_width="match_parent" android:padding="5dp" android:layout_toLeftOf="@+id/btn_delete" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" style="@style/big_textstyle" android:id="@+id/txt_subject"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" style="@style/small_textstyle" android:id="@+id/txt_date"/> </LinearLayout> <ImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:src="@android:drawable/ic_menu_delete" android:id="@+id/btn_delete" /> </RelativeLayout>
Il CursorAdapter lo tratterà mediante override di due metodi in particolare:
newView: crea la View cui associare i dati prelevati dal database. Ciò viene fatto, in questo e molti altri casi, mediante inflating;bindView: riceve in input una View, da completare con i dati di un singolo record del cursore, e il Cursor già posizionato sulla riga giusta. Grazie all'implementazione di newView, questo metodo riceverà sempre una View pronta, creata per l'occasione o “riciclata” da quelle già esistenti. Le operazioni dominanti in bindView riguarderanno il recupero di controlli presenti nella View ed il loro completamento con i dati presi dal Cursor.
Ciò che viene fatto all'interno dei predetti metodi non dovrebbe stupire più in quanto sono le stesse operazioni fatte per gli Adapter customizzati già presentati in questa guida.
Un terzo metodo frutto di override nel CursorAdapter è getItemId. Fornisce l'id del record in base alla posizione e viene usato per completare le condizioni di selezione richieste per la cancellazione.
29. Condividere i dati con i Content Provider
Uno dei principi che è più volte riemerso nel corso di questa guida è la riservatezza dei dati dell'applicazione. Lo si è anticipato già nei primi capitoli e lo si è sperimentato studiando i metodi di persistenza: i file ed i database creati devono essere utilizzati solo dall'applicazione cui appartengono. In effetti, l'esistenza dello stesso Internal Storage ne è già una conferma.
Ma allora – ci si potrebbe chiedere – come possono le applicazioni condividere i propri dati con il resto del sistema?
Un meccanismo di condivisione esiste ed è rappresentato dai ContentProvider, una delle quattro componenti delle app Android, oltre ad Activity, Service e BroadcastReceiver.
Questo capitolo si occuperà di mostrare le caratteristiche di base di un ContentProvider e di come sia possibile crearne uno nella propria applicazione. L'esempio che ci guiderà in questo percorso sarà la “trasformazione” in ContentProvider del database a supporto dello scadenziario visto nel capitolo precedente.
Funzionamento di base di un ContentProvider
I ContentProvider sono riconoscibili mediante un URI, un riferimento univoco. Questi indirizzi sono costituiti da due parti:
- authority: è il nome del Provider in generale. Spesso, per evitare conflitti, richiama il nome del package Java di appartenenza;
- path: costituisce un percorso interno alla singola authority. Spesso rappresenta una tabella del database cui si vuole accedere ma, in generale, indica la tipologia di dati su cui agire.
Un ContentProvider, come avviene per le Activity, deve essere definito in due step:
- creare una classe Java che estenda ContentProvider;
- creare un nodo <provider> all'interno dell'AndroidManifest.xml. Servirà ad associare la classe Java che implementa il Provider (definita al punto precedente) e l'authority scelta per gli URI.
Appena definita la classe ContentProvider, viene richiesto di implementarne i metodi astratti, tra cui query, update, delete e insert. Definiscono le operazioni CRUD ed i parametri richiesti ricordano in tutto – tipologia e funzioni – quelli usati per i database come illustrato nel capitolo precedente. La differenza sarà la mancanza del nome della tabella, sostituita dall'indicazione dell'URI.
Svolti questi passi, qualunque applicazione nel sistema sarà in grado di accedere alla sorgente dati sottesa al ContentProvider.
Sarà sufficiente fare accesso ad un oggetto di sistema, detto ContentResolver e chiedergli di svolgere operazioni sugli URI conosciuti del ContentProvider. Ogni operazione CRUD che verrà chiesta al ContentResolver sarà convertita in una chiamata al corrispondente metodo CRUD della classe ContentProvider.
Il ContentResolver sarà in grado di stabilire la relazione tra URI e classe Java grazie ai dati registrati nel manifest dell'applicazione.
Adattamento del database a ContentProvider
Per attuare l'adattamento del database a ContentProvider, iniziamo con la definizione degli URI. Scegliamo il package Java (“it.html.guida.database”) come authority e che tutti i path inizino con il segmento ”/scadenze”. Queste sono decisioni che spettano al programmatore.
Le azioni che potranno essere invocate sul ContentProvider sono le stesse del precedente capitolo:
- l'elenco delle scadenze: una query che recupera tutto il contenuto della tabella;
- inserire una nuova scadenza;
- richiede la cancellazione di una scadenza riconosciuta mediante l'id;
e questi gli URI presso i quali potranno, rispettivamente, essere richieste:
- content:// it.html.guida.database/scadenze/lista
- content:// it.html.guida.database/scadenze/nuova
- content:// it.html.guida.database/scadenze/elimina
Il ContentProvider verrà implementato dalla classe BillBookProvider mentre la MainActivity rimarrà praticamente invariata. Non sarà più necessario usare oggetti DbManager per accedere ai dati. Il collegamento alla sorgente dati avverrà, in maniera “remota”, attraverso URI e ContentResolver.
All'atto pratico questo determinerà, innanzitutto, che il collegamento al helper sarà contenuto nella classe ContentProvider ed è qui che queste componenti dimostrano un aspetto molto importante in termini architetturali: il disaccoppiamento totale tra lo strato di presentazione (l'Activity) e il livello di accesso ai dati.
La registrazione del ContentProvider nell'AndroidManifest.xml avverrà così:
<application . . . . . .> <activity . . . . . ./> <provider android:name=".BillBookProvider" android:authorities="it.html.guida.database"/> </application>
associando authority e classe Java.
Ecco il ContentProvider:
public class BillBookProvider extends ContentProvider { private DBhelper dbhelper=null; @Override public boolean onCreate() { dbhelper=new DBhelper(getContext()); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder) { Cursor crs=null; try { SQLiteDatabase db=dbhelper.getReadableDatabase(); crs=db.query(DatabaseStrings.TBL_NAME, null, null, null, null, null, null, null); } catch(SQLiteException sqle) { return null; } return crs; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { SQLiteDatabase db=dbhelper.getWritableDatabase(); int res=-1; String id=uri.getLastPathSegment(); try { res=db.delete(DatabaseStrings.TBL_NAME, DatabaseStrings.FIELD_ID+"=?", new String[]{id}); } catch (SQLiteException sqle) { // Gestione delle eccezioni } return res; } @Override public Uri insert(Uri uri, ContentValues values) { SQLiteDatabase db=dbhelper.getWritableDatabase(); long id=-1; try { db.insert(DatabaseStrings.TBL_NAME, null,values); } catch (SQLiteException sqle) { return null; } return Uri.withAppendedPath(uri, Long.toString(id)); } @Override public int update(Uri uri, ContentValues values, String selection,String[] selectionArgs) { // modifica non implementata return 0; } @Override public String getType(Uri uri) { return null; } }
Non ripetiamo il codice dell'Activity perchè ha subito, come detto, modifiche minime. È interessante però notare come cambia l'accesso alla persistenza.
Senza ContentProvider, avremmo chiesto l'elenco delle scadenze così:
db=new DbManager(this); Cursor crs=db.query();
ora, non abbiamo più bisogno del DbManager, pertanto faremo questo:
Cursor crs=getContentResolver().query(Uri.parse("content://it.html.guida.database/scadenziario"), null, null, null, null);
Il riferimento al ContentResolver viene fornito dal Context. Analogamente potremo richiedere l'inserimento di una nuova scadenza all'interno del metodo salva:
ContentValues cv=new ContentValues(); cv.put(DatabaseStrings.FIELD_SUBJECT, sub.getEditableText().toString()); cv.put(DatabaseStrings.FIELD_TEXT, txt.getEditableText().toString()); cv.put(DatabaseStrings.FIELD_DATE, date.getEditableText().toString()); getContentResolver().insert(Uri.parse("content:// it.html.guida.database /scadenze/nuova"), cv);
o la cancellazione mediante id nell'implementazione di OnClickListener:
long id=adapter.getItemId(position); // id dell'elemento getContentResolver().delete(Uri.withAppendedPath(Uri.parse("content://it.html.guida.database /scadenze/elimina"), Long.toString(id)),null,null);
Ultima nota, lavorando con gli URI, è utile guardare con attenzione la documentazione per scoprire i vari metodi di utilità che si possono usare. Ne sono esempio parse e withAppendedPath qui utilizzati.
30. Accedere a ContentProvider
I dispositivi Android gestiscono molti dati come contatti, file archiviati su disco, eventi del Calendario. Tutte queste informazioni possono essere lette e modificate dalle nostre applicazioni mediante ContentProvider. Il carattere particolarmente “personale” di questi dati segnala quanto la programmazione di un dispositivo mobile a volte si allontani dall'astrazione dell'informatica e si intrecci fortemente con la vita reale dell'utente.
Nel capitolo precedente abbiamo visto le caratteristiche dei ContentProvider, funzionamento e validità architetturale. Qui apprezzeremo molto la standardizzazione dei meccanismi di accesso che mettono a disposizione, agevolando la consultazione di basi di dati spesso molto diverse tra loro.
Aspetti cui si dovrà porgere particolare attenzione:
- gli URI non sono più inventati dal programmatore ma questa volta sono parte delle API di accesso al ContentProvider. Il framework cercherà tuttavia di renderne semplice la conoscenza mediante apposite classi che prendono il nome di classi Contract. Essenzialmente si tratta di classi che contengono un gran numero di costanti, generalmente suddivise in sottoclassi, che forniscono i nomi delle tabelle dei dati, dei campi ed altro ancora sulla struttura della sorgente dati;
- solitamente sarà necessario aggiungere delle permission all'interno del file AndroidManifest.xml. Saranno probabilmente più di una, relative a lettura dei dati e alla scrittura.
Per quanto riguarda l'accesso, si userà ugualmente il ContentResolver ed i suoi metodi che permetteranno di mettere in pratica le quattro operazioni CRUD: lettura, inserimento, modifica e cancellazione.
Alcuni ContentProvider di sistema
ContentProvider molto noti del sistema opearativo sono:
- Contacts: include tutte le informazioni sui contatti dell'utente: rubrica telefonica, email, etc. Non se ne parlerà in questo capitolo. Troverà spazio in un contesto più adeguato come l'esplorazione del rapporto tra Android e telefonia;
- MediaStore: gestisce dati relativi a file multimediali contenuti nel sistema tra cui file audio, immagini e video;
- UserDictionary: si occupa delle parole aggiunte dall'utente al dizionario di default;
- Calendar: basato su API disponibili da Android 4 (IceCreamSandwich) e serve a gestire appuntamenti ed eventi sul calendario del dispositivo eventualmente sincronizzato con l'account Google. Verrà illustrato nel prossimo paragrafo.
Un esempio: gestire il Calendario
Le API Calendar sono state aggiunte in Android 4 e possono essere davvero utili per fare in modo che le nostre applicazioni possano aiutare gli utenti a gestire i propri impegni.
Per utilizzare questo provider è necessario innanzitutto aggiungere al manifest le permission richieste, dipendentemente dal tipo di operazioni che si vogliono svolgere (lettura e/o scrittura):
<uses-permission android:name="android.permission.READ_CALENDAR"/> <uses-permission android:name="android.permission.WRITE_CALENDAR"/>
Tutto il sistema ad oggetti del calendario verterà intorno alla classe Contract di competenza, ovviamente CalendarContract. Questa permetterà l'accesso ad una serie di tabelle, ognuna dedicata ad un aspetto. Ecco le più comuni:
CalendarContract.Calendars: contiene le informazioni su ogni Calendario. Nel sistema infatti possono essere disponibili più calendari, alcuni di carattere locale quindi utilizzabili solo sul dispositivo, altri legati ad un account Google. Quest'ultimo caso permetterà una sincronizzazione tra i dati presenti nel telefono e quelli remoti;CalendarContract.Events: è una delle tabelle più importanti ed elenca gli eventi memorizzati nei calendari. L'id del singolo evento servirà per stabilire un collegamento tra questa tabella e altre che ne specificano alcune sfaccettature come Reminders e Attendees;CalendarContract.Attendes: è collegata a Events e memorizza i partecipanti all'evento specificandone, tra l'altro, il nome e l'account;CalendarContract.Reminders: collegata anch'essa a Events, ogni sua riga simboleggia un alert o una notifica impostata per un evento. Ogni evento può avere anche più notifiche.
Vediamo ora alcuni esempi di codice per interagire con il nostro calendario.
Recuperare i calendari disponibili nel sistema:
String[] projection = new String[]{ Calendars._ID, Calendars.NAME, Calendars.ACCOUNT_NAME, Calendars.ACCOUNT_TYPE}; Cursor cursor = getContentResolver(). query(Calendars.CONTENT_URI, projection, Calendars.VISIBLE + " = 1", null, null);
Per vedere tutti i calendari che possono essere trovati nel sistema, come presumibile, è necessario inviare una query mediante ContentResolver. Si faccia caso che l'URI da utilizzare sarà fornito direttamente dalla classe Calendars e così anche per quanto riguarda i campi. Per il resto non c'è niente di nuovo. Il risultato sarà un normale Cursor che si potrà gestire nelle modalità consuete, lettura diretta o CursorAdapter per citarne alcuni.
Inserire un evento in un determinato calendario:
Una volta utilizzato il codice precedente abbiamo a disposizione tutti i calendari registrati nel dispositivo. Come si può vedere osservando l'array projection, tra i campi recuperati dalla query c'è l'ID. A questo si potrà inserire un nuovo evento nel sistema collegandolo ad un calendario fornendone l'ID.
Calendar cal = new GregorianCalendar(2014, 4, 20); cal.setTimeZone(TimeZone.getDefault()); cal.set(Calendar.HOUR, 15); cal.set(Calendar.MINUTE, 30); long dtstart = cal.getTimeInMillis(); ContentValues values = new ContentValues(); values.put(Events.DTSTART, dtstart); values.put(Events.DTEND, dtstart+3*3600*1000); // durata di tre ore values.put(Events.TITLE, "Riunione con il capo"); values.put(Events.CALENDAR_ID, id); values.put(Events.EVENT_TIMEZONE, TimeZone.getDefault().getDisplayName()); Uri uri = getContentResolver(). insert(Events.CONTENT_URI, values);
Nell'esempio abbiamo salvato un promemoria per la riunione con il capo per il 20 maggio. Abbiamo annotato anche che l'impegno durerà tre ore a partire dalle 15:30.
Il salvataggio effettuato si preoccuperà di inserire solo i dati minimi indispensabili ossia quelli assolutamente obbligatori per un evento non ripetitivo. Tra gli altri vediamo che nel ContentValues preparato per l'inserimento è stato collocato anche l'ID del calendario, informazione che può essere recuperata mediante il primo esempio di codice.
Aprendo successivamente l'applicazione Calendar sul dispositivo Android vedremo che effettivamente il promemoria per la riunione è stato salvato ed è visibile nell'immagine seguente.
Processi e servizi
31. Threading
Quando si affronta in maniera approfondita lo studio di una tecnologia di programmazione si sa che prima o poi si finisce col trattare un argomento piuttosto spinoso: i thread ed il loro impiego nella programmazione concorrente.
I thread costituiscono un filone di esecuzione interno ad un'applicazione. In pratica, sono la vera anima operativa di un programma a runtime. Solitamente un qualunque software – nel mondo mobile e non – vede contemporaneamente attivi più thread. Ognuno di essi svolgerà in maniera a sé stante una sequenza di operazioni, come se fosse “un programma nel programma”.
Che ruolo giocano i thread quindi nelle app Android e perchè dovremmo tenerli in considerazione? Si consideri che quando si utilizza un'Activity il thread principale dell'applicazione si occupa prevalentemente di gestire i messaggi relativi al funzionamento dell'interfaccia utente. Svolgere in questo stesso thread operazioni presumibilmente “lente” come ad esempio la lettura e scrittura da file, il prelevamento di dati da un database, il caricamento di immagini rischierebbe di rendere poco reattiva la UI. Conseguenza di ciò sarebbe una user exprerience non troppo gradevole che porterebbe l'utente a sostituire la nostra app con altre molto più scattanti.
La reattività dell'interfaccia è uno degli aspetti delle applicazioni più determinanti per il successo presso il pubblico e la longevità sui market.
Operazioni “lente” dovrebbero essere preferibilmente svolte su thread secondari, detti anche worker thread. Inoltre l'accesso in Rete, importantissimo nella programmazione moderna ma contraddistinto da tempi di latenza variabili, deve obbligatoriamente essere eseguito su un thread secondario.
I thread come in Java
Tutto ciò che si è appreso nello studio di Java sui thread (estensione della classe Thread, API della concorrenza, Executors) può essere utilizzato in Android.
Ad esempio, se in un'Activity volessimo distaccare alcune attività su un thread secondario sarebbe lecito usare una forma di questo tipo:
new Thread() { @Override public void run() { /* * Inserire QUI il codice * da svolgere nel thread secondario */ } }.start();
Ci sono due problemi però. Il primo è che il comune uso dei thread non è molto semplice da usare in maniera corretta. Il secondo è che, in Android, da un thread secondario non è possibile modificare l'interfaccia utente senza usare opportuni meccanismi di comunicazione.
Per fortuna il framework offre un'alternativa che permette di usare un thread secondario in maniera corretta senza il problema di dover gestire la comunicazione tra thread: la classe AsyncTask.
AsyncTask
Per illustrare la classe AsyncTask si mostrerà un esempio poco pratico ma dal forte significato concettuale.
Immaginiamo di avere un'Activity che contiene un solo pulsante. Al click di tale controllo viene attivato un lavoro in background. In questo caso si tratterà di un lavoro puramente fittizio: a scopo di esempio lasceremo il thread in attesa per alcuni secondi tanto per generare ritardo. A scandire i tempi dell'attività in background, ci sarà una finestra di dialogo di tipo ProgressDialog.
Il layout dell'activity è molto semplice, contiene infatti un solo pulsante collocato in posizione centrale.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:onClick="start" android:text="@string/btn_start" /> </RelativeLayout>
Il codice Java della classe invece è il seguente:
public class MainActivity extends Activity { ProgressDialog progress=null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); progress=new ProgressDialog(MainActivity.this); progress.setMax(100); progress.setMessage(getString(R.string.progress_msg)); progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progress.setCancelable(false); } public void start(View v) { new BackgroundTask().execute(); } private class BackgroundTask extends AsyncTask<Void, Integer, String> { @Override protected void onPreExecute() { super.onPreExecute(); progress.setProgress(0); progress.show(); } @Override protected String doInBackground(Void... arg0) { try { for(int i=0;i<10;i++) { publishProgress(new Integer[]{i*10}); Thread.sleep(1200); } } catch (InterruptedException e) {} return "Lavoro Terminato!"; } @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); progress.setProgress(values[0].intValue()); } @Override protected void onPostExecute(String result) { super.onPostExecute(result); progress.dismiss(); Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show(); } } }
Al click del pulsante viene invocato il metodo start() al cui interno si istanzia la classe BackgroundTask e la si manda in esecuzione direttamente. La classe BackgroundTask estende AsyncTask e pertanto rappresenta il cuore dell'esempio.
I metodi contenuti nella classe BackgroundTask sono di due tipi diversi. Il metodo doInBackground è l'unico di quelli implementati che viene eseguito su un thread secondario. Qui dovremo collocare tutte le operazioni “lente”.
Al contrario, onPreExecute, onPostExecute e onProgressUpdate sono eseguiti sul thread principale e si occupano della comunicazione tra thread.
Rispettivamente:
onPreExecute: inizializza le operazioni prima che avvenga l'esecuzione di doInbackground. Nello specifico prepara la ProgressDialog e la mostra;onPostExecute: viene eseguito alla fine di doInBackground ed anch'esso svolge operazioni collegate all'interfaccia utente, provocando l'apparizione di un Toast;onProgressUpdate: viene invocato ogni volta che dall'interno di doInBackground viene chiamato publichProgress. Serve a fornire aggiornamenti periodici all'interfaccia utente ed in questo caso a spostare la barra di progresso in avanti di un passo.
Un altro aspetto di AsyncTask cui si deve prestare attenzione sono i parametri della classe. Nell'esempio, BackgroundTask estende la versione <Void, Integer, String> di AsyncTask. Questi tre tipi di dato saranno, rispettivamente, il tipo di dato accettato in input dai metodi doInBackground, onProgressUpdate,onPostExecute.
L'immagine che segue mostra un momento di esecuzione dell'esempio.
Il discorso su thread ed AsyncTask non si è affatto esaurito, infatti saranno strumenti necessari già dai prossimi capitoli per la gestione di Service e attività in Rete.
33. Lavoriamo in backgroud con i Service
Dal capitolo precedente si è fatta la conoscenza di AsyncTask che, a suo modo, permette di avviare attività asincrone a supporto dell'interfaccia utente. I task gestiti da questa classe non dovrebbero essere molto lunghi, “a few seconds at the most” ('pochi secondi al massimo') come dichiara esplicitamente la documentazione ufficiale.
Per lavori di durata lunga o addiritura indeterminata, si deve ricorrere ad un'appropriata componente Android, i Service.
Per utilizzare un Service è necessario svolgere due operazioni:
- creare una classe Java che estenda Service o un suo derivato;
- registrare il service nel manifest con il nodo
<service><service android:name="LogService"/>
L'attributo android:name definisce quale classe Java implementa il service, in questo caso sarebbe la classe LogService.
Tipologie di Service
I Service sono classificabili in due tipologie, dipendentemente dal modo in cui vengono avviati:
- i service Started vengono avviati tramite il metodo
startService(). La loro particolarità è di essere eseguiti in background indefinitamente anche se la componente che li ha avviati viene terminata. Generalmente non offrono interazione con il chiamante e proseguono finchè non vengono interrotti con il metodostopServiceo si auto-interrompono constopSelf(); - i service Bound vivono in una modalità client-server. Hanno senso solo se qualche altra componente vi si collega. Vengono interrotti nel momento in cui non vi sono più client ad essi collegati.
Chiariamo subito che i service delle due categorie non sono radicalmente diversi. Ciò che li distingue è il modo in cui vengono avviati ed i metodi di callback implementati al loro interno. Uno stesso service può essere avviato in maniera started o bound.
Emerge comunque una differenza nei tipi di lavori che sono più consoni all'uno o all'altra categoria.
I Service Started sono da predilligere per operazioni con una loro finalità indipendente dallo stato delle altre applicazioni. Si potrebbero occupare di aggiornamenti dati in background, scaricamento di file o immagini, sincronizzazione remota verso server esterni, etc. Considerando che il service Started rimarrà in background a lungo la sua esistenza deve essere giustificata dalla finalità preposta.
I Service Bound svolgono il ruolo di supporto ad altre applicazioni. Non rischiano pertanto di essere “dimenticati” in background come potrebbe malauguratamente succedere agli Started ma non sono adatti a lavori da eseguire continuamente in background.
La differenza tra le due tipologie si riflette anche sul ciclo di vita. Il diagramma (fonte: documentazione ufficiale Android) seguente li mette a confronto:
Nell'immagine, sfilano le fasi attraversate da un Service Started (sulla sinistra) e da uno Bound (sulla destra). Entrambi i cicli di vita iniziano e terminano con i metodi di callback onCreate e onDestroy. Le differenze si concentrano nella fase in cui il Service viene attivato. Mentre l'avvio di un service Started viene notificato per mezzo di onStartCommand l'inizio e la fine della connessione con un service bound viene segnalato dai metodi onBind e onUnbind.
Service e Thread
Nel ciclo di vita dei Service, non c'è alcun metodo che viene eseguito in background. Non si trova traccia di qualcosa che ricordi il doInBackground di AsyncTask o il run dei Thread. Questo perchè il Service, di suo, non possiede alcun thread. Fondamentalmente, il suo funzionamento è “sincrono”.
Per permettere l'attività asincrona del service è necessario fornirlo almeno di un thread secondario. Quindi si ripresenta il problema paventato nel capitolo precedente: il thread deve essere fornito mediante estensione della classe Thread o avvio di Executors. Queste sono operazioni che possono essere svolte bene ed in maniera efficiente con un po' di esperienza ma un neo-programmatore potrebbe risultarne scoraggiato. Android, come al solito, offre un'alternativa “pratica” anche in questo caso. Si può usare un discendente di Service, IntentService, che nasce già con un thread incorporato.
Al di fuori delle operazioni in background – da collocare all'interno del metodo onHandleIntent – l'IntentService non richiede molto lavoro. L'unico altro metodo obbligatorio da creare è un costruttore senza parametri.
LogService: il primo servizio
Un esempio classico che permette un rapido approccio ai Service è quello di creare un servizio di log avviato mediante Activity.
Prendiamo un'interfaccia utente dotata di due pulsanti, “Avvia” e “Arresta”. Come il nome lascia presagire il primo avvia un Service, il secondo l'arresta.
Questo il layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Avvia" android:onClick="startService"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Arresta" android:onClick="stopService"/> </LinearLayout>
questo invece il codice dell'Activity:
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void startService(View v) { startService(new Intent(this,LogService.class)); } public void stopService(View v) { stopService(new Intent(this,LogService.class)); } }
Niente di nuovo in entrambi oltre all'uso dei metodi startService e stopService i quali, come spiegato, denunciano l'utilizzo di un Service avviato in modalità Started.
Molto importante: non dimenticare di dichiarare il Service nell'AndroidManifest.
Il Service utilizzato è di tipo IntentService. Non dovremo fornirlo di altro se non di un costruttore senza parametri in input e dell'implementazione di un metodo onHandleIntent:
public class LogService extends IntentService { public LogService() { super("LogService"); } @Override protected void onHandleIntent(Intent i) { int n=0; while(true) { Log.i("PROVA SERVICE", "Evento n."+n++); try { Thread.sleep(10000); } catch (InterruptedException e) { } } } @Override public void onDestroy() { Log.i("PROVA SERVICE", "Distruzione Service"); } }
L'esecuzione in background del thread secondario produrrà l'immissione di messaggi di log in Logcat sfruttando il metodo i della classe Log. Il risultato saranno messaggi simili, nel formato, a quelli visibili in figura:
Ultima nota, il Service è stato fornito di un'implementazione di onDestroy, chiamata in causa al momento dell'arresto del servizio, che aggiunge un messaggio finale di log.
Android e il networking
33. Android e la rete
In un corso Android, che si tratti di tutorial scritti, videocorsi o lezioni frontali, non si può negare che uno dei momenti più attesi dai discenti sia la lezione che riguarda l'accesso alla Rete. Non è la prima volta che in questa guida si parla di accesso alla Rete. È già successo a proposito della WebView, il controllo che permette di integrare web app in un'interfaccia utente. Quello che viene presentato in questo capitolo è un discorso più ampio, un insieme di strumenti che permettono all'applicazione di consultare, scaricare ed inviare dati in Internet.
Prerequisiti
A proposito di connessioni remote, sono due i concetti da tenere a mente, entrambi già incontrati nel corso della guida e fondamentali per evitare errori ed eccezioni di sicurezza:
- nel file AndroidManifest.xml va indicata la permission Internet apponendo il seguente nodo:
<uses-permission android:name=“android.permission.INTERNET”/>; - l'attività in Rete deve essere eseguita in modalità asincrona, su un thread separato. In questo caso, senza scomodare i Service, ci si potrà avvantaggiare della classe AsyncTask, utilizzando tutto il codice che verrà presentato nel metodo doInBackground.
Leggere contenuti in Rete
Per eseguire l'accesso alla Rete, esistono fondamentalmente due modi in Android: usare classi Java standard o librerie esterne incluse nel sistema.
Il tipo di interazione remota più comune è quello via HTTP, il protocollo comunemente usato per la distribuzione dei contenuti Internet. Questo protocollo può essere trattato con la classe HttpUrlConnection, tipica del mondo Java, o con la libreria HttpClient gestita dalla fondazione Apache.
Per programmatori Java entrambi i casi sono generalmente piuttosto noti. Ma quale usare? Uno dei due è migliore dell'altro? Entrambe, al giorno d'oggi, possono considerarsi funzionali. Sicuramente la libreria di Apache è molto estesa e offre tantissime funzionalità ma la classe HttpUrlConnection è più che sufficiente nella maggior parte dei casi come ad esempio nel normale scaricamento di un file. Quindi, di norma, si può usare la HttpUrlConnection tranne nei casi in cui servano funzionalità differenti disponibili solo in HttpClient.
Mettiamo a confronto gli utilizzi.
Immaginiamo che all'indirizzo http://www.mioserver.it/storage/file.txt sia disponibile un file di cui si deve fare il download.
Con HttpUrlConnection si farebbe:
URL url=new URL( "http://www.mioserver.it/storage/file.txt");
recuperando direttamente lo stream ai contenuti:
InputStream is= url.openStream();
oppure passando per la connessione con:
HttpURLConnection conn= (HttpURLConnection) url.openConnection(); InputStream is= conn.getInputStream();
Con HttpClient, in alternativa, si tratta di preparare una request HTTP ed eseguirla ottenendo in cambio una response:
HttpClient request=new DefaultHttpClient(); HttpGet get=new HttpGet("http://www.mioserver.it/storage/file.txt"); HttpResponse response=request.execute(get); InputStream is=response.getEntity().getContent()
In tutti i casi, si è ottenuto un riferimento ad un InputStream. Per suo tramite, si potrà fare accesso ai contenuti del file. Grazie all'astrazione offerta dagli Stream i contenuti potranno essere recuperati come se il file fosse locale. La classe che svolgerà il lavoro dipende dalla tipologia del formato dei dati: binari o testuali.
Se fosse un file testuale da scaricare in uno StringBuffer potremmo mettere in pratica questo codice:
BufferedReader r=new BufferedReader(new InputStreamReader(is)); String s=null; StringBuffer sb=new StringBuffer(); while((s=r.readLine())!=null) { sb.append(s); }
Trasmettere dati in Rete
Se si desidera inviare dati ad un server mediante HTTP, è probabile che lo si voglia fare con il metodo POST. Parafrasando il codice visto con HttpClient, un'operazione del genere si può portare a termine così:
HttpClient client = new DefaultHttpClient(); HttpPost post = new HttpPost(url); List<BasicNameValuePair> parametri = new ArrayList<BasicNameValuePair>(1); parametri.add(new BasicNameValuePair("cognome", "Rossi")); parametri.add(new BasicNameValuePair("nome", "Sergio")); parametri.add(new BasicNameValuePair("eta", "21")); post.setEntity(new UrlEncodedFormEntity(parametri)); HttpResponse resp = client.execute(post);
A differenza delle operazioni di lettura, si è usata la classe HttpPost in sostituzione di HttpGet. I dati da trasmettere vengono codificati in una lista di BasicNameValuePair. Ognuno di questi oggetti è costituito da una coppia chiave/valore. I parametri nel complesso alla fine diventano l'Entity della richiesta HTTP con l'invocazione di setEntity.
Appena sistemati i parametri, il funzionamento di HttpClient procede come abbiamo già visto. La richiesta viene eseguita e della risposta ottenuta si può leggere il contenuto nell'Entity come Stream.
Un caso particolare: il DownloadManager
La lettura da remoto, come visto sinora, è ottimale per scambi di informazioni finalizzati a stabilire una comunicazione tra app e server. Generalmente, i dati ricevuti non hanno una mole eccessiva e sono codificati in un formato comune come JSON, XML, testo o CSV per agevolarne il parsing e l'utilizzo nelle app.
Per download di dimensioni piuttosto grandi, sarebbe opportuno predisporre un funzionamento più “robusto”. Non è necessario reinventare la ruota in quanto esiste un servizio di sistema che già svolge bene questo lavoro: il DownloadManager.
Come tutti i servizi di sistema, se ne deve recuperare un riferimento:
DownloadManager manager= (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
A questo punto si crea una richiesta contenente l'indirizzo del file da scaricare e la si accoda nel manager:
Request request=new Request(Uri.parse("http://.... ")); manager.enqueue(request);
In funzione delle richieste pendenti, il servizio farà il download non appena possibile.
Questo servizio non solo offre “già pronta” una funzionalità molto utile ma è altamente configurabile. Infatti si può scegliere, tra l'altro, se mostrare una barra di avanzamento nell'area delle notifiche, la posizione di salvataggio del file all'interno dello Storage e, cosa molto importante, se svolgere il download con ogni tipo di connessione o solo in presenza di Wi-Fi.
Le potenzialità e la flessibilità del DownloadManager lo rendono un servizio utilissimo tanto da poter considerare assolutamente inutile, se non addirittura dannoso, creare in proprio una soluzione alternativa per il medesimo scopo.
34. Consumare servizi REST da Android
Oltre al puro scaricamento di file, l'interazione con la Rete offre grandissime potenzialità. Una fra tutte: lo sfruttamento di servizi Web. Ormai non è più una novità parlare di Web Services. Si tratta essenzialmente di funzionalità rese disponibili in Rete da servizi remoti dai quali client distribuiti nel mondo possono recuperare informazioni, richiedere elaborazioni e molto altro ancora. Uno stile di servizio Web che si è diffuso molto già da diversi anni è REST (REpresentational State Transfer). Il motivo di ciò è la semplicità con cui può essere implementato e la diffusione dei concetti che ne sono alla base.
Introduzione ai servizi REST
Il funzionamento di questi servizi si basa sulla possibilità di sfruttare risorse disponibili in Rete mediante i classici metodi del protocollo HTTP. Di questi, i più comuni sono GET e POST, vecchia conoscenza degli sviluppatori web: il primo concepito per leggere dati da remoto senza apportare modifiche, il secondo per inviare dati verso il servizio con lo scopo di richiederne l'inserimento nella base dati. Oltre a questi, HTTP possiede altri metodi ed in particolare due verranno coinvolti nel discorso: PUT per richiedere la modifica dei dati e DELETE per averne la cancellazione.
Si noti che questi quattro metodi – POST, GET, PUT, DELETE – richiamano gli stessi quattro concetti espressi dai metodi CRUD dei database: creazione, lettura, modifica e cancellazione.
In questo senso i servizi REST possono essere visti come un modo distribuito per gestire un database.
Inoltre per ognuno dei suddetti metodi, il servizio Web specificherà con apposita documentazione quali URL devono essere contattati.
I ContentProvider, visti nei capitoli precedenti, da un punto di vista concettuale possono essere considerati un'adozione della mentalità REST nella condivisione di informazioni nel sistema Android.
Altro concetto appartenente al protocollo HTTP, sono i codici di stato contenuti nella risposta. Tra i più comuni ricordiamo:
| 200 | OK |
| 400 | Bad Request |
| 403 | Forbidden |
| 404 | Not found |
| 500 | Internal Server error |
Avranno un ruolo importante nei servizi REST ma appartengono anche alla comune esperienza della navigazione Internet: quante volte l'invocazione di un indirizzo web non corretto causava l'apparizione nel browser del classico messaggio “Error 404 – Page not found”?
In generale, al di là dei singoli codici, è importante ricordare che se la prima cifra del codice è 2 significa che comunque l'esecuzione è andata in porto, se è 4 indica errore da parte del client nella richiesta, se è 5 indica l'occorrenza di un errore dal lato server.
Le risorse offerte da servizi REST possono essere rappresentati in una moltitudine di formati, non ce n'è uno ufficiale. L'ideale sarebbe usare non solo formati molto diffusi – XML o JSON – ma possibilmente prevedere la distribuzione della stessa risorsa in vari formati permettendo così la fruizione nella modalità preferita.
Nel prosieguo dell'articolo si approfondirà uno dei formati più comuni per servizi REST, ne vedremo le modalità di utilizzo in Android ed il suo impiego pratico in un servizio web. Stiamo parlando di JSON.
JSON in Android
JSON (Javascript Object Notation) è un formato stringa per la rappresentazione di dati organizzati in oggetti e array. Negli ultimi anni, soprattutto grazie alla sua semplicità e al suo largo impiego in Ajax, ha acquisito una notevolissima popolarità soprattutto a discapito di XML. In Android, se ne può fare uso e, come al solito, il sistema contiene tutto il necessario.
Molto spesso le stringhe in JSON contengono array di oggetti. Per farne il parsing si può apprezzare la comodità delle classi JSONArray e JSONObject disponibili nel package org.json.
Prendiamo la semplice stringa JSON che contiene tre oggetti. Ogni oggetto a sua volta contiene due stringhe, nome e cognome di una persona.
[
{
"nome":"Lucio",
"cognome":"Bianchi"
},
{
"nome":"Paolo",
"cognome":"Neri"
},
{
"nome":"Sergio",
"cognome":"Rossi"
}
]
Se nel nostro codice Android avessimo bisogno di acquisirne i dati, supponendo che l'array fosse nella stringa json, potremmo scrivere le seguenti righe:
JSONArray array=new JSONArray(json); String persone=new String[array.length()]; for(int i=0;i<array.length();i++) { String nome=array.getJSONObject(i).getString("nome"); String cognome=array.getJSONObject(i).getString("cognome"); persone[i]=nome+" "+cognome; }
Come si vede il costruttore di JSONArray implementa direttamente il parsing della stringa passata e l'oggetto ottenuto è consultabile quasi come un array vero e proprio mediante i metodi length(), per leggere la lunghezza, e getJSONObject che restituisce il JSONObject in una determinata posizione.
Ogni JSONObject ottenuto all'interno del ciclo può essere letto “in stile mappa” recuperando i valori in base alla chiave assegnata in JSON. Con semplici modifiche, l'esempio precedente può essere adattato a molti tipi di parsing.
Interagire con un servizio REST
A questo punto dovremmo avere a disposizione tutti i prerequisiti necessari per l'interazione con un servizio REST.
Riepiloghiamoli:
- concetti basilari su cos'è REST: una visione chiara di come viene impiegato HTTP con i suoi metodi e codici di stato;
- saper gestire anche in maniera basilare il parsing e la formattazione JSON o di altro formato che sia necessario usare;
- saper fare in modo che la nostra app riesca ad accedere alla Rete e dialogare in HTTP. Abbiamo visto almeno due modi per farlo: classe Java tradizionale HttpURLConnection, meno funzionalità ma molto comuni e libreria Apache HttpClient, completissima e già inclusa in Android;
- ricordare sempre le due regole base per l'accesso alla Rete da Android: dichiarare le permission adeguate (almeno android.permission.INTERNET) ed eseguire gli accessi da un thread secondario. Per il secondo punto, nella maggior parte dei casi, è sufficiente saper usare AsyncTask.
A titolo di esempio, immaginiamo che ci sia un servizio REST che all'URL http://www.mioservizio.it/persone, via GET, restituisca in JSON un array di oggetti. Ogni oggetto rappresenta una persona specificandone nome,cognome ed età.
Vediamo le porzioni di Java che servirebbero a sfruttare entrambe le funzionalità. Ricordiamo per l'ennesima volta che il seguente codice va usato, previo inserimento della permission INTERNET nel manifest, all'interno di un metodo doInBackground di AsyncTask.
String url="http://www.mioservizio.it/persone"; String[] persone=null; // conterrà i risultati HttpClient request=new DefaultHttpClient(); HttpGet get=new HttpGet(url); HttpResponse response=request.execute(get); responseCode=response.getStatusLine().getStatusCode(); if (responseCode==200) { InputStream istream=response.getEntity().getContent(); BufferedReader r=new BufferedReader(new InputStreamReader(istream)); String s=null; StringBuffer sb=new StringBuffer(); while((s=r.readLine())!=null) { sb.append(s); } JSONArray array=new JSONArray(sb.toString()); persone=new String[array.length()]; for(int i=0;i<array.length();i++) { String nome=array.getJSONObject(i).getString("nome"); String cognome=array.getJSONObject(i).getString("cognome"); String eta=array.getJSONObject(i).getString("eta"); persone[i]=nome+" "+cognome+" di anni "+eta; } return persone; }
Notare che, una volta effettuata la connessione ed ottenuta la risposta, è necessario leggere il codice di risposta HTTP. In questo caso, le operazioni di parsing vengono eseguite solo se il codice HTTP restituito è 200. Come spiegato in precedenza si possono attuare comportamenti differenti per codici di stato diversi.
Le operazioni di interpretazione dei risultati vengono effettuate aggregando gli elementi appresi in questa lezione e nelle precedenti:
- con
response.getEntity().getContent()viene prelevato il contenuto dell'entity HTTP sotto forma di InputStream; - l'InputStream verrà letto iterativamente al fine di ritrovare tutto il suo contenuto all'interno di uno StringBuffer;
- lo StringBuffer, quando completo, conterrà un array JSON formato da un elenco di persone e pertanto se ne potrà fare il parsing con JSONArray;
- dopo il parsing, i dati prelevati dal servizio, saranno disponibili in una struttura dati Java – in questo caso un semplice array di stringhe – e quindi pronti per essere sfruttati nella propria app.
Gli altri metodi HTTP
L'esempio ha mostrato solo una lettura di dati via GET. Gli altri casi non vengono esemplificati nei dettagli ma si consideri che la libreria HttpClient oltre a HttpGet contiene classi corrispondenti agli altri metodi HTTP: HttpPost, HttpPut e HttpDelete.
Inoltre, mentre eventuali parametri nel GET vengono inviati concatenati all'URL in una querystring, negli altri metodi lo si può fare con liste di BasicNameValuePair come mostrato nel capitolo relativo all'accesso in Rete.
Lo studio può comunque essere proseguito sia sulla documentazione Android che della fondazione Apache in entrambi i casi molto completa.
Hardware e sensori
35. Sensori e SensorManager in Android
Molto spesso giocando, usando applicazioni o semplicemente tenendo in mano un device ci è capitato di notare che il dispositivo “si accorge” di una serie di fattori ed eventi fisici: se lo giriamo, se lo scuotiamo e via dicendo.
Altre volte si può essere rimasti stupiti notando che alcune app forniscono informazioni sull'ambiente in cui ci troviamo. Percepiscono magari temperatura, umidità, luminosità.
“Ma come fa a saperlo?”: la domanda nasce spontanea.
Questo capitolo vuole mostrare che queste funzionalità non celano magie ma un po' di elettronica amalgamata con software ben fatto.
I dispositivi Android grazie ai sensori di cui sono forniti riescono a percepire movimenti, condizioni ambientali e lo faranno sempre più e sempre con maggiore precisione grazie all'ampliamento costante di queste tecnologie. A noi spetta la parte, se vogliamo, più divertente di tutto questo: leggere facilmente queste informazioni con le API del framework e usarle per arricchire le nostre app.
Classificazione dei sensori
Iniziamo con un po' di classificazioni. Innanzitutto, i sensori possono essere suddivisi in tre grandi gruppi:
- sensori di movimento: percepiscono le forze fisiche che agiscono sul dispositivo. Ad esempio, l'accelerometro, il giroscopio, sensore di gravità;
- sensori ambientali: rilevano particolari dell'ambiente in cui ci si trova: temperatura, pressione, umidità;
- sensori di posizione: raccolgono dati sulla posizione del dispositivo, ad esempio il sensore di orientamento.
Inoltre, i sensori, dipendentemente dal modo in cui sono implementati, possono essere hardware o software. I primi corrispondono a dei veri e propri elementi elettronici inseriti nel dispositivo. I secondi sono delle elaborazioni basate sui dati raccolti dai sensori hardware. Quelli software sono chiamati anche virtuali in quanto possono essere consultati con lo stesso interfacciamento di quelli hardware dissimulando quindi la loro natura software.
Alcuni sensori devono essere necessariamente hardware, altri esistono solo software mentre alcuni possono essere hardware o software a seconda dell'implementazione che è stata scelta per il particolare dispositivo.
Il SensorManager
Come in molte altre situazioni, il sottosistema Android che vogliamo sfruttare ci viene dischiuso da un system service, accessibile mediante una classe “manager”.
In questo caso, si tratta del SensorManager.
Per ottenerne un riferimento, procediamo ad un passo ormai di rito:
private SensorManager mSensorManager; . . . . . . mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
La prima cosa che può essere utile fare con il SensorManager è chiedergli un inventario dei sensori disponibili nel nostro dispositivo.
Usando una serie di costanti intere (tutte ben spiegate nella documentazione ufficiale) si può chiedere una lista dei sensori:
<Sensor> sensors=mSensorManager.getSensorList(Sensor.TYPE_ALL);
oppure verificare se un sensore è disponibile:
Sensor ss=mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
Un po' tutti i dispositivi avranno a disposizione almeno tre o quattro sensori essenziali per la vita di uno smartphone tra cui accelerometro, orientamento e rotazione.
Leggere dati da un sensore
La prassi comune per ricevere dati periodici da un sensore è registrare un listener nella nostra applicazione. Ciò, da un punto di vista sintattico, obbligherà all'implementazione di un metodo di callback all'interno del quale si potrà fare un qualche uso delle misurazioni rilevate.
Un tipico schema di Activity che legge dati da un sensore potrebbe essere questo:
public class MainActivity extends Activity implements SensorEventListener { private SensorManager mSensorManager; private Sensor sensor; protected void onResume() { super.onResume(); mSensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL); } protected void onPause() { super.onPause(); mSensorManager.unregisterListener(this); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE); sensor = mSensorManager.getDefaultSensor( /* * Costante relativa al sensore da monitorare * */ ); } @Override public void onSensorChanged(SensorEvent event) { /* * Codice di gestione dei nuovi eventi del sensore * */ } @Override public void onAccuracyChanged(Sensor s, int i) { } }
Gli aspetti da notare maggiormente sono:
- nel metodo
onCreateè stato prelevato un riferimento al SensorManager. Opzionalmente questo punto sarà buono per recuperare un riferimento anche al sensore specifico con cui si vuole interagire; - nei metodi
onPauseeonResumeche come sappiamo regolano l'inizio e la fine dell'interazione tra Activity e utente avviene, rispettivamente, la registrazione e la cancellazione del listener; - l'activity implementa l'interfaccia SensorEventListener che forza all'override di due metodi
onAccuracyChangedeonSensorChanged.
Il metodo onSensorChanged costituisce il cuore dell'interazione con il sensore. È qui che arrivano le chiamate del listener ogni volta che sono disponibili nuove misurazioni. L'evento notificato verrà formalizzato con un oggetto di classe SensorEvent.
SensorEvent permette di leggere i valori recuparati come un array numerico. Il tutto visto in questo modo potrebbe sembrare semplice. La difficoltà sta proprio nell'interpretare e sfruttare i valori dell'evento. Essendo i sensori dei misuratori di grandezza fisiche, i dati letti con essi dovrebbero essere sottoposti ad opportune valutazioni nel rispetto, eventualmente, di leggi scientifiche. In un capitolo successivo, si avrà modo di sperimentare l'accelerometro ed in quel caso dovremo interpretare delle accelerazioni.
36. Geolocalizzazione con Android
L'utente passa giornate spostandosi e raccogliendo informazioni – più o meno volutamente – nel proprio device Android. Consultazioni internet, appunti, eventi calendario, foto, chiamate, messaggi. Il dispositivo diventa una specie di “diario errante” dell'esperienza di vita quotidiana. La possibilità di associare informazioni geografiche a questi ricordi apre scenari nuovi e ciò basta a giustificare la rapidissima diffusione che ha avuto nell'informatica mobile la geolocalizzazione.
Intendiamo con questo termine la capacità di un dispositivo di rilevare la propria posizione geografica nel mondo reale. Non stiamo parlando ormai di una dote rara, quasi ogni smartphone o tablet oggi contiene dei sistemi di localizzazione.
I più comuni sono:
- network-based: rileva le reti mobili Wi-Fi e GSM disponibili nella zona ed in base a questi calcola la propria posizione. Non molto accurato ma immancabile nei dispositivi;
- GPS (Global Positioning System): acronimo famosissimo, si basa sull'intercettazione di messaggi inviati da satelliti che ruotano attorno alla Terra. Tali comunicazioni contengono l'informazione oraria ed altri dati relativi all'orbita percorsa. Il dispositivo intercettando i segnali di almeno quattro di questi satelliti con l'applicazione di formule matematiche riesce a calcolare la propria posizione. Accurato e diffusissimo tranne che in alcuni dispositivi di fascia bassa. Praticamente il sistema di localizzazione per antonomasia.
Esempio pratico: GPS nell'Activity
Entriamo subito nel vivo creando un'Activity che richiede informazioni GPS e le mostra nel suo layout. Oltre a latitudine e longitudine l'Activity mediante un oggetto denominato GeoCoder recupererà l'indirizzo cui corrisponde la posizione.
Il layout dell'activity è una griglia molto semplice. TableLayout con una serie di campi di testo da completare:
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"> <TableRow android:padding="5dp"> <TextView android:text="Abilitato" android:padding="5dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:id="@+id/enabled" android:padding="5dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </TableRow> <TableRow android:padding="5dp"> <TextView android:text="Data ora" android:padding="5dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:id="@+id/timestamp" android:padding="5dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </TableRow> <TableRow android:padding="5dp"> <TextView android:text="Latitudine" android:padding="5dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:id="@+id/latitude" android:padding="5dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </TableRow> <TableRow android:padding="5dp"> <TextView android:text="Longitudine" android:padding="5dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:id="@+id/longitude" android:padding="5dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </TableRow> <TableRow android:padding="5dp"> <TextView android:text="Località" android:padding="5dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:id="@+id/where" android:padding="5dp" android:lines="2" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </TableRow> </TableLayout>
Da ricordare che per l'accesso ai dati GPS è necessaria un'apposita permission. Nel manifest andremo ad inserire questa riga:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
Questa permission va bene sia per usare il GPS sia per la localizzazione network-based. Qualora si volesse usare solo quest'ultima è sufficiente la permission ACCESS_COARSE_LOCATION.
All'interno dell'Activity dovremo per prima cosa registrare un Listener presso il LocationManager e lo faremo nel metodo onResume. Tale istanza sarà annullata in onPause.
public class MainActivity extends Activity { private String providerId = LocationManager.GPS_PROVIDER; private Geocoder geo = null; private LocationManager locationManager=null; private static final int MIN_DIST=20; private static final int MIN_PERIOD=30000; private LocationListener locationListener = new LocationListener() { . . . . . . }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override protected void onResume() { super.onResume(); geo=new Geocoder(this, Locale.getDefault()); locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); Location location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (location!=null) updateGUI(location); if (locationManager!=null && locationManager.isProviderEnabled(providerId)) updateText(R.id.enabled, "TRUE"); else updateText(R.id.enabled, "FALSE"); locationManager.requestLocationUpdates(providerId, MIN_PERIOD,MIN_DIST, locationListener); } @Override protected void onPause() { super.onPause(); if (locationManager!=null && locationManager.isProviderEnabled(providerId)) locationManager.removeUpdates(locationListener); } . . . . . . }
Notare che, nell'onResume, il metodo requestLocationUpdates effettua la vera registrazione del listener. I parametri che utilizza sono:
- l'id del provider: la costante stringa che individua il tipo di provider da usare;
- il minimo intervallo di tempo, in millisecondi, che deve trascorrere tra aggiornamenti della posizione;
- la minima distanza in metri che deve intercorrere tra due misurazioni;
- l'oggetto che svolge il ruolo di listener. Lo vedremo subito.
L'oggetto listener registrato viene implementato come classe interna all'Activity:
private LocationListener locationListener = new LocationListener() { @Override public void onStatusChanged(String provider, int status, Bundle extras) { } @Override public void onProviderEnabled(String provider) { // attivo GPS su dispositivo updateText(R.id.enabled, "TRUE"); } @Override public void onProviderDisabled(String provider) { // disattivo GPS su dispositivo updateText(R.id.enabled, "FALSE"); } @Override public void onLocationChanged(Location location) { updateGUI(location); } };
I primi tre metodi – onStatusChanged, onProviderEnabled, onProviderDisabled – notificano, rispettivamente, se il provider è disponibile o meno, se è abilitato, se è stato disabilitato.
L'ultimo metodo onLocationChanged è il cuore del listener e viene invocato ogni volta che nuove informazioni di posizione sono state recapitate.
L'oggetto Location contiene tutto ciò che è stato appreso dall'ultima misurazione del posizionamento e viene inviata al metodo updateGUI per riflettere gli aggiornamenti sulla interfaccia utente:
private void updateGUI(Location location) { Date timestamp = new Date(location.getTime()); updateText(R.id.timestamp, timestamp.toString()); double latitude = location.getLatitude(); updateText(R.id.latitude, String.valueOf(latitude)); double longitude = location.getLongitude(); updateText(R.id.longitude, String.valueOf(longitude)); new AddressSolver().execute(location); } private void updateText(int id, String text) { TextView textView = (TextView) findViewById(id); textView.setText(text); }
All'interno di updateGUI, oltre al codice di modifica delle TextView, è presente l'invocazione al Geocoder per la conversione delle coordinate in un indirizzo vero e proprio. Il Geocoder viene consultato in maniera asincrona mediante AsyncTask. Nel metodo doInBackground, la Location sarà convertita in una stringa frutto della concatenazione delle informazioni reperite:
private class AddressSolver extends AsyncTask<Location, Void, String> { @Override protected String doInBackground(Location... params) { Location pos=params[0]; double latitude = pos.getLatitude(); double longitude = pos.getLongitude(); List<Address> addresses = null; try { addresses = geo.getFromLocation(latitude, longitude, 1); } catch (IOException e) { } if (addresses!=null) { if (addresses.isEmpty()) { return null; } else { if (addresses.size() > 0) { StringBuffer address=new StringBuffer(); Address tmp=addresses.get(0); for (int y=0;y<tmp.getMaxAddressLineIndex();y++) address.append(tmp.getAddressLine(y)+"\n"); return address.toString(); } } } return null; } @Override protected void onPostExecute(String result) { if (result!=null) updateText(R.id.where, result); else updateText(R.id.where, "N.A."); } }
37. Accelerometro: come utilizzarlo
Uno dei sensori più ampiamente diffusi nei dispositivi Android è di sicuro l'accelerometro. Quindi, con riguardo alla sua ampia diffusione ed importanza, in questo capitolo sarà oggetto di un esempio che per quanto semplice potrà apparire dotato di una certa valenza pratica.
Esempio: usare lo "shake"
In questo esempio vedremo come intercettare un evento di shake e collegarvi una reazione. Per shake intendiamo genericamente l'atto di scuotere il dispositivo indipendentemente dalla direzione. Le accelerazioni fisiche impresse sull'hardware vengono captate dall'accelerometro. Il nostro compito sarà quello di capire se la loro intensità complessiva è tale da potervi riconoscere l'avvenimento di uno shake. Solo in questo caso attiveremo la reazione.
Nell'esempio, il layout è costituito da un solo form. Se durante la compilazione viene effettuato uno shake, il dispositivo chiederà a mezzo finestra di dialogo se si vuole procedere alla cancellazione dei valori inseriti nei campi.
Il codice del layout, come presumibile, non offre grandi novità. Questo il contenuto del file res/layout/activity_main.xml:
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TableRow > <TextView android:layout_weight="1" android:layout_height="wrap_content" android:text="Username:" android:id="@+id/label1"/> <EditText android:layout_weight="5" android:inputType="text" android:layout_height="wrap_content" android:id="@+id/text1"/> </TableRow> <TableRow > <TextView android:layout_weight="1" android:layout_height="wrap_content" android:text="Password:" android:id="@+id/label2"/> <EditText android:layout_weight="5" android:inputType="textPassword" android:layout_height="wrap_content" android:id="@+id/text2"/> </TableRow> <TableRow > <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Salva"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Annulla" android:id="@+id/button2"/> </TableRow> </TableLayout>
"Ascoltare" il sensore
L'Activity dovrà svolgere il ruolo di listener per eventi del sensore:
public class MainActivity extends Activity implements SensorEventListener { private SensorManager mSensorManager; private Sensor mAccelerometer; private float lastAcc = 0.0f; private float acceleration = 0.0f; private float totAcc = 0.0f; private boolean onEvent = false; . . . . . . } // fine MainActivity
I membri privati contengono, in primis, riferimenti al SensorManager e ad un Sensor che in questo caso è l'accelerometro. Le altre variabili di tipo float serviranno per custodire valori temporanei nel calcolo delle accelerazioni.
Il metodo onCreate svolge le inizializzazioni assegnando il valore opportuno ai membri privati. Notiamo che le variabili float delle accelerazioni verranno impostate a valori corrispondenti alla gravità terrestre.
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
lastAcc=SensorManager.GRAVITY_EARTH;
acceleration=SensorManager.GRAVITY_EARTH;
}
Nel capitolo riguardante il ciclo di vita delle Activity si è spiegato a cosa servono i metodi onResume e onPause: segnano, rispettivamente, l'inizio e la fine dell'interazione tra interfaccia e utente. Visto che la nostra Activity dovrà registrarsi per ricevere prontamente segnalazioni sugli eventi dell'accelerometro, per evitare di impiegare inutilmente risorse rinnoverà tale registrazione all'interno dell'onResume e la disdirà ad ogni onPause. Al di fuori dell'intervallo di tempo segnato da questi metodi, sarebbe assolutamente inutile oltre che vanamente dispendioso richiedere segnalazioni in merito.
protected void onResume() { super.onResume(); mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL); } protected void onPause() { super.onPause(); mSensorManager.unregisterListener(this); }
L'implementazione dell'interfaccia SensorEventListener richiede l'override di due metodi:
- onAccuracyChanged: chiamato quando l'accuratezza del sensore viene modificata. Nell'esempio non riceverà un'implementazione;
- onSensorChanged: come già appreso alcuni capitoli fa, riceve notifiche sugli eventi del sensore incarnati da un oggetto SensorEvent. Qui verrà inserito tutto il codice di gestione dell'evento e proprio dal SensorEvent leggeremo le accelerazioni ricevute.
@Override
public void onAccuracyChanged(Sensor arg0, int arg1)
{ }
@Override
public void onSensorChanged(SensorEvent event)
{
if (!onEvent)
{
float x = event.values[0];
float y = event.values[1];
float z = event.values[2];
lastAcc = acceleration;
acceleration = x*x+y*y+z*z;
float diff = acceleration - lastAcc;
totAcc = diff*acceleration;
if (totAcc>15000)
{
onEvent=true;
AlertDialog.Builder builder=new Builder(this);
builder.setMessage("Pulire il form?");
builder.setPositiveButton("Sì", new OnClickListener()
{
@Override
public void onClick(DialogInterface arg0, int arg1)
{
clean();
onEvent = false;
}
});
builder.setNegativeButton("No", new OnClickListener()
{
@Override
public void onClick(DialogInterface arg0, int arg1)
{
onEvent=false;
}
});
builder.show();
}
}
}
private void clean()
{
TextView txt1=(TextView) findViewById(R.id.text1);
TextView txt2=(TextView) findViewById(R.id.text2);
txt1.setText("");
txt2.setText("");
}
L'ulteriore metodo visibile nel codice, clean, verrà invocato quando si riterrà opportuno cancellare il form.
Tutto il riconoscimento dello shake si trova in queste righe:
float x = event.values[0]; float y = event.values[1]; float z = event.values[2]; lastAcc = acceleration; acceleration = x*x+y*y+z*z; float diff = acceleration - lastAcc; totAcc = diff*acceleration; if (totAcc>15000) {
Da event verranno lette le tre accelerazioni (una per ogni dimensione dello spazio). La risultante verrà calcolata sommandone i quadrati. La variabile lastAcc serve a salvare l'ultimo valore calcolato – quello che è avvenuto con l'evento precedente – mentre acceleration conterrà il nuovo valore. Infine in totAcc si cercherà di valutare l'entità della variazione. Se tale valore supera la soglia di 15000 assumeremo che lo shake sia avvenuto.
Il codice contenuto nel blocco if quindi sarà la vera reazione allo shake. L'Activity dovrà sapere sempre che il trattamento dell'evento è in corso. Glielo dirà il valore di onEvent che verrà impostato a true non appena la variazione delle accelerazioni raggiungerà 15000.
Gli eventi dell'accelerometro verranno invocati molto spesso quindi dovremo stare attenti ad eseguire meno codice possibile e solo nei casi in cui sia strettamente necessario. Se si nota, non appena viene invocato onSensorChanged si verifica se è in corso la gestione di uno shake con:
if (!onEvent)
In caso positivo nulla sarà fatto per il momento. Ovviamente è fondamentale resettare onEvent impostandolo di nuovo a false nel momento in cui la finestra di dialogo viene chiusa.
38. Touchscreen ed eventi Touch
Pensare un dispositivo Android senza touchscreen ormai è impossibile. La facoltà di un display di riconoscere il movimento di uno o più dita dell'utente su di sé è di fondamentale importanza. Ciò ha rappresentato una svolta generazionale nei device facilitando il pensionamento delle tastiere hardware e permettendo l'avvento di quelle software che noi tutti conosciamo.
In questo capitolo si inizierà a conoscere l'utilizzo del touch in un'app Android e le informazioni fondamentali che tale evento fornisce. Infine, con un esempio, se ne mostrerà un possibile uso.
Abbiamo imparato a definire un listener ogni volta che la nostra app è in attesa di qualcosa. La gestione del touch non fa differenza. Un metodo onTouchEvent riceverà le informazioni relative al singolo evento in un oggetto MotionEvent e predisporrà una reazione.
A dimostrazione della fondamentale importanza che la gestione del touchscreen riveste non sarà necessario implementare alcuna interfaccia – come ad esempio si faceva con SensorEventListener per i sensori – ma il metodo onTouchEvent è “di serie” su qualsiasi View e sulle Activity.
Un oggetto MotionEvent contiene molte informazioni, ma quelle più “eloquenti” per il programmatore sono:
- le coordinate del display in cui è avvenuto l'evento touch;
- la tipologia dell'evento (i più comuni: ACTION_DOWN, generato quando il dito si è posato sullo schermo, ACTION_MOVE, se il dito si è mosso senza staccarsi dallo schermo, ed ACTION_UP nel caso in cui il dito ha lasciato la superficie del display).
L'esempio
In questa dimostrazione riconosceremo mediante touch un click sullo schermo del dispositivo sfruttando il metodo onTouchEvent dell'Activity. Per fare ciò cercheremo una certa sequenzialità tra un evento ACTION_DOWN ed uno ACTION_UP. Con l'occasione, rilevando i tempi delle due fasi, si calcolerà approssimativamente quanti secondi è durato il click.
Il risultato finale è visibile nella figura in cui si vede un Toast che fornisce le informazioni raccolte con il piccolo esperimento.
Layout semplicissimo (file: res/layout/activity_main.xml):
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:textStyle="bold" android:textSize="20dp" android:text="Tieni premuto il dito sul\n display alcuni secondi"/> </RelativeLayout>
Il codice dell'activity concentra le sue peculiarità in onTouchEvent:
public class MainActivity extends Activity { private long inizio=0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override public boolean onTouchEvent(MotionEvent event) { float eventX = event.getX(); float eventY = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: inizio=System.currentTimeMillis(); break; case MotionEvent.ACTION_UP: String posizione="("+Math.round(eventX)+","+Math.round(eventY)+")"; long diffInSec=(System.currentTimeMillis()-inizio)/1000; Toast.makeText(this, "Click durato "+diffInSec+" secondi in posizione "+posizione, Toast.LENGTH_SHORT).show(); return true; } return false; } }
I metodi di MotionEvent utilizzati sono:
- getX() e getY(): forniscono, rispettivamente, ascissa e ordinata della posizione del display in cui si è verificato l'evento;
- getAction(): restituisce una costante intera che permette di riconoscere il tipo di evento.
In caso di ACTION_DOWN viene salvato in una variabile intera dell'Activity l'informazione temporale in millisecondi. In ACTION_UP si farà la differenza tra l'attuale informazione oraria e quella salvata al momento di toccare lo schermo.
39. Multitouch
Quanto visto nel capitolo precedente riguardava esclusivamente eventi scatenati da un singolo puntatore; o meglio, considerato che ci stiamo riferendo alla mano dell'utente, da un solo dito. Sono i cosiddetti eventi single touch. Molto frequenti sono comunque gli eventi multitouch, cioè quelli provocati dall'uso congiunto di più dita. Si pensi a quando, per rimpicciolire un'immagine, puntiamo due dita sul display e senza sollevarle le avviciniamo tra loro.
Concettualmente il multitouch nelle app Android presenta gli stessi fondamenti del suo “cugino” single. Ugualmente si farà uso di:
- TouchEventListener per designare il componente incaricato di ricevere gli eventi relativi al touch;
- onTouchEvent sarà il metodo nel quale verranno recapitate le informazioni relative all'evento rilevato;
- MotionEvent sarà la tipologia di oggetto che veicolerà le informazioni acquisite come posizioni dei puntatori e tipologie di evento.
La differenza starà nel fatto che dovranno essere rese contemporaneamente disponibili tutte le informazioni sulla posizione dei singoli puntatori. Ciò permetterà in questa sede di approfondire la struttura di MotionEvent che, probabilmente, quando si tratta il semplice single touch appare molto più elementare di quello che realmente sia.
Si consideri innanzitutto che tutte le dita poggiate contemporaneamente sul display costituiscono con i loro movimenti una “gestualità” complessiva. Il MotionEvent assegnerà un ID ad ogni puntatore e se ne servirà per distinguere le informazioni raccolte per ognuno di essi.
All'interno del metodo onTouchEvent risulteranno molto utili i seguenti metodi di MotionEvent:
- getPointerCount: restituisce il numero di puntatori attualmente sul display;
- getActionIndex: fornisce l'indice del puntatore che viene trattato in questa singola invocazione di onTouchEvent;
- getPointerId: restituisce l'id del singolo puntatore indicato dall'indice passato in input, solitamente fornito da getActionIndex;
- getX e getY: servono a recuperare le coordinate del singolo puntatore indicato dall'indice, anche in questo caso, ottenuto con getActionIndex;
- getActionMasked: ritorna il codice identificativo del tipo di azione compiuta.
Per quanto riguarda gli eventi, nel capitolo precedente, avevamo attuato un'analisi del click su schermo suddividendolo nelle fasi di ACTION_DOWN (posare il dito sullo schermo) e ACTION_UP (sollevare il dito). Nel multitouch, la filiera delle fasi da osservare sarà leggermente più lunga:
- ACTION_DOWN: rappresenta l'inizio del movimento. Corrisponde al primo puntatore che tocca lo schermo. Le informazioni corrispondenti si troveranno all'indice 0 del MotionEvent;
- ACTION_POINTER_DOWN: per ogni altro puntatore, dopo il primo, che toccano lo schermo nella stessa “gestualità”. Il metodo getActionIndex() fornirà la posizione nel MotionEvent che contiene i dati di questo singolo evento;
- ACTION_MOVE: non vengono aggiunti puntatori ma avviene un cambiamento, come uno spostamento;
- ACTION_POINTER_UP: segnala che un puntatore non primario, ossia uno che ha toccato il display dopo il primo, viene sollevato;
- ACTION_UP: invocato quando anche il primo puntatore, quello che ha dato inizio al movimento, è stato sollevato. La gestualità si è completata.
40. Rilevare le gesture
Abbiamo imparato finora a gestire il touch. Il display percepisce il modo in cui puntatori – in genere le dita dell'utente – ne toccano la superficie prendendo nota delle coordinate, dei movimenti e di tutte le variazioni relative.
L'insieme delle variazioni impresse dai puntatori viene intesa nel suo complesso come una gestualità, o gesture come più comunemente si usa dire.
In effetti, i touch e le relative modifiche potrebbero essere analizzate, come visto, con oggetti MotionEvent all'interno del metodo di callback OnTouchEvent. Ma la frequenza con cui appaiono nel mondo mobile gestures comuni ha indotto sin da subito il team di Android a prevedere appositi metodi per la loro gestione.
Esiste nelle API Android una classe fondamentale per la gestione delle gestures, GestureDetector. Il suo costruttore prende in input due parametri, il Context ed un listener che implementa OnGestureListener:
public class MainActivity extends Activity implements OnGestureListener { private GestureDetector detector; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); detector=new GestureDetector(this, this); } @Override public boolean onTouchEvent(MotionEvent event) { detector.onTouchEvent(event); return true; } . . . . . . }
Lo stralcio di codice appena illustrato mostra come può essere impostata un'Activity in cui vengano trattate le gesture. In dettaglio, si è:
- implementata l'interfaccia OnGestureListener nell'Activity;
- impostato un GestureDetector come membro privato;
- inizializzato il detector all'interno dell'onCreate passando due riferimenti all'Activity stessa: il primo in quanto Context, il secondo come listener per le gestures;
- fatto override del metodo onTouchEvent dell'Activity che “gira” il MotionEvent ricevuto al detector. Questo passaggio è fondamentale: stabilisce l'aggancio tra la ricezione dei touch e la loro interpretazione in quanto gestures.
L'implementazione di OnGestureListener richiede l'override di diversi metodi che rappresentano le principali gestures:
- onDown: rappresenta l'atto di posare il dito sul display. È praticamente l'inizio di qualunque gesture;
- onLongPress: viene invocato solo quando la pressione sul display rimane prolungata;
- onFling: è ciò che comunemente viene chiamato swipe, l'atto del dito con cui si mima lo sfogliare delle pagine verso destra o sinistra;
- onScroll: richiama il concetto comune di fare scorrere una lista di elementi verticalmente o orizzontalmente. Come gesture, viene associato in senso più ampio a qualsiasi sfregamento del dito sul display senza distacco;
- onShowPress: dovuto alla pressione del dito sul display nello stesso punto che non è ancora stata interrotta quindi al momento non interpretabile in altra maniera;
- onSingleTapUp: il classico tap, un colpetto singolo su display assimilabile ad un click.
Come si può prevedere, un'unica gesture può richiamare più metodi tra quelli citati. Ad esempio, pensiamo al fling, la sequenza di metodi che viene invocata è:
- onDown al momento di posare il dito sul display;
- onScroll – non uno solo ma una serie – durante lo sfregamento del dito sul display;
- onFling, al finale, solo quando il dito viene distaccato dal display.
Solo l'ultimo punto rappresenta il completamento della gesture con la sua definitiva connotazione.
Visto che ne abbiamo fatto cenno vediamo un piccolo esempio che può essere attuato all'interno del metodo onFling:
@Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (e1.getX()>e2.getX()) Toast.makeText(getApplicationContext(), "verso sx", Toast.LENGTH_SHORT).show(); else Toast.makeText(getApplicationContext(), "verso dx", Toast.LENGTH_SHORT).show(); return true; }
questa implementazione mostra che vengono ricevuti due oggetti MotionEvent, uno rappresenta il punto di inizio della gesture (il momento di down in cui il dito è stato posato sul display) e l'altro che rappresenta il punto in cui il dito viene sollevato completando il fling.
La differenza tra le ascisse dei due oggetti MotionEvent permette di scoprire se lo swipe è stato eseguito verso destra o sinistra, risultato che viene notificato mediante Toast.
41. Bluetooth
Auricolari, tastiere, mouse. Ma anche modem, navigatori e vivavoce. Il termine Bluetooth richiama alla mente una serie di dispositivi diffusissimi nel quotidiano dell'utente. In generale, stiamo parlando di un protocollo per la comunicazione wireless a brevi distanze, solitamente operante nel raggio di alcuni metri. Gli smartphone attuali lo integrano nella maggior parte dei casi e non potrebbe essere diversamente date le potenzialità di interazione a livello elettronico che offre.
Inoltre se da un lato molti dispositivi che fino a pochi anni fa venivano utilizzati in Bluetooth sono ormai stati assorbiti – a livello di funzionalità – dagli smartphone (si pensi al navigatore satellitare) l'attuale mercato della microelettronica offre nuovi strumenti altamente configurabili per i quali questa via di comunicazione appare uno scenario efficiente e produttivo. In questa categoria rientra la famosissima scheda programmabile Arduino o il microcomputer Raspberry Pi. Possono essere impiegati in soluzioni utili in ambienti domestici, lavorativi o industriali ed un'app Android sarebbe la soluzione ideale per attuarne il controllo.
Sono quindi tanti i motivi per affrontare l'integrazione di Bluetooth nelle API Android ed il presente capitolo mostra una panoramica dei principali concetti utili per mettersi in marcia.
Discovery dei dispositivi
La prima fase, solitamente necessaria, per lavorare con Bluettoth è la ricerca (anche denominata discovery, scoperta) di dispositivi disponibili nei paraggi. L'esempio che stiamo per mostrare si occupa proprio di questo. Cercherà eventuali controparti nei paraggi e ne mostrerà i nomi in una ListView.
Per fare ciò è necessario innanzitutto che:
- il nostro dispositivo Android disponga del supporto Bluetooth;
- nell'AndroidManifest della nostra applicazione siano predisposte le necessarie permission:
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
Oltre che essere disponibile nel dispositivo, il Bluetooth dovrebbe essere attivato. Ciò non è stato richiesto tra i prerequisiti visto che nell'esempio seguente impareremo ad affrontare anche questa situazione.
Il layout dell'app è molto semplice, contiene solo un pulsante per attivare la ricerca ed una ListView per mostrarne i risultati.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Cerca dispositivi" android:onClick="scan"/> <ListView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/listview"/> </LinearLayout>
All'interno dell'Activity Java ci occuperemo, come presumibile, di istanziare tutti gli oggetti necessari al funzionamento del layout, in primis Adapter e ListView, ma soprattutto di approntare il necessario all'utilizzo di Bluetooth.
La classe prioritaria per l'integrazione è ancora una volta uno specifico Adapter: BluetoothAdapter. Sarà lui, una volta che il supporto per il protocollo sarà correttamente attivato, a cercare “in giro” eventuali dispositivi con cui comunicare.
L'impostazione dell'Activity è la seguente:
public class MainActivity extends Activity { private BluetoothAdapter btAdapter; private Set dispositivi; private ListView lv; private ArrayAdapter adapter = null; private static final int BLUETOOTH_ON=1000; . . . . . . }
La costante intera BLUETOOTH_ON servirà solo come codice di richiesta nell'uso dell'Intent. Nelle righe precedenti si vede anche un'altra classe legata al mondo Bluetooth ed è BluetoothDevice. Il Set di oggetti di questo tipo conterrà i riferimenti dei dispositivi individuati nelle vicinanze.
Il metodo onCreate predispone le configurazioni iniziali:
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btAdapter = BluetoothAdapter.getDefaultAdapter();
lv = (ListView)findViewById(R.id.listview);
adapter=new ArrayAdapter(this,android.R.layout.simple_list_item_1);
lv.setAdapter(adapter);
}
mentre il metodo scan serve a gestire il click sul pulsante:
public void scan(View v) { if (!btAdapter.isEnabled()) { Intent turnOn = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(turnOn, BLUETOOTH_ON); } else load(); }
Qui iniziamo ad entrare nel vivo delle funzionalità. Infatti, come detto in precedenza, il supporto Bluetooth nel dispositivo Android deve essere presente ed attivato. Il metodo isEnabled cercherà di verificare proprio questo ed in caso di esito negativo lancerà l'Intent che chiederà all'utente il permesso di farlo (vedere in figura).
La finestra di dialogo sarà proprio il risultato dell'Intent lanciato. Visto che si è usato il metodo startActivityForResult, il risultato verrà consegnato nel metodo onActivityResult di cui faremo l'override:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode, resultCode, data);
if (requestCode==BLUETOOTH_ON && resultCode==RESULT_OK)
{
load();
}
}
Come si può vedere per essere sicuri che l'utente ha accettato l'attivazione del Bluetooth è necessario che il codice di richiesta sia pari a BLUETOOTH_ON ed il codice di ritorno a RESULT_OK, una costante definita all'interno della classe Activity.
Infine il metodo load, più volte invocato, contiene il codice necessario per reperire i dispositivi ed inserirne i nomi all'interno dell'adapter:
private void load() { dispositivi = btAdapter.getBondedDevices(); adapter.clear(); for(BluetoothDevice bt : dispositivi) adapter.add(bt.getName()); }
La figura seguente mostra i nomi dei dispositivi trovati. Nel caso dell'esempio è stato individuata una sola interfaccia Bluetooth con cui comunicare il cui nome è ospite-pc-0.
42. NFC
NFC, letteralmente Near Field Communication (“comunicazione a corto raggio”), è una tecnologia a radiofrequenza che permette di identificare oggetti e scambiare dati a distanza molto ravvicinata, solitamente ad un massimo di 4 cm anche se il valore teorico raggiunge i 10.
Il dialogo si verifica al momento dell'avvicinamento tra i due attori:
- l'initiator, tipicamente un dispositivo evoluto come uno smartphone o un tablet, dotato delle necessarie API per l'interazione in NFC;
- il target, un elemento molto semplice su cui generalmente vengono scritti o letti dati, a volte denominato tag.
Ciò che rende particolarmente accattivante questa tecnologia è che i target sono di dimensioni molto ridotte, piuttosto economici e riescono quasi a scomparire alla vista celandosi in oggetti minuscoli come etichette, smartcard e portachiavi. Uno dei fattori che permette il possesso di queste caratteristiche è l'assenza di alimentazione. Infatti i tag NFC sono passivi: vengono cioè alimentati mediante il campo elettromagnetico indotto dall'initiator al momento dell'avvicinamento. Questo, di converso, è la principale ragione del perchè la distanza di utilizzo si riduce a pochi centimetri.
Le modalità di funzionamento dei dispositivi che supportano NFC sono tre:
- Peer Mode: i dispositivi scambiano dati dialogando “alla pari” ossia con ruoli equivalenti;
- Reader/Write Mode: il dispositivo legge e/o scrive dati da/su un target passivo. È una modalità di utilizzo molto comune;
- Card emulation mode: il dispositivo agisce emulando il comportamento di una card in modo da poter essere letto da ulteriori dispositivi.
NFC in Android
Il supporto per NFC su Android è stato introdotto con la versione 2.3 e si è andato ampliando sempre più fino all'arrivo di Android Beam nella versione Ice Cream Sandwich, che ha definitivamente messo a disposizione degli sviluppatori la Peer Mode.
Tutte le classi necessarie sono state inserite nei package android.nfc e android.nfc.tech.
Affinchè possano essere utilizzate è necessario che vengano rispettati tre prerequisiti, tutti da specificare nel file AndroidManifest.xml:
- la versione minima dell'SDK deve essere la 10, inserendo pertanto:
<uses-sdk android:minSdkVersion="10"/> - a livello hardware, deve essere disponibile il lettore NFC nel dispositivo, richiedendolo come apposita feature:
<uses-feature android:name="android.hardware.nfc" android:required="true"/> - deve essere richiesta la permission necessaria con:
<uses-permission android:name="android.permission.NFC"/>
La classe di accesso al mondo NFC è NfcAdapter. Se ne ottiene un riferimento mediante l'invocazione:
NfcAdapter adapter = NfcAdapter.getDefaultAdapter(this);
Tale riferimento, per essere valido e dimostrare che la tecnologia NFC è nelle nostre disponibilità, dovrebbe:
- non essere nullo;
- restituire true all'invocazione del metodo adapter.isEnabled().
Una volta verificate queste condizioni, possiamo procedere con l'utilizzo di questa tecnologia.
Come tante altre attività nel sistema Android, la notifica di informazioni avviene mediante Intent. Questo tipo di messaggi sono alla base del meccanismo primario per l'interazione con NFC: la scansione di ricerca dei tag.
Lo scopo di questi Intent è quello di essere intercettati direttamente da un'Activity e la loro tipologia specifica quale tecnologia è stata riconosciuta nel tag individuato:
- NfcAdapter.ACTION_NDEF_DISCOVERED: è stato individuato un tag che implementa lo standard NDEF;
- NfcAdapter.ACTION_TECH_DISCOVERED: viene lanciato dal sistema se il tag implementa una tecnologia nota che esula dallo standard NDEF o anche se il tag è NDEF ma non c'è alcuna applicazione installata in grado di gestirla;
- NfcAdapter.ACTION_TAG_DISCOVERED: significa che non c'è alcuna applicazione sul dispositivo che è in grado di gestire gli standard cui il tag si riferisce o che quest'ultimo non mostra di possedere alcuna tecnologia nota.
Affinchè la nostra applicazione possa essere destinataria di uno di questi Intent è necessario che siano specificati all'interno del Manifest uno o più IntentFilter necessari:
<activity android:name="....." android:label="....."> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> <intent-filter> <action android:name="android.nfc.action.NDEF_DISCOVERED"/> <category android:name="android.intent.category.DEFAULT"/> <data android:mimeType="text/plain"/> </intent-filter> </activity>
All'action MAIN negli intent-filter siamo abituati già dai primi esempi di questa guida. Serve a selezionare quale activity all'interno dell'applicazione sia quella che deve essere invocata per prima. Con questo nuovo filtro, invece, la nostra app indica al sistema di voler candidarsi alla gestione di una determinata tipologia di tag, in questo caso NDEF.
Allo scopo, può essere utile sapere che oltre all'action si può specificare quali tecnologie trattare. Lo si può fare utilizzando le risorse di tipo <tech-list> e <tech> in un file di configurazione collocato nella cartella res/xml. Una volta definita questa risorsa la si può collegare ad un intent-filter mediante un nodo <meta-data>.
Un altro aspetto che va spiegato è che la nostra Activity deve registrarsi al foreground dispatch system. Esso dovrà essere attivo in tutta la fase di interazione tra utente e app quindi – come abbiamo imparato a proposito del ciclo di vita – tra l'invocazione dei metodi onResume e onPause. Lo scopo di questo è evitare che se viene avvicinato il dispositivo al tag mentre l'app è già aperta, non si tenterà di aprirla nuovamente. Ciò viene attuato con il metodo enableForegroundDispatch e disattivato con disableForegroundDispatch, entrambi della classe NfcAdapter.
I dati relativi al tag rilevato saranno reperiti all'interno dell'Intent che verrà inviato all'Activity. A seconda della tipologia di target e delle tecnologie con le quali si è scelto di dialogare, si potranno usare delle classi apposite come NdefMessage e NdefRecord, di cui la documentazione ufficiale spiega diffusamente i dettagli.
Multimedia
43. Scattare una foto
Potrebbe essere utile integrare nelle proprie app la capacità di scattare direttamente una foto ed utilizzarla “al volo”. Per farlo sfrutteremo le potenzialità già incluse nel sistema per la consueta logica di “non inventare di nuovo la ruota”.
Il dispositivo non mette solo a disposizione la macchina fotografica in quanto componente hardware ma anche il software e le API di gestione. Scopo di questo capitolo sarà imparare ad aprire dall'app direttamente il programma per scattare la foto ed ottenere come risultato l'immagine acquisita. Il vantaggio di integrare il programma ufficiale per le foto sta nell'avere a disposizione tutta la sua completezza: gestione di zoom, effetti, configurazioni.
Il primo concetto da affrontare è l'uso degli Intent per attivare il software fotografico. Non stiamo affrontando un argomento nuovo, dal momento che gli Intent li abbiamo già usati per invocare l'apertura di un'Activity secondaria in un'app. Avevamo, in quell'occasione, preannunciato l'importanza di questo meccanismo e la sua capacità di recapitare messaggi al di fuori dell'applicazione, a livello di sistema operativo. Adesso è giunto il momento di vederlo in pratica.
L'Intent che useremo richiede l'esecuzione di un'azione a livello globale, definita mediante una costante presente in un ContentProvider di sistema, il MediaStore:
Intent photoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); startActivityForResult(photoIntent, PHOTO_REQUEST_CODE);
Il metodo utilizzato per avviare l'Intent è startActivityForResult. Si tratta di un meccanismo che permette di ricevere un risultato nella Activity chiamante. Il valore PHOTO_REQUEST_CODE è una costante intera definita nell'Activity stessa e serve solo a fornire un ID della richiesta.
Il risultato – quindi il completamento della fotografia – sarà fruibile all'interno del metodo onActivityResult e sarà reperibile mediante il campo data degli Extras:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode, resultCode, data);
if (requestCode==PHOTO_REQUEST_CODE)
{
Bitmap bp = (Bitmap) data.getExtras().get("data");
photo.setImageBitmap(bp);
}
}
Il codice per la gestione dell'evento non fa altro che prelevare l'oggetto Bitmap corrispondente alla foto scattata e posizionarlo all'interno di una ImageView, qui rappresentata dalla variabile photo. Ciò permetterà di vedere la versione rimpicciolita della foto, innestata all'interno del layout.
44. Registrare un video
Dopo aver imparato a scattare una foto ed importarla nel progetto come Bitmap, è arrivato il momento di sfruttare la caratteristica “gemella”: la registrazione di un video.
L'approccio è il medesimo del capitolo precedente ed i concetti di punta sono gli stessi. Li riassumiamo:
- l'attivazione del software di sistema per la realizzazione di video viene invocato mediante un action impostata all'interno del MediaStore, un ContentProvider di sistema;
- l'Intent per il lancio dell'action sarà inoltrato con il metodo startActivityForResult;
- dopo la registrazione del video, verrà eseguito il metodo onActivityResult, grazie al quale potremo sfruttare i risultati.
Le righe seguenti inviano l'Intent per l'apertura del software della videocamera di sistema:
Intent videoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); startActivityForResult(videoIntent, VIDEO_REQUEST_CODE);
Queste istruzioni possono essere inserite in qualunque punto dell'Activity. Tipicamente verranno collocate in un metodo che gestisce, ad esempio, il click di un pulsante. La costante VIDEO_REQUEST_CODE è di tipo int ed è stata definita nell'Activity per riconoscere con un ID l'invocazione.
Il codice che segue mostra il metodo onActivityResult, utile per gestire il ritorno del controllo all'Activity, subito dopo aver girato il video.
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (VIDEO_REQUEST_CODE == requestCode) { Uri videoUri = data.getData(); video.setVideoURI(videoUri); video.start(); } }
Nell'esempio precedente si vede come è stato utilizzato il codice di richiesta VIDEO_REQUEST_CODE, e chiaramente a questo punto si potrebbe svolgere qualunque operazione. In questo caso specifico si è scelto di avviare direttamente la riproduzione del video.
Si noti che è stato utilizzato un controllo VideoView, una tipologia di View che permette di riprodurre un video di cui si è passato un riferimento tramite il metodo setVideoURI. Tale riferimento viene recuperato, sotto forma di URI, dai dati di ritorno dell'Intent.
45. MediaPlayer: riprodurre file audio
La multimedialità è una delle facoltà più importanti di un dispositivo mobile. Basti pensare a quanto siano state innovative e di successo le introduzioni della radio e del lettore MP3 sui telefoni cellulari. Anche Android, quindi, ha un suo componente integrato per la fruizione di contenuti multimediali: il MediaPlayer.
In questo capitolo si imparerà ad utilizzarlo integrandolo rapidamente all'interno di un'app. Con poche righe di codice creeremo un lettore MP3 rudimentale ma funzionante, in grado di riprodurre una sola canzone, che per sempicità salveremo tra le risorse con ID R.id.canzone. Partendo dall'esempio che vedremo, estenderne l'utilizzo a raccolte di file musicali risulterà abbastanza semplice.
I metodi che sfrutteremo della classe MediaPlayer sono i seguenti:
- create: serve per ottenere un riferimento all'istanza del MediaPlayer. Non ne creeremo direttamente uno nuovo mediante l'operatore new di Java, ma faremo in modo che sia il sistema stesso ad instanziarlo. Tra i parametri di questo metodo passeremo anche l'ID della risorsa che corrisponde alla canzone da ascoltare;
- play, stop e pause: dopo tutti i mangianastri, videoregistratori e lettori CD che hanno affollato la nostra vita sin da piccoli, appare quasi superfluo spiegare a cosa servano questi tre metodi. A scanso di equivoci, specifichiamo che essi consentono rispettivamente di avviare, arrestare e mettere in pausa la fruizione del contenuto multimediale;
- getDuration e getCurrentPosition: forniscono informazioni temporali sull'esecuzione del contenuto. Il primo restituisce la durata complessiva della canzone mentre con il secondo otteniamo l'intervallo tempo che separa l'inizio della canzone con la posizione temporale attualmente in riproduzione.
Il lettore MP3
Nel layout del nostro lettore inseriremo una SeekBar (che mostrerà l'avanzamento della musica in esecuzione) ed i pulsanti che consentono di utilizzare i controlli fondamentali:
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TableRow android:gravity="center" android:padding="20dp"> <SeekBar android:id="@+id/bar" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" /> </TableRow> <TableRow android:gravity="center" android:padding="20dp"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="play" android:text="Play" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="pause" android:text="Pause" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="stop" android:text="Stop" /> </TableRow> </TableLayout>
Il risultato finale è mostrato nella figura seguente.
All'interno dell'Activity, il metodo onCreate fisserà i riferimenti principali alla SeekBar e al MediaPlayer:
public class MainActivity extends Activity { private MediaPlayer mp=null; private Handler handler = new Handler(); private double startTime = 0; private SeekBar sk=null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); sk=(SeekBar) findViewById(R.id.bar); mp=MediaPlayer.create(this, R.raw.canzone ); } . . . . . . }
I metodi play, pause e stop sono molto semplici, a dimostrazione della facilità di interazione con il MediaPlayer:
private Runnable updateBar = new Runnable() { public void run() { startTime = mp.getCurrentPosition(); sk.setProgress((int)startTime); handler.postDelayed(this, 100); } }; public void play(View v) { mp.start(); sk.setMax((int) mp.getDuration()); handler.postDelayed(updateBar,100); } public void pause(View v) { mp.pause(); } public void stop(View v) { mp.stop(); }
Nel codice presentato, si vede che per richiedere le tre operazioni di base del Mediaplayer – start, stop, pause – è sufficiente richiamare i metodi omonimi. Tra di essi si inserisce il meccanismo di aggiornamento della barra di progresso. Il suo avanzamento rappresenta l'andamento della riproduzione. Essa è realizzata in maniera piuttosto semplice: si è usato un handler temporizzato che legge ogni 100 millisecondi la posizione attuale di riproduzione, ed attui il conseguente aggiornamento della barra. Il valore massimo cui il progresso può arrivare è la durata totale del brano, e viene impostato all'interno del metodo play utilizzando come fonte di informazione il risultato del metodo getDuration del Mediaplayer.
46. MediaPlayer: riprodurre file video
Nella lezione precedente abbiamo descritto la classe MediaPlayer come uno strumento in grado di fornire funzionalità multimediali, che consentono la fruizione di contenuti audio e video in un'app Android. Nell'esempio presentato in precedenza lo si è utilizzato per realizzare un semplice lettore MP3.
In questa lezione, sperimenteremo le sue capacità di gestione dei video. Grazie al MediaPlayer, infatti, potremo riprodurre un filmato all'interno della nostra app. L'esempio che vedremo utilizzerà un file multimediale contenuto tra le risorse del progetto, ma accenneremo anche all'utilizzo del MediaPlayer per la fruizione di filmati disponibili in rete.
L'esempio
Il video che vorremo visualizzare sarà collocato nelle risorse, in particolare nella cartella raw destinata a contenere file di una tipologia non perfettamente inquadrabile tra le categorie standard (layout, values, menu, drawable, ecc.). Nel codice che verrà presentato, pertanto, ci riferiremo al video come risorsa, ed il suo ID sarà R.raw.video.
Quanto già appreso in precedenza in merito al MediaPlayer resta valido. In questo caso ci occuperemo per lo più di istanziarlo (sempre con il metodo di comodo create), avviare la proiezione del video e rilasciare le risorse al termine delle operazioni.
Il layout
Si è volutamente scelto di utilizzare un layout molto semplice. L'unico widget al suo interno svolge il ruolo di “contenitore” del video:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#EEFFCD" android:padding="20dp" > <SurfaceView android:id="@+id/surfView" android:layout_width="match_parent" android:layout_height="300dp" /> </RelativeLayout>
Il widget, in particolare, è un controllo di classe SurfaceView. Si tratta di una superficie disegnabile “incastonata” nella finestra dell'interfaccia utente. Solitamente, non è necessario specificare altre impostazioni, se non la posizione e le dimensioni.
I contenuti al suo interno vengono manipolati mediante un oggetto di classe SurfaceHolder. Un riferimento ad esso può essere recuperato tramite il metodo getHolder(), disponibile nella classe SurfaceView.
Il codice
L'Activity implementa l'interfaccia SurfaceHolder.Callback, che richiede il completamento di tre metodi: surfaceCreated, surfaceChanged, surfaceDestroyed. La loro invocazione si verificherà, rispettivamente, la prima volta che la superficie viene creata, ogni volta che subisce dei cambiamenti e quando viene distrutta.
Nel nostro esempio, implementiamo soltanto il metodo surfaceCreated. Al suo interno, sicuri dell'avvenuta creazione della superficie, potremo predisporre il codice di avvio del video.
Ecco l'Activity:
public class MyActivity extends Activity implements SurfaceHolder.Callback { private MediaPlayer mediaPlayer; private SurfaceHolder holder; private SurfaceView surface; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_my); surface = (SurfaceView) findViewById(R.id.surfView); holder = surface.getHolder(); holder.addCallback(this); } @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { mediaPlayer= MediaPlayer.create(this,R.raw.video); mediaPlayer.setDisplay(holder); mediaPlayer.setOnPreparedListener( new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mediaPlayer) { mediaPlayer.start(); } } ); mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mediaPlayer) { mediaPlayer.release(); } }); } @Override public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i2, int i3) { } @Override public void surfaceDestroyed(SurfaceHolder surfaceHolder) { } }
Appena avviato l'esempio, vedremo il video apparire ed andare in esecuzione nel layout. Prima di farlo però dovremo ricordarci di inserire un video nelle risorse.
All'interno del metodo onCreate dell'Activity, non facciamo altro che svolgere impostazioni di base: assegnamo un layout alla UI e facciamo in modo che il SurfaceHolder utilizzi l'Activity stessa come listener per i propri eventi di callback.
Più interessante è invece il codice all'interno del metodo surfaceCreated. Per prima cosa, viene recuperato un riferimento al MediaPlayer tramite create(), e viene passato anche l'ID del filmato come argomento. Successivamente assegnamo il display al MediaPlayer indicando, in pratica, quale sarà il contenitore del video.
Affinchè il video possa essere avviato al momento opportuno, invocheremo il metodo start() all'interno di un listener di classe OnPreparedListener. Analogamente, vorremo poter liberare memoria al termine della proiezione; pertanto, all'interno di un listener di tipo OnCompletionListener, invocheremo il metodo release(), sempre appartenente alla classe MediaPlayer.
Video "remoti"
Prima di terminare, vediamo come potere visualizzare un video remoto. In questo caso, il file che vogliamo riprodurre non sarà contenuto nelle risorse, bensì disponibile in rete ad un determinato URL. Per riprodurlo, quindi, dovremo:
- inserire nel file AndroidManifest.xml la permission per l'accesso a Internet:
<uses-permission android:name="android.permission.INTERNET" /> - invocare un overload del metodo
create()che non richieda un ID di una risorsa, ma un oggetto Uri contenente l'indirizzo remoto del video:mediaPlayer = MediaPlayer.create(this,Uri.parse("http://www.example.com/video.mp4"));
47. Gestire l'audio
Quello del multimedia è uno dei settori più floridi e di maggiore interesse delle applicazioni per dispositivi mobili. Abbiamo già visto come sia possibile utilizzare il MediaPlayer per la riproduzione di contenuti multimediali. In questo capitolo approfondiremo la tematica relativa alla gestione dell'audio. In particolare verranno trattati due aspetti: la registrazione dell'audio e l'AudioManager.
Registrare e riascoltare
L'esempio presentato è un grande classico del multimedia: una semplice applicazione con due pulsanti, uno per avviare/fermare la registrazione, l'altro per gestire l'ascolto.
Vediamo subito il layout, molto semplice:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/btn_registra" android:text="Registra" android:onClick="registra"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/btn_ascolta" android:text="Ascolta" android:onClick="ascolta"/> </LinearLayout>
Il codice dell'Activity, oltre all'onCreate(), include diversi altri metodi che servono a gestire i pulsanti:
public class MainActivity extends Activity { private MediaRecorder registratore = null; private MediaPlayer riproduttore = null; private static String filename = null; private boolean ascoltando=false; private boolean registrando=false; @Override public void onCreate(Bundle args) { super.onCreate(args); setContentView(R.layout.activity_main); filename = Environment.getExternalStorageDirectory().getAbsolutePath(); filename += "/registrazione.3gp"; } @Override public void onPause() { super.onPause(); if (registratore != null) { registratore.release(); registratore = null; } if (riproduttore != null) { riproduttore.release(); riproduttore = null; } } . . . . . . }
I membri privati visibili nell'Activity sono:
- due booleani, chiamati ascoltando e registrando. Loro eventuali valori positivi indicherebbero che è in corso, rispettivamente, la riproduzione dell'audio e la registrazione;
- filename è una stringa che indica il nome del file in cui andremo a salvare l'audio registrato. Il salvataggio verrà fatto nella directory principale dello storage esterno il che permetterà, in caso di dubbi, di cercarlo sulla SD card ed ascoltarlo con il lettore di sistema;
- i due protagonisti dell'esempio, MediaRecorder e MediaPlayer. Appartengono entrambi al package android.media e servono a registrare l'audio – il primo – e a gestirne la riproduzione, il secondo.
Il frammento di codice precedente mostra anche onCreate e onPause. Il primo metodo non fa altro che svolgere inizializzazioni, mentre il secondo fa un po' di pulizia in memoria controllando che il MediaPlayer ed il MediaRecorder siano non-nulli ed in tal caso chiede il rilascio delle rispettive risorse.
Al click su uno dei pulsanti del layout, verranno impartiti i comandi per la registrazione e l'ascolto. In entrambi i casi l'Activity cambierà nome al pulsante, assegnando un'etichetta appropriata. In tutto ciò, giocano un ruolo fondamentale i due booleani, denominati ascoltando e registrando, che permettono di avere sempre chiaro in quale fase di lavoro l'applicazione si trovi. Questi i metodi che reagiscono al click dei pulsanti:
public void registra(View v) { Button btn=(Button) v; if (registrando) { // serve ad interrompere fermaRegistrazione(); btn.setText("Registra"); } else { // serve ad iniziare la registrazione registra(); btn.setText("Ferma registrazione"); } registrando=!registrando; } public void ascolta(View v) { Button btn=(Button) v; if (ascoltando) { // serve ad interrompere fermaRiproduzione(); btn.setText("Ascolta"); } else { // serve ad iniziare la riproduzione dell'audio riproduci(); btn.setText("Ferma"); } ascoltando=!ascoltando; }
Infine, vediamo il cuore dell'esempio, la parte che conterrà il codice più significativo: i metodi che provvedono ad avviare/interrompere riproduzione e registrazione.
private void riproduci() { riproduttore = new MediaPlayer(); try { riproduttore.setDataSource(filename); riproduttore.prepare(); riproduttore.start(); } catch (IOException e) { // gestisci eccezione } } private void fermaRiproduzione() { riproduttore.release(); riproduttore = null; } private void registra() { registratore = new MediaRecorder(); registratore.setAudioSource(MediaRecorder.AudioSource.MIC); registratore.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); registratore.setOutputFile(filename); registratore.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); try { registratore.prepare(); } catch (IOException e) { // gestisci eccezione } registratore.start(); } private void fermaRegistrazione() { registratore.stop(); registratore.release(); registratore = null; }
Per quanto riguarda il MediaPlayer, non abbiamo nulla da aggiungere a quanto visto nel capitolo al riguardo. Anche il MediaRecorder non riserva grandi sorprese. Come si può vedere, è uno strumento molto completo: non solo permette di impostare tutti i parametri della registrazione – tra cui il microfono come sorgente ed il formato .3gp – ma si occupa di tutta la fase di registrazione su file (è sufficiente impostargli nome e percorso della destinazione).
AudioManager
Oltre all'esempio riportato, è giusto citare un'altra classe appartenente al framework: AudioManager. Si tratta di un servizio di sistema, richiamabile quindi nel seguente modo:
AudioManager manager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
Offre diversi metodi per configurare rapidamente vari aspetti dell'audio. Almeno due di essi meritano di essere citati per la grande importanza che rivestono:
- setRingerMode: permette di impostare il volume della suoneria del dispositivo. Richiede in input un valore intero da scegliere tra valori costanti: RINGER_MODE_SILENT (silenzioso), RINGER_MODE_NORMAL (normale suoneria), RINGER_MODE_VIBRATE (con vibrazione);
- adjustVolume: permette di regolare il volume. La direzione della variazione di volume va descritta con apposite costanti: ADJUST_LOWER (per diminuire) e ADJUST_RAISE (per aumentarlo).
48. Android e la low latency
Dopo i capitoli tecnici che hanno mostrato le azioni più comuni che possono essere eseguite nel multimediale, ci soffermiamo, in questo capitolo, su una problematica particolare la cui risoluzione è tuttora in corso. Stiamo parlando della latenza audio nei sistemi Android. Tecnicamente, ci si riferisce alla latenza come al ritardo temporale che subisce un segnale audio quando passa attraverso un sistema. Questo “passare attraverso” comprende una sequenza di fasi che possono essere riassunte in tre diverse attività: conversione analogico-digitale (fase di ingresso), elaborazione, conversione digitale-analogico (fase di uscita).
Il pubblico interessato maggiormente a questa problematica è sicuramente quello dei professionisti della musica (DJ, musicisti o semplicemente appassionati), oltre ovviamente ad un settore piuttosto ampio di sviluppatori. Sul tema, non si può negare che il mondo Android si trovi in un certo ritardo rispetto ad Apple, anche se nelle ultime versioni è stato fatto molto per migliorare la latenza audio sui sistemi Android. La ricerca di una “cura” presenta difficoltà relative alla diversità delle origini del problema stesso: capacità hardware, librerie disponibili, rapporto con il codice nativo, oltre alla consueta aggravante della frammentazione del panorama elettronico, che non rende universalmente valide le soluzioni individuate.
La corsa verso la bassa latenza ha accelerato molto il suo ritmo negli ultimi anni, vivendo un particolare momento di fama durante il Google IO 2013, in un talk riguardante il miglioramento delle performance audio:
La documentazione Android specifica che dalla versione 4.1 sono stati introdotti dei cambiamenti architetturali finalizzati alla soluzione del problema. Viene avvertito comunque il lettore che tutte le spiegazioni in merito fornite non sono rivolte agli sviluppatori di applicazioni, ma ai produttori di hardware e relativi driver, per una corretta implementazione delle problematiche audio sulle nuove versioni del sistema.
Grafica
49. Animazioni con XML
In questa guida abbiamo già approcciato la sfera della grafica, quando abbiamo parlato di stili ed immagini. Questo capitolo vuole proporre l'approccio ad un settore della grafica molto vasto, affrontabile con soluzioni avanzate e pretenziose o in maniera più semplice ma comunque efficace. Inizieremo parlando delle animazioni, tipiche di presentazioni pubblicitarie e videogame, ma che possono essere presenti anche su altre tipologie di app in cui sia utile poter dare, per così dire, un tocco di vitalità.
Animazioni con XML
In precedenza, abbiamo scoperto che con i Drawable è possibile disegnare in XML. Ora scoprirermo che è anche possibile creare animazioni con questo formato di dati. Gli esempi che mostreremo sono probabilmente il modo più rapido per ottenere i primi risultati ed iniziare ad affrontare uno scenario così ampio con poco sforzo.
Le animazioni, come molte altre cose nei nostri progetti, sono risorse. La loro configurazione in XML dovrà essere inserita in un file all'interno della cartella res/anim.
Per gli esempi che utilizzeremo, predisporremo un semplice layout con un testo “Hello world” in posizione centrale, ed un pulsante in alto a sinistra con su scritto “Attiva animazione”. Alla pressione di quest'ultimo controllo, l'animazione verrà avviata e sarà applicata alla scritta “Hello world”. Data la facilità di configurazione delle animazioni in XML, le direttive saranno inserite nel file res/anim/animazione.xml e sarà sufficiente sostituirne il contenuto per sperimentare nuove animazioni.
Il layout che utilizzeremo è il seguente:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:text="Avvia animazione" android:onClick="avvia"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:id="@+id/txt" android:text="@string/hello_world" /> </RelativeLayout>
Di seguito è mostrato, invece, il codice Java dell'Activity:
public class MainActivity extends Activity { private Animation anim=null; private TextView txt=null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); txt=(TextView) findViewById(R.id.txt); anim = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.animazione); } public void avvia(View v) { txt.startAnimation(anim); } }
Come si vede, è molto semplice, e somiglia agli esempi già visti. Le uniche novità sono:
- la classe AnimationUtils che, all'interno del metodo
onCreate, carica l'animazione semplicemente richiamandone l'ID di risorsa; - il metodo startAnimation che attiva l'animazione sulla View, in questo caso una TextView.
Ciò che manca è completare il file dell'animazione, che abbiamo deciso di chiamare animazione.xml. Come primo esperimento realizzeremo una rotazione a 360 gradi, ripetuta 3 volte:
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <rotate android:fromDegrees="0" android:toDegrees="2000" android:pivotX="50%" android:pivotY="50%" android:duration="360" android:repeatMode="restart" android:repeatCount="3" android:interpolator="@android:anim/cycle_interpolator"/> </set>
I nuovi tag impiegati sono due: <set> che racchiude l'insieme delle animazioni e <rotate>, specifico per l'operazione da effettuare. Ciò ci consentirà di eseguire l'animazione in seguito al click dell'apposito pulsante.
Fatto questo, potremo provare un effetto di ingrandimento con il tag <scale>, semplicemente sostituendo il contenuto del file animazione.xml:
<scale xmlns:android="http://schemas.android.com/apk/res/android" android:duration="1000" android:fromXScale="1" android:fromYScale="1" android:toXScale="4" android:toYScale="4" android:pivotX="50%" android:pivotY="50%"> </scale>
Il risultato sarà l'ingrandimento del testo “Hello world” di un fatto 4 (cioè, 4 volte più grande).
Quelli visti, ovviamente, sono solo degli esempi. La documentazione ufficiale permetterà di trovare tante altre opportunità ed idee. Sottolineaiamo comunque alcuni spunti interessanti:
- il tag <set> non deve necessariamente contenere una singola animazione, anzi il suo stesso nome richiama il concetto dell'insieme e lascia intendere che al suo interno possono essere concentrate più direttive XML;
- impostando opportunamento gli attributi sarà possibile ottenere effetti finali molto diversi. Oltre a <scale> e a <rotate> ci sono altri tag che molto utili, come <alpha> che regola le variazioni di trasparenza permettendo di creare effetti di dissolvenza, e <translate> che permette di ordinare spostamenti delle View;
- se si vogliono effettuare sequenze di animazioni, ed eseguire operazioni tra di esse, è possibile trasformare l'Activity o un altro oggetto in un listener per i relativi eventi. Ciò viene fatto implementando l'interfaccia AnimationListener e comunicandolo all'istanza Animation con il metodo
setAnimationListener. Fatto questo, si inserirà il codice nei metodi di cui è obbligatorio fornire l'override:onAnimationStart,onAnimationEndeonAnimationRestart.
50. Supporto multirisoluzione
La diffusione di Android su dispositivi molto eterogenei ha fatto la fortuna di questo sistema operativo, permettendogli di dimostrare le sue doti di adattamento. Ma come sappiamo ha creato non pochi grattacapi agli sviluppatori. Uno degli aspetti più delicati, infatti, è la necessità di adattare il layout al display.
Nel capitolo relativo alle risorse, si è spiegato come l'unità di misura da preferire sia il Density-Indipendent Pixel, in sigla dp, che rappresenta un pixel non vincolato dalla densità del display. Proprio questo concetto di densità, intesa come rapporto tra numero di pixel e dimensioni dello schermo, ha assunto un ruolo di primo piano, prevaricando in importanza sia la risoluzione che la misura dello schermo.
Il passo concettuale doveroso, a questo punto, è l'abbandono della visione “pixel-centrica” delle misure proprio perchè questa non tiene conto della densità di popolazione dei pixel nel display. Definire le misure in pixel per gli elementi dei layout comporterebbe una visualizzazione molto diversa tra schermi a bassa densità (dove le immagini verrebbero allargate) e quelli ad alta densità (dove le immagini risultarebbero rimpicciolite). La densità viene misurata in dpi ed il livello di 160 dpi (densità media o mdpi) rappresenta il punto di equilibrio. A questo livello, un pixel equivale ad un dp.
Gli altri livelli comuni per le densità sono:
- ldpi, la bassa densità, impostato a 120 dpi;
- mdpi, 160 dpi, come detto è la baseline delle densità;
- tvdpi, pensata per le tv: solitamente impostata a 213 dpi;
- hdpi, densità alta: 240 dpi;
- xhdpi, densità ultra-alta: 320dpi.
Per comprendere meglio quanto detto, può essere utile considerare la relazione tra pixel e dp, che può essere sintetizzata matematicamente come segue:
px = dp*(dpi/160)
Ciò può essere letto affermando che il rapporto tra pixel e dp è pari a quello tra la densità del display ed il livello di parità 160 dpi.
Non è un caso, quindi, che tra le risorse di default che Eclipse include nei propri progetti, l'immagine del logo del robottino verde (denominato ic_launcher.png) è replicata in più cartelle di risorse, ogni volta con dimensioni differenti:
- in drawable-mdpi, misura 48×48 pixel;
- in drawable-hdpi, 72×72 pixel;
- in drawable-xhdpi, 96×96 pixel.
Più è alta la densità di pixel cui è destinata l'immagine, più grandi sono le sue dimensioni. Ciò per contrastare il naturale rimpicciolimento che provocherebbe un maggior numero di pixel, a parità di dimensioni. Tutto ciò avviene nel rispetto delle proporzioni dettate dalla formula descritta poc'anzi.
Best practises
Affinchè le nostre applicazioni possano avere una migliore diffusione su dispositivi eterogenei, è fondamentale dotarle di interfacce fluide, e per questo è bene tenere a mente alcuni consigli:
- utilizzare il più possibile i RelativeLayout, che ha per sua natura si adatta ai display;
- nel dimensionamento di View e Layout, evitare di dichiarare esplicitamente le misure facendo ricorso il più possibile ai valori wrap_content e match_parent;
- tutte le misure necessarie vanno indicate in dp (o in sp per i font) evitando qualsiasi altra unità di misura che abbia attinenza col mondo reale (pixel, millimetri, pollici, etc.);
- diversificare le risorse in base alle possibili configurazioni dei dispositivi, predisponendo i nomi delle cartelle caratterizzati dagli appositi modificatori. Ciò vale per le densità, con le directory drawable-ldpi, drawable-hdpi, eccetera, ma anche per le diverse modalità di orientamento del display (layout-land per landscape o layout-port per portrait) e per le dimensioni del display (layout-small per schermi non superiori ai 3 pollici, ad esempio). La serie completa dei modificatori è disponibile sulla documentazione ufficiale.
51. Animazioni con ViewPropertyAnimator
Abbiamo visto che è possibile utilizzare l'XML per effettuare le animazioni. Oltre a questo tipo di possibilità, è necessario conoscere anche un altro strumento, ovvero la classe ViewPropertyAnimator. Si tratta di un oggetto relativamente giovane del framework, introdotto con Android 3.1 che permette di attuare animazioni con un approccio totalmente “Java”, tutto configurato via codice.
Questa classe offre le animazioni più comuni, che possono essere invocate mediante appositi metodi:
- rotazione lungo entrambi gli assi o solo rispetto all'asse X o Y: metodi
rotation,rotationX,rotationY; - ingrandimento o riduzione, anche questo su uno o più assi, con
scale(),scaleX()escaleY(); - traslazione in una o più direzioni con
translation(),translationX()otranslationY; - trasparenza, che può essere modificata con il metodo
alpha.
Per ottenere un riferimento ad un oggetto ViewPropertyAnimator è necessario invocare il metodo animate() sulla View. Ad esempio, il comando:
txt.animate().rotationY(180);
consente di effettuare la rotazione della View txt rispetto all'asse Y di 180 gradi.
È disponibile anche un altro meccanismo che avevamo apprezzato nelle animazioni XML: l'attivazione di codice in conseguenza a determinati eventi connessi dell'effetto grafico. È possibile utilizzare più approcci.
Il primo è utilizzando un AnimationListener: un oggetto può implementare questa interfaccia e definire l'override dei metodi astratti. ViewPropertyAnimator utilizza il metodo setListener, che riceve il riferimento all'oggetto configurato.
In alternativa, si possono usare metodi specifici di ViewPropertyAnimator: withStartAction e withEndAction. In entrambi i casi di deve passare come parametro un oggetto Runnable, che include il codice da eseguire al momento opportuno.
Le righe di codice seguente svolgono la medesima rotazione vista in precedenza, ma mostrano un messaggio Toast alla conclusione:
txt.animate().rotationY(180).withEndAction( new Runnable() { Override public void run() { Toast.makeText(getApplicationContext(), "Rotazione finita", Toast.LENGTH_SHORT).show(); } });
Gli altri metodi disponibili e sopra citati possono essere utilizzati in maniera analoga, ed un buon esercizio potrebbe essere quello di applicare tali metodi per riprodurre gli stessi esempi citati nel capitolo sulle animazioni XML, ovviamente ignorando i file XML ed inserendo le invocazioni a ViewPropertyAnimator all'interno del metodo avvia.
52. Accelerazione hardware
Una delle tendenze degli ultimi anni vede i dispositivi mobile guadagnare una fetta ulteriore di mercato sempre crescente rispetto ai PC tradizionali. Una domanda che in molti si stanno ponendo riguarda, ad esempio, la capacità dei tablet di poter sostituire nelle case di molti utenti i computer portatili. Oggetto di questo salto generazionale potrebbe essere quel pubblico composto dai non tecnici, ma dai normali utilizzatori interessati a consultare Internet, alla comunicazione e al multimedia.
In questa “lotta” tra dispositivi desktop e mobile, giocano un ruolo fondamentale i videogame e la soddisfazione che può dare la grafica 3D realizzata per un tablet piuttosto che per un PC. Le prestazioni, in questo senso, non dipendono solo dall'elaborazione software, ma anche dalla disponibilità di risorse hardware.
Gli strumenti che hanno accompagnato sinora l'evoluzione della grafica sono soprattutto:
- le librerie OpenGL, utilizzabile in binding da Java o in modalità nativa da NDK;
- il potenziamento dell'infrastruttura di calcolo, con GPU sempre più potenti ed un crescente utilizzo di processori multicore sui dispositivi.
Il problema con l'hardware sta nel fatto che, in genere, i potenziamenti si traducono in un maggior dispendio energetico, aggravando le problematiche di alimentazione già critiche sui dispositivi mobile.
Questa breve panoramica mostra il quadro della situazione, che ha portato ad un'importante innovazione in Android 3.0: la redirezione dell'intero sottosistema di UI verso l'accelerazione hardware.
A partire da questa versione del sistema operativo, quindi, tutte le operazioni di rendering su Canvas di una View possono sfruttare maggiormente le potenzialità dell'hardware del dispositivo, prima tra tutti la GPU. L'accelerazione hardware può essere attivata o disattivata dalla configurazione dell'applicazione, per poterla utilizzare quando sia effettivamente utile. In alcuni casi, infatti, la sua attivazione spropositata potrebbe comportare problemi inaspettati, soprattutto su operazioni in 2D.
All'interno dell'AndroidManifest, l'attributo XML che permette di abilitare/disabilitare l'accelerazione hardware è android:hardwareAccelerated ed il suo valore è di tipo booleano. Un aspetto molto importante è che, per evitare come detto spiacevoli inconvenienti, questa funzionalità può essere applicata a vari livelli:
- Applicazione;
- Activity;
- Window;
- View.
Inoltre, le View sono dotate del metodo isHardwareAccelerated, che permette di controllare a runtime se l'accelerazione hardware è attiva.
Telefono
53. Gestire gli SMS
La comunicazione via SMS potrebbe apparire un po' old-style in questi tempi così “social”. Eppure si tratta della forma di messaggistica più diffusa al mondo, utilizzata con disinvoltura da persone appartenenti ad ogni fascia di età e disponibile su ogni dispositivo che disponga di funzionalità telefoniche. Relativamente al mondo Android, l'utilità in campo professionale dell'interazione via SMS è ancora ampia. Per fare un esempio, esistono molti dispositivi per la domotica, come caldaie, sistemi antifurto, etc. che notificano lo stato dell'impianto o il verificarsi di situazioni particolari via SMS. Ecco: in un caso del genere un'app Android potrebbe rimanere in attesa di tali messaggi, ed utilizzarli interagendo con l'utente.
In questo capitolo, si affronterà proprio l'invio e la ricezione di SMS in un'app Android.
BroadcastReceiver
Delle quattro componenti che costituiscono un'applicazione Android, finora ne abbiamo viste tre: Activity, ContentProvider e Service. Qui introdurremo la quarta: il BroadcastReceiver.
Si tratta di un oggetto che si registra presso il sistema operativo, per essere allertato non appena si verifica una determinata circostanza. L'attivazione avviene mediante il classico meccanismo degli Intent, includendo altri elementi già visti come il PendingIntent e gli IntentFilters.
Un BroadcastReceiver – utile in tantissime circostanze, non solo per gli SMS – viene creato estendendo la classe omonima ed implementando il metodo onReceive. Tale metodo riceve in input due parametri: il Context per l'interazione con il sistema e un Intent. Quest'ultimo contiene tutte le informazioni riguardanti l'evento.
Affinchè funzioni, il BroadcastReceiver deve essere registrato nel sistema e ciò può essere fatto in due modi:
- via XML, nell'AndroidManifest, mediante il tag
<receiver>; - in Java, usando il metodo
registerReceiver.
In questo capitolo ci avvarremo di entrambe le modalità di registrazione.
Invio di SMS
Per prima cosa impareremo ad inviare SMS da un'app. Per fare ciò, utilizzeremo una classe di sistema denominata SmsManager, adoperandola per due operazioni significative:
- ci faremo resituire un riferimento all'SmsManager;
- invieremo per suo tramite un messaggio di testo, con il numero del destinatario ed il testo.
Da non dimenticare che è necessario dichiarare l'apposita permission:
<uses-permission android:name="android.permission.SEND_SMS"/>
Per completezza, queste due operazioni saranno integrate con l'uso di BroadcastReceiver per notificare il successo nell'invio.
String numero = "3301234567"; // di pura fantasia String testo = "Ciao, come stai?"; SmsManager smsManager = SmsManager.getDefault(); PendingIntent inviato = PendingIntent.getBroadcast(getApplicationContext(), 0, new Intent("SMS_INVIATO"), 0); PendingIntent consegnato = PendingIntent.getBroadcast(getApplicationContext(), 0, new Intent("SMS_CONSEGNATO"), 0); suInvio=new BroadcastReceiver() { @Override public void onReceive(Context arg0, Intent arg1) { if (getResultCode()==Activity.RESULT_OK) Toast.makeText(arg0, "SMS inviato correttamente", Toast.LENGTH_LONG).show(); else Toast.makeText(arg0, "Errore in invio", Toast.LENGTH_LONG).show(); } }; suConsegna=new BroadcastReceiver() { @Override public void onReceive(Context arg0, Intent arg1) { if (getResultCode()==Activity.RESULT_OK) Toast.makeText(arg0, "SMS consegnato", Toast.LENGTH_LONG).show(); else Toast.makeText(arg0, "Errore", Toast.LENGTH_LONG).show(); } }; registerReceiver(suInvio, new IntentFilter("SMS_INVIATO")); registerReceiver(suConsegna, new IntentFilter("SMS_CONSEGNATO")); smsManager.sendTextMessage(numero, null, testo, inviato, consegnato);
Il codice proposto è una modalità completa per gestire l'invio di un SMS. Il numero del destinatario ed il testo sono contenuti in due stringhe, che abbiamo chiamato rispettivamente numero e testo. Le due righe di codice essenziali sono:
SmsManager smsManager = SmsManager.getDefault(); smsManager.sendTextMessage(numero, null, testo, inviato, consegnato);
La prima riga individua l'SmsManager e la seconda si occupa dell'invio. Tutto ciò che si trova tra loro serve a gestire la notifica di invio e consegna.
Ognuna di queste due fasi viene gestita con:
- un PendingIntent che congela un Intent relativo ad azioni personalizzate (
SMS_INVIATOper l'invio eSMS_CONSEGNATOper la consegna), che sarà inviato al momento del completamento di ognuna delle due fasi; - un BroadcastReceiver che si registrerà per essere informato del lancio del PendingIntent corrispondente.
Nell'Activity sono stati definiti due membri di classe BroadcastReceiver:
private BroadcastReceiver suInvio = null; private BroadcastReceiver suConsegna = null;
La loro inizializzazione vera e propria è stata effettuata in fase di invio. Il metodo onReceive in questo caso apre un Toast di notifica.
Affinchè il BroadcastReceiver sia attivo è necessario che venga registrato. Visto che ci sono due coppie BroadcastReceiver-PendingIntent, avverranno due registrazioni, ognuna delle quali assocerà un Receiver con il corrispondente IntentFilter relativo all'azione richiesta:
registerReceiver(suInvio, new IntentFilter("SMS_INVIATO")); registerReceiver(suConsegna, new IntentFilter("SMS_CONSEGNATO"));
Infine possiamo richiedere che nel metodo onPause vengano cancellate le registrazioni dei BroadcastReceiver, nel seguente modo:
@Override protected void onPause() { super.onPause(); unregisterReceiver(suInvio); unregisterReceiver(suConsegna); }
Ricezione
Per quanto riguarda la ricezione di SMS, non aggiungeremo concetti nuovi, dal momento che utilizzeremo nuovamente la classe BroadcastReceiver.
Prima di tutto, specifichiamo le permission nel Manifest; questa volta occorrono le seguenti:
<uses-permission android:name="android.permission.RECEIVE_SMS"/> <uses-permission android:name="android.permission.READ_SMS" />
Poi creiamo una classe Java che eredita da BroadcastReceiver:
public class IncomingSMS extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Bundle extras = intent.getExtras(); if ( extras != null ) { Object[] smsExtra = (Object[]) extras.get( "pdus"); for ( int i = 0; i < smsExtra.length; ++i ) { SmsMessage sms = SmsMessage.createFromPdu((byte[])smsExtra[i]); String testo = sms.getMessageBody().toString(); String numero = sms.getOriginatingAddress(); /* Svolgimento di operazioni sul messaggio*/ } } } }
Infine registriamo presso il sistema il BroadcastReceiver, inserendo un apposito tag nel manifest:
<receiver android:name=".IncomingSMS"> <intent-filter> <action android:name="android.provider.Telephony.SMS_RECEIVED" /> </intent-filter> </receiver>
L'azione inclusa nell'IntentFilter, questa volta, non è definita da noi come nel caso dell'invio. Rimaniamo in attesa dell'evento SMS_RECEIVED definito nel package Telephony.
Per quanto riguarda più in dettaglio il codice scritto in Java, tra gli Extras dell'Intent ce n'è uno con etichetta pdus. Dobbiamo trasformare le varie PDU arrivate – in pratica gli SMS grezzi - in oggetti di classe SmsMessage. Questo permetterà anche di poterli leggere in maniera agevole con metodi ad hoc. Dopo la conversione, infatti, per ottenere il testo del messaggio è sufficiente invocare il metodo getMessageBody, mentre per il numero del mittente si ricorre a getOriginatingAddress.
54. Elenco delle chiamate
Dopo aver trattato l'invio e la ricezione degli SMS, possiamo dedicarci all'altra “metà del cielo” in fatto di telefonia: le chiamate. In questo capitolo, non affronteremo ancora l'inoltro e la gestione delle chiamate in arrivo, ma discuteremo l'utilizzo dell'elenco storico delle telefonate inoltrate, ricevute e perse.
Come viene gestito questo insieme di informazioni in Android? La discussione che segue si basa su concetti di persistenza già trattati, ed è emblematica in quanto questo tipo di gestione viene adottato anche per altre tipologie di informazioni.
Abbiamo conosciuto l'utilizzo di Sqlite come database naturale per Android e successivamente abbiamo apprezzato le particolarità dei ContentProvider come sistema per condividere dati nel sistema. Inoltre si era già detto che Android fa uso di ContentProvider per gestire l'inserimento e la fruizione di insiemi di dati come i contatti, il calendario o il MediaStore.
Anche i dati delle chiamate vengono fruiti mediante un ContentProvider, ovvero CallLog.
Come abbiamo già sperimentato, quando ci si vuole interfacciare con un ContentProvider di sistema, il principale problema è imparare a conoscerne la struttura, studiarne le classi e gli Uri disponibili. La buona notizia è che questo è praticamente l'unico problema, perchè l'interfacciamento con i ContentProvider avviene sempre alla stessa maniera.
L'Uri di riferimento – l'indirizzo cui fa capo questo sottosistema informativo – è CallLog.Call.CONTENT_URI. Quindi per recuperare un elenco delle chiamate è sufficiente eseguire questa richiesta:
Cursor crs=getContentResolver().query(CallLog.Calls.CONTENT_URI, null, null, null, null);
Il Cursor ottenuto conterrà moltissimi record, ognuno dei quali si riferisce ad una chiamata effettuata, ricevuta o persa. Gli aspetti che prenderemo in considerazione sono identificati dai seguenti campi:
- CallLog.Calls.NUMBER: il numero di telefono;
- CallLog.Calls.TYPE: il tipo di telefonata (inoltrata, ricevuta o persa). I possibili valori sono costanti: CallLog.Calls.OUTGOING_TYPE, CallLog.Calls.INCOMING_TYPE e CallLog.Calls.MISSED_TYPE;
- CallLog.Calls.DATE: informazione temporale.
Nell'esempio che vedremo, ogni record elaborato sarà mostrato in un layout a tre colonne (file: res/layout/callrow.xml):
<?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="wrap_content"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:maxLines="10" android:id="@+id/numero" /> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:id="@+id/tipo" /> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:id="@+id/dataora" /> </LinearLayout>
Per il resto, l'esempio inserisce i dati raccolti all'interno di una ListActivity. Viene utilizzato un CursorAdapter, componente già visto, preparato appositamente per agganciare i risultati di una query al layout:
public class MainActivity extends ListActivity { private CursorAdapter adapter=null; private SimpleDateFormat simple=new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Cursor crs=getContentResolver().query(CallLog.Calls.CONTENT_URI, null, null, null, null); adapter=new CursorAdapter(this,crs,0) { @Override public View newView(Context c, Cursor crs, ViewGroup vg) { View v=LayoutInflater.from(c).inflate(R.layout.callrow, null); return v; } @Override public void bindView(View v, Context ctx, Cursor crs) { TextView numero=(TextView) v.findViewById(R.id.numero); numero.setText(crs.getString(crs.getColumnIndex(CallLog.Calls.NUMBER))); TextView tipo=(TextView) v.findViewById(R.id.tipo); switch(crs.getInt(crs.getColumnIndex(CallLog.Calls.TYPE))) { case CallLog.Calls.OUTGOING_TYPE: tipo.setText("OUT"); break; case CallLog.Calls.INCOMING_TYPE: tipo.setText("IN"); break; case CallLog.Calls.MISSED_TYPE: tipo.setText("MISSED"); break; } TextView dataora=(TextView) v.findViewById(R.id.dataora); String data=simple.format(new Date(crs.getLong(crs.getColumnIndex(CallLog.Calls.DATE)))); dataora.setText(data); } }; setListAdapter(adapter); } }
Il risultato prodotto, mostrato nell'immagine seguente, è esattamente ciò che ci aspettavamo: l'elenco storico delle chiamate.
55. Chiamare ed intercettare una chiamata
Nella lezione precedente abbiamo imparato ad accedere allo storico delle chiamate. In questo capitolo vedremo come si può inoltrare una chiamata dalla propria app ed ottenere i dati delle telefonate in arrivo.
Nonostante si tratti di due casistiche differenti, le classi predisposte nel framework sono in buona parte le stesse; pertanto, i due argomenti potranno essere affrontati insieme.
Classi e strumenti per la telefonia
Sfrutteremo le classi messe a disposizione dal sottosistema Telephony del framework Android. Particolarmente utili saranno:
- TelephonyManager: ogni sottosistema Android viene utilizzato mediante un system service, al quale si accede tramite Context. Questo ci fornirà la controparte con cui dialogare dalla nostra app per interagire con la telefonia;
- PhoneStateListener: è un listener in grado di intercettare la variazione di stato del sistema telefonico. Ci avvertirà se il telefono inizia a squillare, se c'è una chiamata in corso o se attualmente è inattivo. Sarà utilizzato sia relativamente alle chiamate in uscita, che per quanto riguarda quelle in entrata, e le fasi descritte saranno utili in entrambi i casi;
- le costanti di stato: il TelephonyManager è dotato di una serie di costanti che identificano tutte le possibili situazioni. Anche le tre circostanze descritte al punto precedente corrispondono ad opportuni valori: CALL_STATE_RINGING (telefono squilla), CALL_STATE_OFFHOOK (telefonata in corso), CALL_STATE_IDLE (nessun evento in corso).
Altro elemento fondamentale, di cui si è già discusso nei capitoli precedenti, è il BroadcastReceiver. Riepilogando, si tratta di una delle quattro componenti delle applicazioni Android – oltre ad Activity, ContentProvider e Service – ed ha la caratteristica di rimanere “in attesa” finché qualche evento lo sveglia. Quando ciò si verifica, il BroadcastReceiver attiva un metodo – l'unico che normalmente il programmatore deve implementare – denominato onReceive. Nell'AndroidManifest della nostra app saremo noi a specificare quale evento riattiverà il BroadcastReceiver, mediante un apposito IntentFilter che sarà connotato da una specifica action.
Le permission necessarie
Come abbiamo già fatto diverse volte nei capitoli precedenti, dovremo anche in questo caso dichiarare le necessarie permission ogni volta che la nostra app effettua meccanismi di comunicazione particolari. Anche la telefonia ricade in questa casistica, pertanto alcune permission saranno necessarie.
Negli esempi successivi se ne renderanno utili due in particolare:
- una necessaria per effettuare chiamate dall'app:
<uses-permission android:name="android.permission.CALL_PHONE"/> - un'altra per poter leggere lo stato del sistema telefonico, ed ottenere i dettagli della telefonata (in partenza o in arrivo):
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
Effettuare una chiamata
Inizieremo con l'imparare ad effettuare telefonate direttamente dalla nostra app. Utilizziamo un layout molto elementare: totalmente vuoto e con un solo pulsante al centro. Il funzionamento che vogliamo applicare consiste nell'inoltro di una chiamata ad un numero prestabilito, che sarà associato al click sul pulsante. Il recapito telefonico da contattare sarà definito in una stringa.
Il file del layout (res/layout/activity_main.xml) è il seguente:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Invia" android:onClick="invio"/> </RelativeLayout>
All'interno dell'Activity predisporremo due metodi – onCreate ed il metodo denominato invio per l'inoltro della telefonata – ed una classe interna, estensione di PhoneStateListener per il controllo della chiamata.
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TelephonyManager telephonyManager = (TelephonyManager) this.getSystemService(Context.TELEPHONY_SERVICE); telephonyManager.listen(new Listener(),PhoneStateListener.LISTEN_CALL_STATE); } public void invio(View v) { Intent callIntent = new Intent(Intent.ACTION_CALL); callIntent.setData(Uri.parse("tel:0611223344")); startActivity(callIntent); } private class Listener extends PhoneStateListener { @Override public void onCallStateChanged(int state, String incomingNumber) { switch (state) { case TelephonyManager.CALL_STATE_OFFHOOK: Log.i("CHIAMATA IN USCITA", "TERMINALE IMPEGNATO"); break; case TelephonyManager.CALL_STATE_IDLE: Log.i("CHIAMATA IN USCITA", "IDLE"); } } } }
La vera e propria chiamata è effettuata dall'Intent. Come al solito questo potente meccanismo di sistema può attivare una funzionalità – in questo caso la chiamata – semplicemente richiedendo un'azione (Intent.ACTION_CALL) e inserendo nei dati a corredo il numero da contattare (nell'esempio precedente, tel:0611223344).
Si noti che l'invocazione al TelephonyManager nel metodo onCreate non è quella che consente di effettuare la chiamata, ma serve solo per la registrazione del listener. Quest'ultimo ci è d'aiuto per capire il momento esatto in cui il nostro terminale diventa occupato (cioè quando viene registrato un evento di CALL_STATE_OFFHOOK). Quando si ritornerà al successivo stato CALL_STATE_IDLE, non ci sarà più alcuna chiamata in corso. La stringa incomingNumber in questi casi sarà sempre vuota.
In questo esempio, le permission indicate in precedenza servono entrambe: una perché si effettua la chiamata, l'altra per potere consultare lo stato del telefono.
Intercettare chiamate in arrivo
Vediamo ora come intercettare una chiamata in arrivo, ed ottenerne il recapito telefonico di provenienza utilizzando solo un BroadcastReceiver.
Per fare ciò creeremo una classe denominata IncomingReceiver, che sarà registrata nel file AndroidManifest.xml:
<receiver android:name=".IncomingReceiver"> <intent-filter> <action android:name="android.intent.action.PHONE_STATE" /> </intent-filter> </receiver>
L'implementazione è la seguente:
public class IncomingReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { TelephonyManager manager = (TelephonyManager) context .getSystemService(Context.TELEPHONY_SERVICE); Listener PhoneListener = new Listener(); manager.listen(PhoneListener, PhoneStateListener.LISTEN_CALL_STATE); } private class Listener extends PhoneStateListener { public void onCallStateChanged(int state, String incomingNumber) { switch(state) { case TelephonyManager.CALL_STATE_RINGING: Log.d("CHIAMATA", "SQUILLO DA "+incomingNumber); break; case TelephonyManager.CALL_STATE_OFFHOOK: Log.d("CHIAMATA", "RISPOSTO"); break; case TelephonyManager.CALL_STATE_IDLE: Log.d("CHIAMATA", "IDLE"); } } } }
Come si vede è necessario estendere la classe BroadcastReceiver ed implementi il metodo onReceive che sarà eseguito al momento della chiamata in ingresso.
Anche in questo caso, il TelephonyManager è richiesto per registrare il listener. In questo caso esso servirà a recuperare i dettagli relativi alla chiamata in entrata, tramite la lettura degli stati del sistema telefonico.
All'interno del listener, la stringa incomingNumber non sarà vuota, bensì valorizzata con il numero che ci sta chiamando.
Quando il nostro telefono inizia a squillare, il nostro terminale entra nello stato CALL_STATE_RINGING. Se rifiuteremo la chiamata passeremo ancora a CALL_STATE_IDLE, altrimenti quando la conversazione verrà accettata, lo stato rilevato sarà CALL_STATE_OFFHOOK.
Per quest'ultimo esempio, la sola permission richiesta è android.permission.READ_PHONE_STATE.
56. Gestire i Contatti
La rubrica di Android è la sorgente principale di tutti i recapiti telefonici da poter contattare. Considerando che si tratta di un insieme di dati strutturati, disponibili a livello di sistema, non stupisce il fatto che essa sia stata implementata nei termini di un ContentProvider.
I contatti, ai quali normalmente ci si riferisce con il termine rubrica, rappresentano uno dei principali ContentProvider disponibili su Android. La sua struttura può sembrare piuttosto confusa, e per questo occorre innanzitutto fare un po' d'ordine.
Sul funzionamento dei ContentProvider non c'è nulla di nuovo da aggiungere rispetto a quanto visto nelle precedenti lezioni. In questa sede ricordiamo brevemente che un ContentProvider permette di accedere a dati condivisi tramite le classiche operazioni di creazione-lettura-modifica-cancellazione mediante i metodi insert, query, update e delete. A differenza di quanto accade con i database relazionali, essi non richiedono di accedere direttamente a tabelle, bensì di individuare la risorsa oggetto dell'operazione mediante un riferimento univoco di classe Uri.
Proprio dall'organizzazione delle classi e dal reperimento degli URI inizia questo capitolo. L'organizzazione dei contatti viene distribuita su tre livelli:
- Contact;
- RawContact;
- Data.
Prima di spiegare le differenze tra i tre, si tenga a mente che per utilizzare un dispositivo Android, l'utente deve associare ad esso uno o più account Google. Il sistema operativo deve quindi prendersi carico di registrare i dati dei contatti ed accoppiarli con l'account che li utilizza.
Mentre i Contact rappresentano singole persone indipendentemente dalla quantità di informazioni che di ognuna di esse si posseggono (email, telefono di casa, telefono dell'ufficio, cellulare, etc.), i RawContact includono l'associazione tra un contatto e un account. Inoltre per ogni persona esiste un solo Contact e per ogni Contact possono esistere più RawContact.
I Data invece rappresentano i singoli dettagli che formano ogni RawContact.
Le classi per la gestione dei contatti che useremo saranno pertanto tre:
- ContactsContract.Contacts, i singoli contatti;
- ContactsContract.RawContacts, i “raw contact”;
- ContactsContract.Data, i dati di dettaglio.
Le permission
Consueto obbligo è quello di dichiarare le permission adeguate, dipendentemente dalle operazioni che si vogliono svolgere:
- per la sola lettura dei contatti, utilizzeremo la seguente sintassi:
<uses-permission android:name="android.permission.READ_CONTACTS"/> - se vogliamo anche inserire e modificare i dati, includeremo quanto segue:
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
Lettura dei contatti
L'Uri per l'accesso all'insieme di dati (Contact, RawContact o Data) è contenuto in una costante stringa di nome CONTENT_URI, della classe ContactsContract.Contacts.
L'elenco dei contatti disponibili può essere letto tramite un Cursor con le seguenti righe di codice:
String[] projection={Contacts._ID,Contacts.DISPLAY_NAME}; Cursor crs=getContentResolver() .query(ContactsContract.Contacts.CONTENT_URI, projection, null, null, null);
In questo esempio, i dati inseriti nella proiezione sono solo due dei tanti disponibili, ma sono quelli più utili: l'ID del contatto, indispensabile per accedere a tutti gli altri dati, ed il nome del contatto.
Possiamo visualizzarli, come fatto in altre occasioni, combinando l'utilizzo delle classi CursorAdapter e ListView.
Inserimento di dati
Immaginiamo di voler inserire alcuni dati per un nuovo RawContact. Non dovremo fare altro che creare il nuovo contatto, ottenerne l'ID ed utilizzarlo per inserire singoli Data:
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); operations.add(ContentProviderOperation .newInsert(ContactsContract.RawContacts.CONTENT_URI) .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null) .build()); operations.add(ContentProviderOperation .newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, "Guido Rossi") .build()); operations.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, "0611223344") .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_HOME) .build()); operations.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Email.DATA, "guido@rossi.it") .withValue(ContactsContract.CommonDataKinds.Email.TYPE, ContactsContract.CommonDataKinds.Email.TYPE_WORK) .build()); try { cr.applyBatch(ContactsContract.AUTHORITY, operations); } catch (RemoteException e) { // gestione delle eccezioni } catch (OperationApplicationException e) { // gestione delle eccezioni }
Quando operazioni di questo tipo vengono effettuate come un'aggregazione di diversi inserimenti, può essere utile usare le ContentProviderOperation. I loro principali fattori di utilità sono l'uso di un builder per comporre in modo agevole la richiesta, e l'esecuzione dell'operazione in batch.
Nelle righe di codice precedenti per prima cosa viene creato un ArrayList di ContentProviderOperation. Poi vi si aggiungono una alla volta una serie di operazioni, utilizzando questi comandi:
- newInsert , per creare il nuovo inserimento;
- withValue , per specificare i parametri da inserire;
- build , per costruire il comando completo.
L'esecuzione vera e propria avverrà richiedendo al ContentResolver di eseguire il batch con il metodo applyBatch.
Publishing
57. AndroidManifest.xml e le capabilities
Abbiamo già discusso ed utilizzato il file AndroidManifest.xml durante questa guida. È chiaro che programmare applicazioni Android senza saperlo configurare è impossibile. L'uso che ne abbiamo fatto finora ha riguardato per lo più la definizione delle componenti da inserire nell'applicazione. Abbiamo visto che se si vogliono utilizzare le quattro componenti fondamentali – Activity, ContentProvider, Service, BroadcastReceiver – è necessario, oltre ad estendere l'opportuna classe Java, inserire un adeguato elemento XML nel manifest, in particolare all'interno del nodo <application>:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="..." android:versionCode="1" android:versionName="1.0" > . . . . . . <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name="..." android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <receiver android:name="..."> <intent-filter> <action android:name="..." /> </intent-filter> </receiver> <provider android:name="..." android:authorities="..."/> <service android:name="..."/> </application> </manifest>
In questo capitolo ci interesseranno altri particolari relativi al file manifest, ma che saranno inseriti esternamente al nodo <application>. Ci occuperemo di tutti quegli aspetti che descrivono i requisiti che un dispositivo deve possedere affinchè la nostra applicazione possa esservi installata.
Si tratta di un preambolo indispensabile per pensare alla pubblicazione della nostra app.
L'elemento <uses-sdk> è molto importante per specificare il range di versioni Android in cui l'applicazione può funzionare. La finalità principale di questo nodo è garantire la retrocompatibilità dell'applicazione. Tramite questo elemento è possibile specificare l'API level, ovvero la versione delle API utilizzate, espressa con un numero intero. Non è necessario conoscere a memoria tutte (anche perchè l'elenco completo è sempre disponibile su Internet); tuttavia bisogna tenere presente che le “pietre miliari” della storia di Android non sono molte. I principali API level sono i seguenti:
- 7 e 8: corrispondono ad Android 2.1 e 2.2 e costituiscono la soglia minima che ormai ha senso supportare nelle proprie applicazioni;
- 11: Android 3 (Honeycomb), che ha rappresentato una piccola rivoluzione, in cui sono state integrate alcune caratteristiche basilari nel framework (per esempio Fragments e Loaders);
- 14: inizia Android 4 (Ice Cream Sandwich), l'alba della versione attuale del sistema operativo.
Gli attributi di <uses-sdk> sono:
- minSdkVersion: indica la minima versione supportata. Può andare bene, ormai, che sia 7 o 8 ma se è inferiore alla 11 sarà importante acquisire confidenza con la libreria di supporto per integrare, nelle versioni più datate, le novità di Honeycomb. È importante che questo attributo sia esplicitamente dichiarato perchè, se non lo fosse, la minima versione supportata sarà l'API level 1 (e ciò, oggi come oggi, ha poco senso);
- targetSdkVersion: rappresenta la versione principale per la quale è stata pensata l'applicazione;
- maxSdkVersion: molto poco utilizzato, anche perchè sconsigliato dalla stessa documentazione, questo attributo rappresenta il massimo API level supportato. Dal momento che lo scopo di
<uses-sdk>è la gestione della retrocompatibilità, maxSdkVersion rischia di impedire l'installazione dell'applicazione su dispositivi più recenti.
Il tag <permission> è stato già incontrato parecchio nel corso di questa guida. Necessario ogni volta che la nostra applicazione deve avviare comunicazioni o accessi particolari, richiede almeno che la definizione dell'attributo android:name, che specifica esattamente il tipo di permission richiesta. Finora l'abbiamo incontrato in:
- accesso alla Rete:
android.permission.INTERNET; - localizzazione:
android.permission.ACCESS_FINE_LOCATIONeandroid.permission.ACCESS_COARSE_LOCATION; - storage esterno:
android.permission.WRITE_EXTERNAL_STORAGE; - comunicazioni telefoniche:
android.permission.CALL_PHONEeandroid.permission.READ_PHONE_STATE; - Content Providers: vari casi (contatti, calendario, chiamate, etc.).
Infine, <uses-feature> è la risposta alla frammentazione hardware e software del sistema Android. Permette di specificare di quali caratteristiche hardware o software l'applicazione ha bisogno per funzionare.
Gli attributi di cui dispone sono:
android:name: una stringa che definisce quale dotazione del dispositivo è necessaria affinchè l'applicazione funzioni correttamente. A livello hardware, si potrebbe avere bisogno di verificare se l'equipaggiamento elettronico del device comprende tecnologie come Bluetooth o NFC, nonchè dispositivi come la videocamera o lo schermo multitouch;android:required: è un valore booleano. Se impostato atrueindica che la caratteristica è assolutamente obbligatoria per il funzionamento dell'app; altrimenti indica che la feature è fortemente consigliata ma non obbligatoria;android:glEsVersion: indica la versione delle librerie OpenGL ES richiesta dall'app.
58. Iscrizione a Google e invio dell'app
Oltre ad Android, Google consente anche la pubblicazione delle app sul suo “mercato” ufficiale, dove possiamo trovare software sia gratuiti che a pagamento: Google Play.
L'obiettivo di molti programmatori che si avventurano nello studio di una guida come questa è la pubblicazione di una propria app sul market. In questa lezione parleremo di quello ufficiale, fornito da Google; nei capitoli successivi discuteremo delle principali alternative.
Prima di procedere, riflettiamo su quali possano essere i vantaggi di pubblicare sul market. Probabilmente molti penseranno subito al profitto economico, al poter vendere la propria applicazione. Molti pogrammatori alle prime armi sognano di ideare ed implementare un'applicazione che costi quasi nulla, e risuscire a ricavere milioni dalle vendite. Ma sappiamo bene che ciò è molto più difficile di quanto possa sembrare. Indipendentemente dalla possibilità di realizzare o meno questo “sogno”, è bene sottolineare che ci sono altri vantaggi nel possedere (ed utilizzare) un account sviluppatore presso Google Play. Ad esempio, lo si potrebbe popolare di applicazioni gratuite per dimostrare le proprie abilità, e presentarsi ad un colloquio di lavoro per programmatori con un portfolio ricco e valido. Per non parlare della gratificazione personale derivante o da guadagni per vie diverse dalla vendita, con altre forme di monetizzazione.
Registrarsi
Per iniziare la propria vita di sviluppatore ufficiale Android è necessario effettuare il log-in con un account Google, ad esempio quello usato per Gmail, sull'apposita pagina di accesso.
Fatto ciò, è sufficiente seguire i passi seguenti:
- sottoscrivere il contratto di pubblicazione sull'Android Market. Questo specifica per lo più cosa si può e non si può fare, le regole da rispettare, i contenuti vietati e così via;
- versare la quota di 25 dollari mediante il sistema di pagamento Google Wallet (operazione che dovrà essere effettuata soltanto una volta).
Preparazione del pacchetto di pubblicazione
Quando si dispone di un account Google per sviluppatori, si deve preparare il pacchetto di distribuzione dell'app, in formato .apk, che dev'essere firmato mediante un nostro certificato. Per fare ciò, su Eclipse dovremo fare click con il tasto destro del mouse sul nome del progetto e selezionare la voce Android Tools –> Export Signed Application Package.. Seguirà una procedura che ci chiederà di predisporre un certificato o, se già se ne possiede uno, di specificare il keystore, ossia la collocazione dei propri certificati sul sistema. Con qualche selezione ed alcuni dati inseriti il pacchetto sarà pronto per la pubblicazione.
La pubblicazione
La pubblicazione vera e propria dell'app avverrà dal proprio pannello da sviluppatore dopo aver eseguito il login. Oltre al pacchetto .apk firmato, saranno necessari due screenshot dell'applicazione, un'icona dell'app ed una descrizione delle finalità e dei funzionamenti, possibilmente in più lingue. All'occasione della pubblicazione verranno fornite indicazioni circa le dimensioni ed i dettagli di ognuno di questi elementi.
Se si desidera pubblicare applicazioni a pagamento, è necessario registrare anche un account commerciale presso Google Checkout. Dopo aver fornito le informazioni richieste, anche questo aspetto sarà completato e si potranno pertanto vendere le proprie creazioni.
59. Market alternativi
Il market di Google è la piazza principale attraverso cui diffondere le proprie app. Ovviamente, non è detto che un'applicazione Android debba necessariamente essere distribuita mediante un “mercato”. Potrebbe essere venduta o scaricata direttamente, da un proprio sito o mediante altri servizi. Indubbiamente, scegliere di distribuirla mediante un canale ufficiale sottopone ad alcuni obblighi e limitazioni, ma d'altro canto offre vantaggi non indifferenti, come la maggiore reperibilità da parte degli utenti, catalogazione più precisa dei contenuti e strumenti di statistica e analisi.
Oltre all'Android Market di Google esistono mercati alternativi, realtà spesso istituite e gestite dai “grandi” della Rete. Ne vediamo alcuni:
- Amazon Appstore: già trattato in un precedente articolo su HTML.it, rappresenta un mercato in grande ascesa, forte di tutto il sostegno che può dare un colosso come Amazon, tra i pionieri dell'e-commerce. Permette agli sviluppatori di registrarsi gratuitamente e caricare le proprie app da subito, anche in vendita. Disponibile in circa 200 nazioni, offre diverse caratteristiche interesanti tra cui la possibilità di distribuire applicazioni non native, in HTML5, senza il bisogno di pacchettizzarle, e delle API in grado di integrare strumenti di monetizzazione nell'applicazione. Altro aspetto interessante, testimonianza di quanto valga il supporto di Amazon, è la possibilità di sfruttare la piattaforma di analisi statistica Amazon Analytics Service;
- SlideME Market può vantarsi di essere il primo market Android. Nato nel 2008, qualche mese prima di Android Market (oggi Google Play), vuole essere il mercato universale, soprattutto a tutela dei produttori di dispositivi Android che non fanno capo a Google. SAM, il client per questo mercato, è già presente su dispositivi di molti produttori ed in caso di necessità può essere scaricato dalla homepage del progetto. Sul sito si presenta un po' come il mercato della libertà: tante nazioni, tanti distributori di dispositivi supportati, zero costi per lo sviluppatore, libertà di monetizzazione. Si può caricare l'app, descriverla a tutto tondo con screenshot, video e testi, oltre a scambiare pareri con gli utenti;
- Samsung ha proposto SamsungApps (anch'esso scaricabile da internet), un market specializzato in applicazioni per i suoi dispositivi, e non limitato al supporto di Android ma anche di Windows, Bada e Symbian. I punti di forza maggiormente pubblicizzati sul sito sono la sicurezza, la qualità delle applicazioni e l'alto grado di compatibilità delle app con i dispositivi Samsung.
Il mondo dei market delle app è in continua espansione, come è lecito aspettarsi per un sistema operativo così dilagante. Si pensi ad AppsBrain, AppsFire o GetJar per fare qualche altro nome. Ma molti ancora ne nasceranno. Quando lo sviluppatore deve decidere quale sia il market giusto cui affidare la propria app, i fattori che dovrebbe valutare sono tanti, tra cui:
- limitazioni e clausole per l'ammissione dell'app nel market;
- costi da sostenere per lo sviluppatore;
- mezzi di pagamento offerti ai clienti;
- possibilità di monetizzazione;
- disponibilità di client per l'installazione delle app direttamente su dispositivo;
- diffusione del market.
60. Modelli di Monetizzazione
Avere un account sviluppatore Google permette di caricare un'app e renderla disponibile tramite download dal market. Inoltre, corredando il tutto con un account Google Checkout, è possibile diventare un “mercante” di app, rilasciandole a titolo non gratuito.
Il “come” trarre guadagno da un'app resta una problematica attualmente piuttosto discussa, e venderla non è l'unica via di monetizzazione esistente. Questo capitolo si propone di illustrare per sommi capi le possibilità più comuni per poter far fruttare una propria applicazione, trasformando in moneta sonante gli sforzi di sviluppo, progettazione e sperimentazione.
Ecco le forme più comuni di “monetizzazione”:
- Vendere un'app: quando si parla di “vendere” un'app si pensa subito al distribuirla in forma non gratuita. Eppure le statistiche dicono che un modo molto comune e particolarmente proficuo di vendere app è quello di offrirne una versione base gratuitamente, magari con limitazioni di utilizzo temporali o di funzionalità e proporne versioni più complete in vendita. Ciò non solo risulta una delle principali forme di guadagno, ma permettendo anche all'utente di pagare per un'app che ha suscitato realmente il suo interesse, almeno nella versione gratuita;
- una riedizione nel mondo mobile di un modello tipico dei siti Internet è la pubblicità in-app. Significa che è possibile inserire nell'interfaccia utente della propria applicazione dei banner pubblicitari, occupanti uno spazio ridotto. È possibile includerli nella propria app rivolgendosi a servizi come AdMob (uno dei più utilizzati in assoluto) o ad AdSense di Google, che si occupano di raggruppare gli inserzionisti in base alle catogorie di interesse e proporre i più adeguati per la propria app. Affinchè questo meccanismo abbia successo è necessario innanzitutto che l'app sia molto diffusa, quindi gratuita, utile e molto usabile;
- altro meccanismo che si può prendere in considerazione è la vendita di prodotti in-app. Si tratta di uno strumento molto diffuso e consiste nella possibilità di far acquistare all'utente prodotti o servizi per lo più a carattere digitale, offrendoli durante il normale utilizzo dell'app.
Reference
61. Reference: User Controls
Questo capitolo offre una panoramica dei più comuni widget che possono essere utilizzati nelle interfacce Android. La finalità non è quella di fornire una documentazione dettagliata, piuttosto una forma di glossario che a colpo d'occhio permetta di individuare un controllo da utilizzare e di averne a disposizione le minime proprietà da configurare per un corretto funzionamento. A tale scopo, i controlli sono stati raggruppati per categorie, e di ognuno verranno illustrati gli attributi XML ed i metodi Java utili nell'uso pratico più comune.
Tra gli attributi XML, non verranno mai citati layout_width, layout_height e id, in quanto già affrontati e di utilizzo comune a tutte le View del framework Android.
Il testo
I primi controlli con cui solitamente si fa conoscenza sono i classici “input” per form. Essi permettono all'Activity di svolgere uno dei principali compiti per cui essa è utilizzata: interagire con l'utente e gestire l'immissione dati. Tra questi, la gestione del testo assume una posizione di assoluto rilievo.
TextView
Nella sua forma base rappresenta una label in grado di mostrare stringhe statiche. È il modo più comune per visualizzare contenuti testuali nell'interfaccia utente.
La configurazione avviene in buona parte (ma non unicamente) in XML. Ecco i principali attributi impiegati:
| Attributo | Descrizione |
|---|---|
text | Rappresenta la stringa da mostrare |
textColor | Rappresenta il colore del testo |
textSize | Rappresenta la dimensione del testo |
lines | Rappresenta il numero esatto di righe che deve contenere la TextView |
maxLines | Rappresenta il numero massimo di righe che può contenere la TextView |
minLines | Rappresenta il numero minimo di righe che deve contenere la TextView |
textIsSelectable | Specifica se il testo può essere selezionabile o meno |
text | Rappresenta |
text | Rappresenta |
text | Rappresenta |
EditText
È un “discendente” di TextView e ne costituisce la sua versione modificabile tramite input dell'utente. Tutte le proprietà viste per laTextView sono ugualmente valide anche per EditText. Vale la pena sottolineare che di uso molto comune è l'attributo inputType che indica il formato dei dati inseriti dall'utente nel campo specifico. Può assumere diversi valori indicanti i tipi di input più comuni, tra cui text, number, phone, date, datetime, password.
AutoCompleteTextView
Viene usato come casella di input (come EditText, da cui eredita gran parte delle proprietà), con la caratteristica che offre una lista di possibili valori per l'inserimento. Questi, mostrati in un menu a tendina man mano che l'utente digita caratteri, vengono selezionati all'interno di una struttura dati presentata da un Adapter.
Per usare AutoCompleteTextView, è fondamentale:
- passare un Adapter opportunamente inizializzato, mediante codice dinamico Java, tramite il metodo
setAdapter(è perfetto allo scopo un ArrayAdapter<String>); - eventualmente impostare alcuni parametri mediante attributi XML, come
dropDownHeightedropDownWidth, che indicano rispettivamente altezza e larghezza del menu a discesa, ecompletionThreshold, un intero che rappresenta il numero minimo di caratteri che l'utente deve inserire prima che i suggerimenti vengano offerti.
MultiAutoCompleteTextView
È una classe derivata da AutoCompleteTextView. Permette di ricevere suggerimenti riferiti non all'intero testo ma a singole sottostringhe. Oltre ad un Adapter impostato mediante setAdapter, ha bisogno di un apposito Tokenizer che implementi l'interfaccia MultiAutoCompleteTextView.Tokenizer.
CheckedTextView
È una TextView che implementa l'interfaccia Checkable. È utile all'interno delle ListView in cui il ChoiceMode è stato impostato ad un valore diverso di CHOICE_MODE_NONE.
Accorpa in sé le caratteristiche di una TextView e di una CheckBox. Infatti, con un controllo di questo tipo si possono impostare tutte le caratteristiche del testo (contenuto, stile e proprietà comportamentali) ma si può anche gestire la checkbox inclusa.
Attributi XML molto utili in proposito sono:
| Attributo | Descrizione |
|---|---|
checkMark | Indica il Drawable usato per raffigurare il check su cui cliccare. Per valorizzarlo si possono usare anche stili della piattaforma come ?android:attr/listChoiceIndicatorMultiple |
checked | Attributo booleano che indica il valore di inizializzazione della checkbox |
L'immagine di seguito riporta un EditText ed un AutoCompleteTextView utilizzati all'interno di in un layout:
Pulsanti
Sono i controlli tipici di un form, in quanto permettono con un click di attivare un'azione da parte dell'Activity, e molto spesso avviano l'elaborazione dei dati inseriti dall'utente.
Button
Il più classico e tipico dei pulsante, al quale può essere associata un'azione in risposta al click. Da questa classe ereditano tutti i tipi di widget che rappresentano pulsanti su Android, e per quanto possa sembrare strano essa eredita dalla classe TextView. Per questo motivo, da essa deriva alcune proprietà, come quelle relative al testo.
Gli attributi XML più utili sono i seguenti:
| Attributo | Descrizione |
|---|---|
text | Rappresenta il testo che appare sul pulsante |
onClick | È una stringa che rappresenta il nome del metodo dell'Activity che gestirà il click. Tale metodo deve obbligatoriamente restitire void ed accettare in input una View, identificabile con il pulsante stesso che ha ricevuto il click |
ImageButton
Identico al Button nell'impiego e nella configurazione, ma con l'aggiuntiva possibilità di utilizzo di un'immagine al posto del testo. Includendo un ImageButton in un layout, piuttosto che l'attributo text sarà importante configurarne uno chiamato src, che riceve come valore la risorsa drawable da raffigurare sul pulsante.
ToggleButton
È un pulsante da utilizzare come interruttore. Tramite questo widget, si può specificare uno di due stati possibili, ed il click esercitato su di esso comporta la transizione da uno stato all'altro.
Al pari di un Button supporta l'attributo onClick per indicare il metodo dell'Activity che gestirà le operazioni corrispondenti alla variazione di stato. Questo controllo permette di indicare due testi alternativi da usare come etichette del pulsante. Si tratta delle proprietà textOn e textOff. Al cambiamento di stato, l'etichetta passerà da un valore all'altro in automatico.
RadioButton
Quando l'utente deve scegliere tra alcune opzioni prestabilite, è piuttosto comune presentare nell'interfaccia un gruppo di RadioButton. La caratteristica principale dell'intero gruppo è la mutua esclusività tra le opzioni: in altre parole, ne può essere selezionata una sola.
Ogni RadioButton supporta le proprietà tipiche di un Button: text per impostare l'etichetta, onClick per definire il metodo di gestione di un click. È importante che i RadioButton vengano inclusi in un RadioGroup, un oggetto che maschera un LinearLayout arricchendolo dei comportamenti utili alla gestione dei RadioButton, tra i quali:
orientation, che indica (al pari dei LinearLayout) se la serie dei controlli contenuti deve svilupparsi in orizzontale o in verticale;checkedButton, che rappresenta il RadioButton impostato a checked come opzione di default dell'intero gruppo. È valorizzato con l'id del pulsante prescelto.
CheckBox
È il controllo “booleano” per eccellenza. Il click su di esso generalmente appone o rimuove un segno di “spunta”. L'uso di questo widget consiste nell'impostare ad esso un valore di default, e nel leggerne lo stato.
Nel codice Java questo può essere fatto con i metodi setChecked(boolean) e isChecked() che, rispettivamente, impostano il valore del controllo e ne leggono lo stato restituendolo in una variabile booleana.
AdapterView e Adapter
Uno degli argomenti centrali delle interfacce utente in Android è la comprensione del pattern Adapter e del suo utilizzo mediante particolari View, appartenenti alla famiglia delle AdapterView. Tutti i controlli di seguito elencati lavorano a contatto con unAdapter, pertanto tra i loro metodi più utilizzati in assoluto ci sono quelli relativi a questo aspetto: setAdapter per impostare l'Adapter da utilizzare, e getAdapter per recuperarne il riferimento.
ListView
La ListView rappresenta la tipica visualizzazione di un elenco di valori. Qualora questi siano troppi per la dimensione del controllo, viene attivato in automatico uno scroll verticale.
È possibile dotarla di una header, da mostrare in testa ai valori, e di un footer, da accodare alla lista. Questi elementi possono essere impostati e rimossi, in Java, mediante i metodi addHeaderView e removeHeaderView ed i corrispondenti addFooterView e removeFooterView.
Altro elemento importante da un punto di vista grafico è il divisore, un'immagine o un colore che separa gli elementi della lista in maniera personalizzata. Il divisore può essere impostato sia dinamicamente, mediante il metodo setDivider(Drawable), che in XML con l'attributo andorid:divider. Inoltre si può impostare lo spessore del divisore con setDividerHeight(int) e android:dividerHeight.
Infine, è possibile definire in XML l'attributo android:entries che imposta un'array di risorse con cui verrà popolata la lista. Ciò può tornare particolarmente utile nei casi in cui la lista dei valori sia preimpostata.
Infine la ListView permette di definire la modalità di selezione degli elementi – che può essere singola, multipla e nulla – attraverso la proprietà choiceMode, disponibile sia dinamicamente con metodi Java che staticamente come attributo XML.
GridView
Una GridView viene utilizzata per rappresentare griglie. Il suo funzionamento di base si avvicina molto a quello della ListView. Potremmo sommariamente dire che svolge lo stesso lavoro su “più colonne”. Per questo motivo, gli attributi XML di maggiore interesse sono:
| Attributo | Descrizione |
|---|---|
numColumns | Definisce il numero di colonne che devono comporre la griglia |
columnWidth | Imposta la larghezza delle colonne |
horizontalSpacing | Definisce la spaziatura di default tra le colonne |
verticalSpacing | Definisce la spaziatura di default tra le righe |
Spinner
È il “menu a tendina” delle interfacce Android. Sul suo funzionamento di base non c'è molto da spiegare in quanto ha molto in comune con la ListView. Per utilizzare uno Spinner è innanzitutto importante popolarlo. Lo si può fare aggregandovi un Adapter esterno o mediante un array di risorse con l'attributo entries.
La gestione della selezione di un elemento può essere svolta impostando un apposito listener con il metodo setOnItemSelectedListener, implementando altresì l'override del metodo onItemSelected per specificare il codice da eseguire.
Informazioni sulla posizione o il valore dell'elemento selezionato possono essere recuperate, in Java, con i metodi getSelectedItem(), getSelectedItemId() e getSelectedItemPosition().
ExpandableListView
È un AdapterView che mostra un elenco di risultati suddivisi in gruppi. Tutta la gestione si basa sulla possibilità di creare un elenco di gruppi, facendo sì che, cliccando su una singola voce, si apra una lista di elementi attinenti al gruppo scelto.
Risulta un po' più complicata la gestione dell'Adapter, soprattutto se si decide di crearne uno personalizzato che estende BaseExpandableListView. In questo caso, i metodi getGroup, getGroupCount, getGroupId, getGroupView serviranno a gestire i gruppi, mentre i metodi getChild, getChildrenCount, getChildId e getChildView svolgeranno le corrispondenti operazioni relative agli elenchi di elementi attinenti ai gruppi.
Misurare il tempo
Il framework di Android include anche diversi controlli per la gestione delle date e degli orari, sia come input utente che semplicemente per la visualizzazione.
DatePicker e TimePicker
I picker sono le tipologie di controlli più comunemente utilizzati nelle interfacce utente per impostare data e ora. Il loro scopo è quello di permettere l'inserimento di questi dati da parte dell'utente in un formato corretto anche in relazione al proprio fuso orario. Ciò è basilare affinché i dati vengano interpretati correttamente.
AnalogClock e DigitalClock
Sono degli orologi veri e propri. Mostrano l'ora attuale del sistema, il primo in forma analogica –con le lancette – il secondo in forma digitale, quindi riportando come testo l'informazione (ad esempio “8:23:15 PM”).
Per utilizzarli è sufficiente collocarli nel layout e, totalmente in autonomia, continueranno a visualizzare l'ora aggiornata del sistema.
È importante sottolineare che non possono essere usati per inserire informazioni data/ora; per questo scopo, come già detto, esistono appositamente i picker.
Chronometer
È un cronometro che permette di misurare il trascorrere del tempo. Una volta collocato nel layout, va gestito mediante codice Java, specialmente con i due metodi start e stop che, rispettivamente, avviano e fermano il cronometro.
Altri aspetti importanti del cronometro sono il tempo di partenza, che può essere gestita con i metodi setBase e getBase, ed il formato in cui l'informazione temporale viene espressa, con setFormat e getFormat. Quest'ultimo aspetto può essere definito staticamente in XML mediante l'attributo android:format.
Le "barre"
Le varie barre che esistono in Android ricalcano gli analoghi indicatori che siamo abituati ad utilizzare o vedere all'opera nel web e nelle applicazioni desktop. Lo scopo è il medesimo e le varie tipologie, qui di seguito indicate, ne mostrano le diverse incarnazioni.
ProgressBar
La ProgressBar è la tipica barra che indica l'avanzamento di un'operazione in corso di svolgimento. Assume solitamente due forme: lo “spinner” (da non confondere con il controllo AdapterView) rappresentato da una forma circolare che gira, e la barra orizzontale vera e propria. La differenza tra le due visualizzazioni non è solo grafica, ma consente di adattarsi alla possibilità di quantificare il task in esecuzione: lo spinner è più adatto a lavori indeterminati (download di file dalla rete, per esempio), mentre la barra si può usare per qualunque lavoro in cui il tempo o la mole di lavoro residuo può essere quantificata.
Gli attributi XML più comuni nella definizione della ProgressBar sono:
| Attributo | Descrizione |
|---|---|
progress | Indica il livello di completamento cui si è arrivati, adatto alla forma “a barra” |
max | Indica il massimo valore della barra, indica il completamento del task |
indeterminate | Valore booleano che stabilisce se la barra deve includere l'indicazione del livello di completamento |
style | Indica la forma che deve avere la barra. Non definendo lo stile, la ProgressBar sarà uno spinner; indicando uno stile (ad esempio uno stile orizzontale, specificando il valore @android:style/Widget.ProgressBar.Horizontal) sarà mostrata una barra vera e propria |
Per indicare le precedenti proprietà dinamicamente in Java, esistono opportuni membri di classe e metodi. In particolare, molto comune è utilizzare il metodo setProgress, mediante il quale sarà possibile aggiornare il valore attuale della barra, ricalcando l'avanzamento del task in corso.
SeekBar
È una classe derivata dalla ProgressBar, con la differenza che ne è una versione modificabile. In questo widget, infatti, l'utente può cambiare il livello raggiunto trascinando un'immagine che funge da indicatore. Un esempio tipico è il lettore audio, in cui si potrebbe spostare indietro l'indicatore della SeekBar per riascoltare una porzione di audio precedente.
Tutti le sue caratteristiche principali sono quelle della ProgressBar, ma per il suo scopo specifico vale la pena tenere a mente:
- l'attributo thumb, che può essere valorizzato con l'ID di una risorsa drawable, permettendo di scegliere la forma dell'indicatore trascinabile;
- il listener OnSeekBarChangeListener, che dal codice Java consente di associare ed eseguire un'azione tutte le volte che l'utente modifica la posizione dell'indicatore.
RatingBar
La classe RatingBar deriva da SeekBar, quindi indirettamente anche da ProgressBar. Permette di visualizzare una barra di progresso modificabile, che non è però di forma orizzontale, bensì è costituita da una serie di simboli, tipicamente stelle. Il funzionamento è simile a quello della SeekBar, ed il suo campo di applicazione è tipicamente correlato alla possibilità di assegnare un voto discreto (tipicamente, come già detto, un numero di stelle).
L'attributo numStars offre la possibilità di scegliere da “quante stelle” deve essere costituita la RatingBar.
Immagini e Web
ImageView
È il widget che contiene un'immagine. Il suo attributo principale è src, che indica la sorgente dell'immagine da visualizzare (tipicamente una delle risorse dell'applicazione). Il metodo Java che serve ad impostare il Drawable da rappresentare è setImageResource(int), e l'intero richiesto come parametro è l'ID della risorsa.
WebView
È un browser vero e proprio inserito all'interno del layout. Nella guida gli è stato dedicato un intero capitolo; quindi, in questa sede, ne riassumiamo le caratteristiche principali. Oltre all'opportuno dimensionamento del controllo, l'operazione principale da svolgere è l'assegnazione dell'URL di cui mostrare il contenuto online. Lo si può fare con il metodo Java loadUrl, al quale verrà passata una stringa rappresentante un indirizzo web, o con loadData al quale, tra l'altro, si deve passare una stringa contenente codice HTML da visualizzare.
È importante ricordare che per consentire l'accesso alla rete da parte dell'applicazione è necessario dichiarare nel manifest l'apposita permission, cioè android.permission.INTERNET.
Edizione precedente (2011)
62. Il cuore di un'applicazione Android: AndroidManifest.xml
L'AndroidManifest.xml è il file che definisce i contenuti e il comportamento della nostra applicazione: all'interno di questo file sono elencate le Activity e i Service dell'applicazione, con i permessi che necessita per funzionare correttamente.
Ogni progetto Android include un AndroidManifest.xml memorizzato nella directory principale del progetto: in questo file XML si possono inserire nodi per ogni elemento (Activity, Service, Content Provider e così via) che compone la nostra applicazione, impostando i permessi per determinare come questi interagiscono l'un l'altro e con le altre applicazioni.
Come abbiamo visto ogni applicazione gira all'interno di un proprio processo Linux, per cui ci sono delle restrizioni ben specifiche: ogni processo non può accedere alla memoria di un altro processo, e ad ogni applicazione è assegnato uno specifico identificatore utente. Inoltre i file di un applicativo non possono essere letti o scritti da altri applicativi: anche l'accesso a diverse operazioni critiche sono protette, e dobbiamo specificatamente chiedere i permessi per utilizzarle. Questo tipo di richieste vanno impostate nell'AndroidManifest.xml .
Quando l'applicazione viene installata il gestore dei pacchetti concede o non concede i privilegi a seconda di come li abbiamo configurati nell'AndroidManifest.xml.
Tra i principali permessi che possiamo richiedere ci sono i seguenti:
- READ_CONTACTS: leggere (ma non scrivere) i dati dei contatti dell'utente.
- WRITE_CONTACTS: scrivere (ma non leggere) i dati dei contatti dell'utente
- RECEIVE_SMS: monitorare l'arrivo di messaggi SMS
- INTERNET: accedere ed utilizzare la connessione Internet
- ACCESS_FINE_LOCATION: utilizzare un accurato sistema di localizzazione come il GPS
La radice di ogni AndroidManifest.xml è il tag <manifest>: questo tag include i nodi che definiscono i componenti dell'applicazione, l'ambiente di sicurezza, e tutto ciò che fa parte dell'applicazione. Nelle lezioni successive vedremo come si configura correttamente questo importante file e quali strumenti possiamo utilizzare per facilitare il nostro lavoro.
63. Android Virtual Device: virtualizziamo i dispositivi
L'Android Virtual Device (AVD) permette agli sviluppatori di testare le loro applicazioni senza dover per forza possedere e collegare al PC uno smartphone Android vero e proprio. Gli AVD possono essere creati con differenti configurazioni per emulare differenti tipi di smartphone reali. L'SDK di Android non include alcun AVD pre-installato, dunque è necessario creare almeno un device virtuale prima di poter testare le nostre applicazioni in un emulatore.
Come vedremo in dettaglio nelle lezioni successive, ogni AVD è configurato almeno con un nome, una versione di SDK supportata (Android 1.6, Android 2.1 e così via), un valore che indica la quantità di memoria da riservare per la scheda SD e una risoluzione per lo schermo.
Il plugin ADT di Eclipse (nelle lezioni precedenti abbiamo visto come installarlo e come integrarlo all'interno dell'IDE) permette di gestire come facilità gli AVD e gli emulatori. In particolare permette di lanciare automaticamente l'emulatore all'interno dell'AVD selezionato per testare o effettuare il debug dei nostri progetti. Ovviamente chi non volesse utilizzare Eclipse e il plugin corrispondente può controllare l'emulatore attraverso la linea di comando (il terminale).
64. Resource : la gestione delle risorse in Android
Una Resource può essere un'immagine bitmap, una stringa di testo che valorizza una etichetta o un bottone, o può essere qualsiasi altra informazione che non sia codice e che è necessaria per il corretto funzionamento dell'applicazione Android.
Tutte le Resource vengono compilate all'interno dell'applicazione: questo è molto utile per implementare l'internazionalizzazione o per supportare diversi tipi di device. Le Resource sono memorizzate e create all'interno di una directory specifica, la directory res del nostro progetto: il compilatore delle risorse di Android processa le risorse in base alle sotto-directory in cui sono memorizzate e in base al formato dei file.
Ad esempio i file immagine JPG o PNG dovrebbero essere memorizzati in una directory all'interno del percorso res/drawable mentre i file XML che descrivono e configurano i layout delle schermate dovrebbero essere memorizzate in una directory all'interno del percorso res/layout. Possiamo aggiungere alle directory suffissi specifici per indicare una particolare lingua, un determinato orientamento dello schermo, la densità dei pixel e molto altro ancora.
Il compilatore delle risorse comprime e impacchetta le risorse delle nostre applicazioni, e successivamente genera una classe chiamata “R” che contiene tutti gli identificatori che possiamo utilizzare per riferirci alle risorse del programma. Questa tecnica assicura che tutte le referenze delle Resource siano valide e permette di risparmiare spazio non dovendo memorizzare anche le stringhe di referenza come avviene in ambiente Java standard.
65. Creazione di un'applicazione
Ora siamo pronti per implementare la nostra prima applicazione Android: iniziamo con il classico esempio “Hello Word”.
Prima di creare un nuovo progetto però dobbiamo installare alcuni componenti aggiuntivi nell'SDK di Android, componenti che ci permetteranno, in fase di creazione del progetto, di selezionare un target di riferimento per il progetto stesso. Per fare questo, come vedremo dettagliatamente nelle lezioni successive, apriamo l' “Android SDK and AVD Manager” seguendo questo percorso dal menu di Eclipse: Window / Android SDK and AVD Manager (per installare Eclipse e l'SDK di Android si veda la lezione I software necessari).
Nella finestra che compare selezioniamo la voce Available packages del menu di sinistra, poi la voce Android Repository nel pannello di destra e selezioniamo i componenti che vogliamo installare (per questo e per i prossimi esempi che vedremo possiamo installare anche solo il pacchetto “SDK Platform Android 2.1, API7?). Facciamo clic su Install selected e poi, nella finestra successiva, su Install. Il sistema installerà automaticamente anche gli “Android SDK Platform-tools”. Ad installazione conclusa possiamo proseguire.
Fig. 1: L'installazione dei componenti aggiuntivi di Android
Dal menu File di Eclipse selezioniamo New / Project. Nella finestra che compare selezioniamo Android / Android Project e poi clicchiamo su Next.
A questo punto appare la finestra di dialog New Android Project: con questo wizard configureremo il nuovo progetto.
Inseriamo la stringa “HelloWord” come nome del progetto, “HelloWordApp” come nome dell'applicazione, “com.html_it.examples” come nome del pacchetto, “HelloWordActivity” come nome dell'Activity. La stringa che inseriamo come nome dell'applicazione è quella che comparirà nella barra del titolo dell'applicazione, dunque in una implementazione reale bisogna avere qualche accortezza per la scelta di questo nome.
Fig. 2: La finestra di configurazione dell'applicazione
Rimane da selezionare il build target per l'applicazione, scegliamo per il nostro esempio il target Android 2.1 (API level 7), e il Min SDK (nel nostro caso 7), ovvero la versione minima di Android richiesta per la nostra applicazione.
A questo punto clicchiamo su Finish, e l'ADT genererà la struttura rappresentata nella figure 3: questa struttura rimane molto simile anche per applicazioni più complesse e di maggiori dimensioni rispetto all'esempio “HelloWord”. La struttura la vedremo in dettaglio nella lezione successiva.
66. Struttura di un'applicazione
Riproponiamo, per comodità, la figure 3 già vista nella lezione precedente e ne descriveremo in dettaglio le varie componenti.
Fig. 3: La struttura di un'applicazione Android
Un'applicazione Android deve avere almeno tre componenti per poter funzionare:
- Il file AndroidManifest.xml, che come abbiamo visto nelle lezioni precedenti si trova nella directory principale del progetto ed è responsabile della configurazione e del comportamento della nostra applicazione.
- La cartella “src” che contiene il codice sorgente
- La cartella “resource” che contiene le risorse necessarie alla nostra applicazione per un corretto funzionamento: questa cartella può contenere sottocartelle per l'organizzazione delle risorse, come ad esempio “drawable”, “raw”, “layout”, “anim” e così via
Abbiamo già introdotto il file AndroidManifest.xml, mentre il codice sorgente inizieremo a vederlo nelle lezioni successive.
Puntualizziamo invece alcune caratteristiche della gestione delle risorse in ambiente Android: innanzitutto dobbiamo sottolineare che Android supporta una organizzazione lineare per la cartella “res”. Questo significa che non sono supportate sottocartelle oltre il secondo livello, ad esempio all'interno della directory “layout” (e ovviamente all'interno delle altre sottocartelle dirette della directory “res”) il sistema non riconosce le sottocartelle create.
Un'altra puntualizzazione necessaria riguarda le cartelle “raw” e “assets”, che se necessarie creeremo nella cartella “res”. Queste due directory possono contenere la stessa tipologia di file, solo che quelli contenuti nella cartella “raw” sono considerati delle risorse a tutti gli effetti, mentre i file contenuti in “assets” no. I file contenuti in “raw” saranno localizzati e accessibili attraverso gli ID autogenerati, mentre il contenuto della cartella “assets” è considerato come contenuto generico: questo significa per esempio che nella cartella “assets” possiamo organizzare arbitrariamente la gerarchia di cartelle e sottocartelle, cosa che come abbiamo visto non è permessa quando ci occupiamo delle risorse.
La cartella “layout” contiene invece alcune View della nostra applicazione: è in questa cartella che mettiamo i file XML che definiscono le interfacce della app (invece che definirle da codice).
Le cartelle “drawable-ldpi”, “drawable-hdpi”, “drawable-mdpi” contengono immagini ed altre risorse grafiche per differenti risoluzioni e densità degli schermi, mentre nella cartella “anim” possiamo mettere le animazioni eventualmente utilizzate dalle nostre applicazioni. In “menu” ci saranno i descrittori XML che definiscono la struttura dei vari menu e in “values” possiamo mettere altre risorse utilizzate dall'applicazione, come ad esempio stili, stringhe e definizioni di colori (sempre sottoforma di descrittori XML).
67. Creare un dispositivo di emulazione
Per eseguire il building e il debugging di una applicazione Android dobbiamo prima completare i seguenti passi:
- Configurare un Android Virtual Device (AVD) per l'emultatore
- Creare una configurazione di debug per il progetto (lo vedremo nella lezione successiva)
Per completare queste procedure possiamo utilizzare il plugin ADT di Eclipse: l'IDE collegherà il debugger all'emulatore Android (o allo smartphone che usiamo per sviluppare) così potremo liberamente eseguire il debug delle applicazione in maniera comoda e precisa.
Android Virtual Device
L'Android Virtual Device descrive il tipo di device che l'emulatore può simulare, includendo la piattaforma Android supportata (Android 1.6, Android 2.1 e così via). La configurazione dell'AVD comprende la scelta della dimensione dello schermo, la possibilità di utilizzare una SD card e la sua dimensione. Per creare un AVD è necessario seguire questa procedura:
- Lanciare l'Android SDK e AVD Manager da Eclipse cliccando sull'icona dedicata che troviamo nella toolbar (figure 5) oppure seguendo il percorso di menu Window / Android SDK and AVD Manager;
Fig. 5: Il pulsante di accesso all'Android SDK in Eclipse
- Comparirà un dialog diviso in due parti: il menu e il pannello gestionale. Clicchiamo sulla voce “Virtual Devices” visibile nel box di sinistra: nella parte destra del dialog comparirà l'elenco degli AVD finora creati (se è il primo AVD che creiamo chiaramente la lista sarà vuota);
- Cliccare sul pulsante “New” per creare un nuovo AVD;
- A questo punto dobbiamo compilare i campi richiesti (figure 6). Scegliamo un nome per l'AVD; Selezioniamo il target: ad esempio per il supporto ad Android 2.1 scegliamo la voce “Android 2.1-update – API Level 7?. Ricordiamo che i target disponibili si installano seguendo la voce “Available packages” del menu di sinistra, e selezionando le piattaforme disponibili che compaiono cliccando sulla voce “Android Repository” del pannello destro;
- Scegliamo la capacità della card SD: l'immagine che verrà creata prenderà spazio nel nostro disco fisso, pertanto scegliamo un valore ragionevole, ad esempio 1024 Mib (il valore minimo è di 9 MiB);
- Il passo successivo è la scelta di una skin, ovvero la modalità di visualizzazione dell'emulatore. Selezioniamo la skin ”(HVGA)”, che rappresenta la visualizzazione in modalità “portrait”;
- Clicchiamo sul pulsante “Create AVD” e attendiamo la fine del processo di creazione.
Fig. 6: La creazione di un nuovo Android Virtual Device (AVD)
Finito il processo di creazione potremo vedere l'AVD che abbiamo creato nel pannello di destra “List of existing Android Virtual Device located at .”. Se volete testare il vostro emulatore è sufficiente selezionarlo e cliccare sul pulsante “Start” sulla destra della finestra dialog.
68. Configurare l'esecuzione e il debug
Prima di lanciare la nostra applicazione “HelloWord” è necessario creare una configurazione di “Run” o di “Debug” per il progetto. Vediamo i passi da completare per creare una configurazione di “Debug”, quella che useremo di più in fase di sviluppo:
- Sempre in Eclipse, nel menu principale selezioniamo il percorso Run / Debug Configurations;
- Nell'elenco disponibile sulla parte sinistra della finestra di dialog che si apre eseguiamo un doppio click sulla voce “Android Application” per creare una nuova configurazione;
- Cambiamo il nome della configurazione appena create da “New_configuration” a “Android Debug” (o altro nome);
- Clicchiamo sul pulsante Browse e scegliamo il progetto che vogliamo debuggare, nel nostro caso “HelloWord”;
- Spostiamoci sulla scheda Target e controlliamo il target associato al nostro progetto. Se lasciamo la spunta su Automatic verrà scelto in automatico il target adatto al progetto lanciato, altrimenti possiamo scegliere di spuntare la voce Manual: scegliendo questa opzione ogni volta che lanciamo un progetto con la configurazione “Android Debug” ci verrà chiesto di selezionare un target. Questa opzione è molto utile e comoda quando dobbiamo testare la nostra app su differenti smartphone o emulatori;
- Clicchiamo sul pulsante Apply per confermare le modifiche.
Fig. 7: La configurazione del debug di un'applicazione Android
La creazione di una configurazione per il running delle app è un processo molto simile a quello che abbiamo appena visto per la creazione della configurazione di debugging. Da un punto di vista pratico ciò che cambia è al punto 1 della precedente lista, dove al posto della voce Run / Debug configurations dovremmo seguire il percorso Run / Run Configurations.
Da un punto di vista funzionale invece ciò che cambia principalmente nell'utilizzare una configurazione di running piuttosto che una di debugging è che se usiamo quella di debugging il debugger di Eclipse sarà collegato alla nostra applicazione, facilitando il debug del nostro codice anche grazie all'utilizzo dei breakpoint.
Utilizzando il plugin ADT di Eclipse, quando eseguiamo il running o il debugging della nostra applicazione succede quanto segue:
- Viene compilato il progetto corrente e convertito in un eseguibile Android (.dex);
- Gli eseguibili e le risorse esterne della applicazione vengono organizzate in un package Android (.apk);
- Il device virtuale selezionato viene fatto partire (se non è già stato fatto partire prima, altrimenti questo punto non viene eseguito);
- L'applicazione viene installata nel device vituale selezionato;
- L'applicazione viene fatta partire nel device vituale.
69. Eseguire test sull'emulatore e sullo smartphone
Vediamo dunque come eseguire il debug della nostra applicazione di prova usando, prima, l'emulatore di Eclipse e, dopo, su un terminale reale.
Test nell'emulatore
Siamo pronti per lanciare e testare la nostra app nell'emulatore precedentemente configurato. Tutto quello che dobbiamo fare è cliccare sul pulsante Debug (
) che troviamo nella toolbar di Eclipse; oppure premere il tasto F11 o scegliere la voce Debug dal menu Run.
Una volta cliccato sul pulsante Debug verrà lanciato il l'emulatore: al primo lancio è necessario aspettare un po' di tempo prima di poter interagire con il device virtuale, dunque aspettiamo con calma che lo startup si concluda (anche alcuni minuti su computer meno potenti). Quando l'emulatore è pronto per l'interazione possiamo premere e trascinare il pulsante di sblocco con il lucchetto verde finché non sblocchiamo il device virtuale, come possiamo vedere nell'immagine seguente:
Fig. 8: L'emulatore prima di essere sbloccato
Se abbiamo fatto tutto correttamente nel nostro emulatore vedremo una schermata molto simile a quella seguente:
Fig. 9: L'emulatore con la nostra applicazione di prova
Come possiamo vedere nella figure 9, l'applicazione è molto semplice: viene visualizzato un controllo di tipo TextView con una linea di stato, “Hello Word, Helloword!”. Abbiamo creato e testato la nostra prima applicazione Android!
Test nello smartphone
Facciamo un ulteriore passo in avanti: dopo aver testato la nostra semplice applicazione del device virtuale, vediamo come si comporta su un device reale.
Per procedere abbiamo bisogno di un cavo USB per collegare il nostro smartphone al PC e completare la seguente procedura:
- In Eclipse selezioniamo Run / Debug Configurations;
- Clicchiamo due volte su Android Debug;
- Clicchiamo sulla scheda Target e impostiamo Deployment Target Selection Mode su Manuale“. Selezionare la voce Manual che ci permetterà di scegliere se eseguire il debug nell'emulatore o nel device;
- Clicchiamo sul pulsante Apply;
- Colleghiamo attraverso una porta e un cavo USB il nostro smartphone al PC.
Nel caso in cui stessimo lavorando in ambiente Linux (es. Ubuntu) è necessario eseguire qualche passaggio ulteriore:
- Accedere alla directory platform-tools: la troviamo nella directory in cui abbiamo installato l'SDK di Android;
- Eseguiamo il “kill” del server “adb” lanciando il comando sudo ./adb kill-server;
- Riavviamo il server “adb” con il comando sudo ./adb start-server. In questo modo il server avrà i permessi necessari per accedere al device collegato al Pc;
- Facciamo clic sul pulsante Debug per concludere la configurazione.
A questo punto comparirà la finestra Android Device Chooser, in cui vengono visualizzati i device, virtuali e/o reali, di cui possiamo disporre nel nostro PC e che possiamo scegliere per eseguire il debug dell'applicazione. Il nostro device deve essere abilitato per il debug attraverso la connessione USB. Per attivare questa configurazione, selezioniamo il menu Impostazioni dello smartphone, poi la voce Applicazioni e qui impostiamo la seguente configurazione:
- Attiviamo l'opzione Origini sconosciute;
- Selezioniamo la voce Sviluppo;
- Nelle opzioni disponibili in questa sezione attiviamo le voci Debug USB e Rimani attivo: quest'ultima opzione disabilita il controllo sull'attivazione dello schermo, lasciandolo sempre attivo.
Per concludere il processo è sufficiente fare doppio clic sul device in cui si vuole eseguire l'applicativo nella finestra Android Device Chooser.
Eseguito l'ultimo passaggio, Eclipse installerà l'applicazione Android nel device che abbiamo selezionato dall'elenco dei device disponibili (reali o virtuali) e la eseguirà. Se abbiamo eseguito questa procedura per l'applicazione “Hello Word” vista nella lezione precedente, nel nostro smartphone comparirà una schermata molto simile a quella rappresentata dalla figure 9.
Rimane da sottolineare ancora una questione: ogni volta che finiamo di lavorare sullo smartphone dobbiamo ricordarci di disabilitare le impostazioni attivate sopra. L'attivazione permanente dello schermo, se non disabilitata, ovviamente porta ad un rapido scaricamento della batteria, mentre l'attivazione delle altre opzioni al di fuori del contesto di sviluppo lascia aperte notevoli falle per la sicurezza del nostro device.
70. Definire caratteristiche e interfaccia grafica
Sviluppare software è un'attività complessa perché richiede diverse fasi da completare in modo attento e preciso per raggiungere pienamente il proprio obiettivo.
Nel contesto dello sviluppo mobile questo concetto è ancora più radicato: sviluppare un applicativo mobile è un'attività che richiede particolari attenzioni perché ci muoviamo in un ambiente in cui le risorse sono limitate, i monitor sono di piccole dimensioni e l'efficienza diventa una delle componenti fondamentali per la buona riuscita del nostro lavoro.
In questa e nelle seguenti lezioni vedremo alcune tecniche e best practise per scrivere codice Android efficiente e professionale e alcune linee guida per la progettazione degli applicativi. Partiamo dalle caratteristiche e dall'interfaccia grafica.
Le caratteristiche
Supponiamo di voler sviluppare un applicativo che implementi il famoso gioco del Tris: lo chiameremo MyTris.
La struttura di questa applicazione è molto semplice, infatti possiamo immaginarla composta di cinque elementi:
- Splash: lo splash rappresenta l'immagine visualizzata quando l'applicazione è in fase di caricamento, in cui normalmente vengono rappresentati il logo del gioco o dell'applicativo e il numero di versione. È anche molto comune la presenza di una musica di sottofondo;
- Menu: il menu permette all'utente di scegliere tra diverse opzioni, come ad esempio configurare l'applicativo, avviare il gioco, vedere i punteggi, accedere ai credits, visualizzare l'help e così via ;
- Avvio: questa schermata è quella che compare una volta avviato il gioco o l'applicativo;
- Help: il compito dell'help è quello di visualizzare le istruzioni di utilizzo del gioco o dell'applicativo, descrivendo i controlli disponibili, gli obiettivi, e nel caso dei giochi anche come si vince e il metodo con cui si calcolano i punteggi;
- Credits: in questa schermata vi saranno le informazioni sull'azienda o su chi si è occupato dello sviluppo o della distribuzione dell'applicativo: queste informazioni sono molto importanti perché ci permettono di essere contattati;
La struttura proposta per il gioco MyTris è in realtà una struttura molto tipica che ritroviamo implementata sia in giochi sia in applicativi di altro genere, indipendentemente dalla piattaforma di sviluppo. Questo perché è un valido modello che permette di avere una buona usabilità, una facilità d'uso ma anche flessibilità e chiarezza.
L'interfaccia grafica
Le interfacce utente in Android possono essere create in modo procedurale o dichiarativo. Nel primo caso si intende l'implementazione dell'interfaccia grafica nel codice: ad esempio, in un'applicazione Swing scriviamo il codice Java per creare e manipolare tutti gli oggetti dell'interfaccia utente, come JButton, JFrame e così via.
La creazione dichiarativa non richiede la scrittura di codice: l'esempio tipico del metodo dichiarativo è rappresentato dalla creazione di una pagina web statica, in cui utilizziamo l'HTML per descrivere cosa vogliamo vedere nella pagina. L'HTML è dunque dichiarativo.
Android permette la creazione di interfacce sia procedurali sia dichiarative: possiamo creare un'interfaccia utente completamente in codice Java (metodo procedurale) oppure possiamo creare l'interfaccia utente attraverso un descrittore XML (metodo dichiarativo). Android inoltre permette anche un approccio ibrido, in cui si crea un'interfaccia in modo dichiarativo e la si controlla e specifica in modo procedurale (si richiama il descrittore XML da codice e si continua a lavorare da lì).
Se guardiamo nella documentazione Android per i componenti delle interfacce utente vedremo che gli attributi presenti nelle API Java e quelli utilizzabili negli XML dichiarativi sono gli stessi: questo dimostra che in entrambi i casi abbiamo a disposizione le medesime possibilità. Dunque quale metodo è meglio utilizzare?
Entrambi i metodi sono soluzioni valide, anche se Google suggerisce nella maggior parte dei casi di usare la metodologia dichiarativa perché spesso il codice XML è più corto e semplice da leggere e capire rispetto al corrispondente codice Java, ed inoltre è molto meno probabile che cambi nelle versioni future della piattaforma.
71. Specificare le attività dell'applicazione
Abbiamo quindi ragionato sulla struttura base del nostro applicativo MyTris ed abbiamo analizzato e tracciato le linee guida per la progettazione delle interfacce che potremo implementare. Il passo successivo è quello di ragionare sul “core” del nostro gioco: le Activity.
MyTris necessiterà almeno di cinque Activity, una per ogni caratteristica del gioco:
- SplashActivity: questa sarà l'Activity lanciata di default che svolgerà il semplice compito di mostrare un layout composto molto probabilmente da una immagine, una musica di sottofondo suonata fino al caricamento della MenuActivity;
- MenuActivity: questa Activity mette a disposizione il menu del gioco, proponendo un layout composto da diversi pulsanti, uno per ogni feature dell'applicazione. Inoltre ad ogni pulsante assoceremo un gestore per l'evento “clic pulsante”, in modo tale che una volta premuto venga lanciata l'Activity legata al pulsante stesso;
- PlayActivity: questa Activity sarà la reale implementazione del gioco; sarà responsabile di gestire gli input dell'utente, di tenere i punteggi della partita e di gestire tutte le dinamiche del gioco che implementeremo;
- HelpActivity: visualizzerà un testo con le spiegazioni sul funzionamento del gioco e sul calcolo dei punteggi;
- CreditsActivity: visualizzerà un testo con le informazioni su chi ha sviluppato il prodotto e su come sia possibile contattarlo. Avrà un layout molto simile alla HelpAcvitity: probabilmente visualizzeremo il testo servendoci di un controllo TextView scrollabile;
Ogni Activity avrà la propria classe e il proprio layout memorizzato nella directory resources: in alcuni casi, come per le activity ScoreActivity e HelpActivity, visto che i loro compiti sono molto simili, si potrebbe pensare di condividere uno stesso layout e di impostare il testo da visualizzare a tempo di esecuzione invece che all'interno del layout stesso.
Possiamo inoltre servirci di schemi e wireframe per analizzare e studiare la navigabilità e l'usabilità del gioco, per verificare eventuali problematiche e criticità e trovare le soluzioni che più si addicono alle caratteristiche del nostro gioco.
72. Implementare le funzionalità
Ora che abbiamo visto come progettare una tipica applicazione Android possiamo passare all'implementazione. Tenendo presenti le caratteristiche del nostro gioco teorico MyTris, vedremo in questa lezione alcuni dettagli e principi che sono alla base dello sviluppo di un'applicazione Android. Vedremo poi come metterli in pratica.
Il primo elemento che vedremo più in dettaglio è l'application context. L'application context è un elemento centrale e importante per tutte le funzionalità disponibili al più alto livello applicativo: possiamo utilizzare l'application context per accedere alle risorse e alle impostazioni condivise tra istanze di diverse Activity.
Una volta recuperato l'application context, ad esempio con il seguente codice:
Context context = getApplicationContext();
possiamo utilizzarlo per accedere ad un ampio insieme di caratteristiche e servizi a disposizione dell'applicazione. Ad esempio possiamo richiamare il metodo getSharedPreferences() dell'application context per recuperare le preferenze condivide dell'applicazione, oppure possiamo richiamare il metodo getResources() per recuperare le risorse dell'applicazione:
String hello = context.getResources().getString(R.string.hello);
Visto che la classe Activity deriva dalla classe Context possiamo utilizzare il riferimento this oppure richiamare esplicitamente l'application context.
Potremmo dunque riscrivere l'esempio precedente come segue:
String hello = getResources().getString(R.string.hello);
L'application context lo utilizziamo anche per:
- Lanciare istanze di Activity.
- Gestire le directory, i database e i file protetti dell'applicazione.
- Richiamare i gestori di servizi di sistema (e.g. servizio di localizzazione).
- Applicare e controllare i permessi a livello applicativo.
- Richiamare gli asset dell'applicazione.
Possiamo dunque utilizzare l'application context per lanciare istanze delle Activity. Per il nostro gioco di esempio MyTris, dovremmo implementare, ad esempio, cinque Activity, una per ogni schermata.
Oltre all'application context abbiamo a disposizione altre due opzioni per richiamare e lanciare istanze di Activity: implementare il lancio di una Activity nel file manifest oppure lanciare un'Activity figlia da una Activity padre per riceverne un risultato.
Per ogni applicazione Android dobbiamo definire nel file manifest una Activity di default: nel nostro caso potrebbe essere SplashActivity, che come abbiamo visto nelle lezioni precedenti avrà il compito di visualizzare un'immagine e di riprodurre una musica come introduzione al gioco.
Nelle prossime lezioni vedremo una serie di tutorial che ci guideranno nello sviluppo di piccole funzionalità come la definizione dei layout, la gestione dei menu, la gestione con i form dell'input dell'utente, e molto altro ancora.
73. Running sull'emulatore
Durante l'implementazione possiamo servirci dell'emulatore per testare il nostro lavoro: questo strumento è molto comodo perché ci fornisce un preciso feedback sull'andamento del progetto.
Bisogna però tenere presente che questo strumento ha dei grossi limiti: mentre possiamo testare e verificare ad esempio la visualizzazione dei layout portrait (versione verticale) e landascape (versione orizzontale), la navigabilità e i menu, attualmente l'emulatore non implementa tutte le caratteristiche hardware supportate da Android, come la fotocamera, la vibrazione, i LED, l'accelerometro, e molto altro ancora.
In tutti quei casi in cui l'applicativo che dobbiamo sviluppare ha bisogno di queste caratteristiche hardware non ci rimane altra scelta che testare il nostro lavoro direttamente su uno smartphone (la procedura completa per testare le app su un device reale la trovate nella lezione 15).
Dopo aver opportunamente configurato il device reale potremo installare e testare la nostra applicazione studiandone il comportamento in un contesto reale: in questo modo verifichiamo che il software risponda effettivamente a quello che abbiamo programmato.
74. Lavoriamo con i Layout
Iniziamo le lezioni pratiche con lo studio dei layout. I layout sono estensioni della classe ViewGroup usati per posizionare i controlli delle nostre UI (User Interface): i layout possono essere nidificati permettendoci in questo modo di creare interfacce complesse usando anche combinazioni tra queste.
Android mette a disposizione dello sviluppatore alcuni semplici modelli di layout che semplificano notevolmente la creazione delle interfacce: possiamo creare combinazioni di questi “tipi” per rendere le nostre interfacce semplici da utilizzare e capire.
Vediamo alcune delle principali classi layout che la piattaforma ci mette a disposizione:
- TableLayout: questo tipo di layout ci permette di disporre gli oggetti View usando una griglia composta da righe e colonne, proprio come una tabella;
- LinearLayout: il LinearLayout allinea ogni View figlia sia in senso verticale sia in senso orizzontale (un layout orizzontale ha una riga di oggetti View, mentre un layout verticale è composto da una colonna di oggetti View);
- RelativeLayout: questo è il layout più flessibile tra quelli nativi perché permette di definire le posizioni di ogni oggetto View in relazione agli altri oggetti View presenti;
- FrameLayout: questa è la tipologia più semplice di layout, perché non fa altro che posizionare in ordine di inserimento tutti gli oggetti View partendo dall'angolo in alto a sinistra dello schermo. È molto comodo quando implementiamo degli swticher di immagini o delle interfacce a tab.
Vediamo come utilizzare i layout. Creiamo un progetto che chiamiamo HelloLinear (vedere le lezioni precedenti per le istruzioni sulla creazione di un nuovo progetto Android). Una volta completato il processo di creazione apriamo il file /res/layout/main.xml, che dovrebbe presentarsi più o meno come segue:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" /> </LinearLayout>
Questo file XML definisce un layout di tipo LinearLayout verticale, come indica l'attributo orientation impostato a vertical. Il layout contiene un widget figlio di tipo TextView, sostanzialmente un oggetto che visualizza del testo (quello contenuto dalla risorsa indicata nell'attributo text che in questo caso è impostato su @string/hello). Il risultato di questo layout è molto simile a quello rappresentato dalla seguente figura:
Fig. 10: L'emulatore di Android con un'applicazione di prova
Iniziamo a modificare questo layout: la prima cosa che facciamo è creare una nuova risorsa di tipo stringa. Per fare questo apriamo il file /res/values/string.xml ovvero il file che contiene le definizioni di tutte le risorse stringa. Quando lavoriamo con i file descrittori Eclipse ci fornisce, attraverso il plugin ADT, due modalità di visualizzazione e utilizzo: una testuale e una visuale. Nella figura seguente potete vedere la modalità visuale (la modalità testuale si attiva selezionando il tab string.xml e visualizza appunto un semplice XML modificabile):
Fig. 11: La modalità visuale delle risorse
Come mostra l'immagine, è selezionata la risorsa stringa identificata dal nome “hello” che ha il valore “Hello Word, HelloLinear!”. Questa è proprio la stringa che attualmente viene richiamata nel layout attraverso l'identificatore-nome “hello”: questo è il motivo per cui quando lanciamo il test di questa applicazione vediamo il seguente risultato:
Fig. 12: L'interfaccia con le nuove stringhe
dove il cerchio in verde evidenzia il valore stampato a video della risorsa stringa che contiene il nome dell'applicazione (nella figure 6 è la seconda risorsa in elenco denominata “app_name” ) mentre il cerchio rosso evidenzia il valore stampata a video della stringa “hello” vista poc'anzi. Quindi è evidente che possiamo modificare questi valori a nostro piacimento, anche dopo la creazione del progetto Android.
Utilizzando la modalità visuale aggiungiamo una risorsa di tipo stringa: clicchiamo dunque sul pulsante Add (v. figure 13 qui sotto) e nella finestra che compare verifichiamo che sia selezionato il tipo di risorsa string e clicchiamo su Ok. Concluso il processo di creazione delle risorsa possiamo procedere con l'impostazione dei valori come rappresentato nella figura seguente:
Fig. 13: Le impostazioni dei valori della nuova risorsa
Salviamo il file: abbiamo così creato una nuova risorsa stringa identificata da “name” impostata sul valore “Inserisci il tuo nome”.
Torniamo sul layout ed in particolare sul file ”/res/layout/main.xml“ e modifichiamolo come segue:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/name" /> <EditText android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="" /> </LinearLayout>
Abbiamo eliminato dal layout la visualizzazione della stringa identificata da “hello”, abbiamo inserito la visualizzazione della risorsa stringa creata da noi poc'anzi identificata da “name” e abbiamo aggiunto un altro widget, un oggetto EditText, ovvero un elemento molto simile ad una casella di testo HTML. Il risultato finale sarà simile al seguente:
Fig. 14: L'interfaccia con l'oggetto EditText
La figure 14 mostra un layout molto diverso dalla versione precedente (v. figure 12). Abbiamo modificato anche il nome dell'applicazione: come possiamo notare osservando più attentamente la figura nella barra grigia posta nella zona superiore della schermata rappresentata non compare più la stringa “Hello Linear” ma “HTML.IT – Applicazione 1”.
Possiamo eseguire questo tipo di esperimento con le principali tipologie di layout per vedere come si comportano gli oggetti che inseriamo e per imparare a disporre i widget per comporre interfacce più complesse e funzionali.
Nella prossima lezione modificheremo questo layout di esempio per implementare l'interfaccia di un semplice form e gestirne l'invio dei dati.
75. Gestione dell'input: lavoriamo con i form
Aggiungiamo alla nostra applicazione Android di esempio dei moduli per gestire le interazioni con l'utente
Proseguiamo lo studio della piattaforma Android ed introduciamo la gestione dei form e dell'input utente. In questa lezione riprenderemo il progetto introdotto nelle lezioni precedenti: modificheremo il layout per creare un form ed impareremo a gestire l'invio dei dati inseriti.
La prima cosa da fare è modificare il layout /res/layout/main.xml come segue:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/form_intro" android:layout_marginTop="15dip" android:layout_marginBottom="25dip" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/name" /> <EditText android:id="@+id/edit_name" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/lastname" android:layout_marginTop="15dip" /> <EditText android:id="@+id/edit_lastname" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="" /> <Button android:id="@+id/form_button" android:layout_height="wrap_content" android:text="@string/form_button_label" android:layout_marginTop="15dip" android:layout_marginLeft="90dip" android:layout_width="130dip" /> </LinearLayout>
Lasciamo invariata la tipologia del layout: utilizzeremo nuovamente il LinearLayout. La prima TextView inserita visualizza semplicemente un messaggio informativo che, come vedremo tra poco, imposteremo nel file string.xml .
Successivamente creiamo due coppie di widget costituite ognuna da un controllo TextView e da un controllo EditText (righe 16-39), per finire con un oggetto Button (righe 41-48) che attiverà l'invio del form. Il risultato che otteniamo sarà simile al seguente:
Fig. 15: Interfaccia con moduli e il bottone di invio
Come mostrato nella figure 15, ciò che otteniamo è simile ad un classico form in HTML: vi sono due caselle di testo e un bottone per l'invio del form. Prima di vedere il dettaglio del file risorse stringa /res/values/string.xml aggiornato, soffermiamoci ancora qualche istante sul nuovo layout e sul codice che lo gestisce.
Abbiamo introdotto alcune direttive per gestire gli spazi e i margini tra i widget, come ad esempio: android:layout_marginTop=“15dip” e android:layout_marginBottom=“25dip” che aggiungono, rispettivamente, un margine superiore di 15dip ed uno inferiore di 25dip (dove dip sta per density-independent pixels, un'unità di misura astratta basata sulla densità fisica dello schermo dello smartphone).
Inoltre abbiamo aggiunto gli attributi id ai controlli EditText e al Button:
android:id=”@+id/edit_name“android:id=”@+id/edit_lastname“android:id=”@+id/form_button“
Questi valori ci serviranno più avanti per identificare e richiamare questi widget da codice e poterne gestire i valori e le azioni ad essi associate.
Abbiamo introdotto nuovi widget ai quali abbiamo dovuto associare alcune risorse stringa. Vediamo allora il file delle risorse stringa (il file /res/values/string.xml) aggiornato:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">HTML.IT - Applicazione 1</string> <string name="form_intro">Invia il form premendo sul pulsante di invio.</string> <string name="name">Inserisci il tuo nome</string> <string name="lastname">Inserisci il tuo cognome</string> <string name="form_button_label">Invia</string> <string name="label_form_name">Nome Inserito:</string> <string name="label_form_lastname">Cognome Inserito:</string> </resources>
In questo caso non dovrebbe esserci nulla di nuovo o inaspettato: abbiamo definito delle risorse stringa identificate dall'attributo name il cui valore è contenuto all'interno dei tag <string></string> e verrà visualizzato lì dove la risorsa sarà richiamata.
Prima di analizzare il restante codice anticipiamo cosa vogliamo ottenere. Abbiamo un'Activity di default che crea l'interfaccia vista poc'anzi e che, attraverso il clic del pulsante Invia invierà ad un'altra Activity i dati che l'utente inserirà nelle opportune caselle di testo, la quale li visualizzerà con un'altra opportuna interfaccia.
Passiamo dunque ad analizzare il codice della prima Activity, quella lanciata di default. Nella cartella /src/html/it/examples/com troviamo il file HelloLinear.java ovvero il file in cui implementiamo l'Activity di default. Analizziamo in dettaglio il codice di questo file:
package html.it.examples.com; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; public class HelloLinear extends Activity implements OnClickListener { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); final Button button = (Button) findViewById(R.id.form_button); button.setOnClickListener(this); } @Override public void onClick(View v) { // TODO Auto-generated method stub switch ( v.getId() ) { case R.id.form_button: final EditText edit_name = (EditText)findViewById(R.id.edit_name); final EditText edit_lastname = (EditText)findViewById(R.id.edit_lastname); Bundle bundle = new Bundle(); bundle.putString("name", edit_name.getText().toString()); bundle.putString("lastname", edit_lastname.getText().toString()); Intent form_intent = new Intent(getApplicationContext(), Form.class); form_intent.putExtras(bundle); startActivity(form_intent); break; } } }
Il metodo onCreate() (rigo 12) è chiamato quando l'Activity viene creata e lanciata. Qui impostiamo il layout di default setContentView(R.layout.main); e attiviamo la gestione del clic sul widget Button visto precedentemente, impostando un listener sulla Activity stessa (che infatti implementa l'interfaccia OnClickListener).
Una classe che implementa l'interfaccia OnClickListener deve obbligatoriamente implementare il metodo OnClick(), metodo in cui si gestisce l'azione eseguita, in questo caso il clic sul Button Invia: soffermiamoci dunque sulla gestione dell'evento.
La prima cosa da fare (righe 23-24) è recuperare i riferimenti dei controlli EditText definiti precedentemente e che serviranno per salvare i dati inseriti dall'utente, in questo caso il nome ed il cognome:
final EditText edit_name = (EditText)findViewById(R.id.edit_name); final EditText edit_lastname = (EditText)findViewById(R.id.edit_lastname);
A questo punto (righe 25-27) creiamo un oggetto Bundle che utilizziamo per salvare i dati inseriti dall'utente:
Bundle bundle = new Bundle(); bundle.putString("name", edit_name.getText().toString()); bundle.putString("lastname", edit_lastname.getText().toString());
Utilizziamo il metodo putString() dell'oggetto bundle per salvare i dati inseriti, recuperati poi con il metodo getText() del widget EditText.
Con i dati memorizzati nel Bundle creiamo l'Intent per la chiamata dell'Activity alla quale li vogliamo passare (righe 28-30), nel nostro caso Form.class, associamo all'Intent il Bundle ed infine lanciamo l'Activity :
Intent form_intent = new Intent(getApplicationContext(), Form.class); form_intent.putExtras(bundle); startActivity(form_intent);
A questo punto l'applicativo “passerà la palla” ad una nuova Activity chiamata Form. Vediamo allora com'è definita questa Activity:
package html.it.examples.com; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class Form extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.form); final TextView text_name = (TextView) findViewById(R.id.view_name); final TextView text_lastname = (TextView) findViewById(R.id.view_lastname); Bundle bundle = this.getIntent().getExtras(); text_name.setText(bundle.getString("name")); text_lastname.setText(bundle.getString("lastname")); } }
Questa Activity è molto semplice: dopo aver impostato un nuovo layout (che vedremo in dettaglio a breve) richiama nel codice i due controlli TextView in cui visualizzeremo i dati inseriti dall'utente ed inviati dall'Activity di default attraverso il Bundle. Quindi recuperiamo i valori dal Bundle tramite il metodo getString() e li impostiamo rispettivamente negli oggetti TextView text_name e text_lastname. Il risultato sarà simile al seguente:
Fig. 16: Interfaccia con la risposta all'invio del modulo
Il layout rappresentato nella figure 11 è definito nel descrittore /res/layout/form.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/label_form_name" android:layout_marginTop="15dip" android:layout_marginBottom="10dip" /> <TextView android:id="@+id/view_name" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/label_form_lastname" android:layout_marginTop="15dip" android:layout_marginBottom="10dip" /> <TextView android:id="@+id/view_lastname" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout>
Come abbiamo visto nelle lezioni teoriche ogni volta che scriviamo una nuova Activity dobbiamo definirla nel file AndroidManifest.xml, il file principale per la configurazione di ogni applicazione Android. Prima di lanciare l'applicazione vediamo dunque come modificare l' AndroidManifest.xml per evitare di incorrere in un errore:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="html.it.examples.com" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="7" /> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".HelloLinear" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".Form" android:label="@string/app_name"> <intent-filter/> </activity> </application> </manifest>
Il codice aggiunto, che appunto dichiara l'esistenza di una nuova Activity, è quello evidenziato.
76. Menu e dialog
In questa lezione estenderemo ulteriormente le funzionalità della nostra applicazione Android dotandola con un classico menu per la gestione delle impostazioni.
Android supporta due tipi di menu: quelli che si attivano quando premiamo l'apposito pulsante fisico dello smartphone e i menu di contesto, quelli che si attivano tenendo premuto il dito sullo schermo. Noi implementeremo un esempio di menu del primo tipo, come quello rappresentato nella seguente figura:
Fig. 17: Interfaccia con un pulsante Impostazioni
Il menu Impostazioni si attiva premendo il pulsante Menu dell'emulatore o il corrispettivo pulsante fisico dello smartphone. Selezionando il menu si apriranno le opzioni disponibili, rappresentate nella seguente figura:
Fig. 18: Le due opzioni del menu Impostazioni
Vediamo i passaggi necessari per implementare questo menu.
La prima cosa che dobbiamo fare è creare una nuova cartella risorse: /res/menu/. In questa cartella creiamo un file XML menu.xml in cui definiamo il menu:
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/settings" android:title="@string/settings_label" android:alphabeticShortcut="@string/settings_shortcut" /> </menu>
Con questo nuovo descrittore XML definiamo un menu composto da un item con identificatore uguale a settings e valorizzato con due stringhe, una per il testo che comparirà nella voce di menu (Impostazioni, cfr. figure 17) e una che possiamo utilizzare come shortcut. Aggiorniamo anche il file delle risorse stringa /res/values/string.xml inserendo le nuove risorse necessarie:
<string name="settings_label">Impostazioni</string> <string name="settings_title">Impostazioni</string> <string name="settings_shortcut">i</string>
A questo punto dobbiamo modificare il file HelloLinear.java per legare il menu che abbiamo appena creato all'Activity di default, in modo che quando premiamo il tasto fisico di menu dell'emulatore o dello smartphone l'Activity di default sappia come gestire tale evento. Per fare questo aggiungiamo le seguenti direttive import:
import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem;
e implementiamo l'override del metodo onCreateOptionsMenu():
@Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu, menu); return true; }
Il metodo getMenuInflater() restituisce un'istanza di MenuInflater che utilizziamo per leggere la definizione del menu dall'XML vista poc'anzi e trasformarla in una View effettiva. Ogni volta che viene selezionata una voce di menu viene richiamato il metodo onOptionsItemSelected() di cui implementiamo l'override sempre nella stessa classe, proprio per gestire la selezione della voce di menu Impostazioni:
@Override public boolean onOptionsItemSelected(MenuItem item) { switch ( item.getItemId() ) { case R.id.settings: startActivity(new Intent(this, Settings.class)); return true; } return false; }
Riassumiamo quanto fatto finora: abbiamo creato il menu principale e l'abbiamo associato all'Activity di default. Questo elemento ha una voce di menu che si chiama Impostazioni: per gestire la selezione di questa voce di menu abbiamo implementato il metodo onOptionsItemSelected() che dopo aver verificato quale voce di menu è stata selezionata (nel nostro caso quella con identificatore uguale a R.id.settings) svolge l'azione corrispondente, nel nostro caso lanciare l'Activity Settings che vedremo tra poco.
Android mette a disposizione degli strumenti che semplificano molto la definizione di tutte le preferenze dell'applicativo e la visualizzazione di queste utilizzando pochissime linee di codice. Questo perché nella maggior parte della applicazioni che svilupperemo sarà presente un menu Impostazioni o Preferenze, cioè un menu con il quale l'utente può personalizzare alcuni comportamenti dell'applicazione.
Definiamo dunque le preferenze in un nuovo file risorsa /res/xml/settings.xml:
<?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <CheckBoxPreference android:key="item1" android:title="@string/menu_item_1" android:summary="@string/item1_summary" android:defaultValue="true" /> <CheckBoxPreference android:key="item2" android:title="@string/menu_item_2" android:summary="@string/item2_summary" android:defaultValue="true" /> </PreferenceScreen>
Come mostrato nella figure 13, l'utente può scegliere di attivare/disattivare due impostazioni, Item1 e Item2, entrambe sono due controlli di tipo CheckBoxPreference (in sostanza due widget molto simili alle checkbox dell'HTML). Anche in questo caso dobbiamo aggiornare il file risorsa /res/values/string.xml per le valorizzazioni delle voci di menu delle preferenze:
<string name="menu_item_1">Item 1</string> <string name="menu_item_2">Item 2</string> <string name="item1_summary">Summary Item 1</string> <string name="item2_summary">Summary Item 2</string>
Rimane da vedere la definizione della classe Settings.java:
package html.it.examples.com; import android.os.Bundle; import android.preference.PreferenceActivity; public class Settings extends PreferenceActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.settings); } }
Questa Activity estende PreferenceActivity e richiama il metodo addPreferencesFromResource(): questo legge le definizioni delle preferenze dall'XML passato come parametro (R.xml.settings) e permette di creare le viste corrispondenti nell'Activity corrente.
Non dobbiamo dimenticare di inserire la nuova Activity nel file di configurazione AndroidManifest.xml, nel quale aggiungiamo il seguente codice:
<activity android:name=".Settings" android:label="@string/settings_title"></activity>
Lanciamo nuovamente l'applicazione, premiamo il pulsante del menu, selezioniamo Impostazioni e cambiamo i valori delle voci Item1 e Item2: se proviamo ad uscire dal programma e poi a rientrarci, vedremo che le voci di menu delle preferenze sono nuovamente entrambe attive, indifferentemente dalle nostre scelte precedenti.
Questo succede perché non abbiamo gestito e memorizzati i valori delle scelte effettuate dall'utente: questo argomento esula dal carattere introduttivo di queste lezioni, ma per ora possiamo considera che questi valori potrebbero essere memorizzati come dati locali dell'applicativo.
77. Gestione della localizzazione
I servizi di localizzazione in Android si basano su due API: quella del mapping e quella della location. Queste API sono divise in due pacchetti separati ed indipendenti, rispettivamente com.google.android.maps e android.location: l'API del mapping fornisce strumenti che facilitano la visualizzazione e la manipolazione di una mappa, come ad esempio la gestione dello zoom e cambiare la modalità di visualizzazione (vista satellite, street view, ecc.), mentre l'API location mette a disposizione strumenti per la gestione del GPS (Global Position System) e dei dati di localizzazione in tempo reale.
In questa lezione implementeremo un semplice servizio che visualizzerà alcune informazioni circa le coordinate geografiche che simuleremo ed invieremo all'emulatore.
Modifichiamo il file /res/menu/menu.xml aggiungendo il codice seguente:
<item android:id="@+id/where_am_i" android:title="@string/where_am_i_label" android:alphabeticShortcut="@string/where_am_i_shortcut" />
con il quale inseriamo una nuova voce di menu per accedere all'Activity che implementeremo tra poco. Chiaramente dobbiamo anche aggiornare il file delle risorse stringa /res/values/string.xml:
<string name="where_am_i_label">Dove Sono?</string> <string name="where_am_i_title">Dove Sono</string> <string name="where_am_i_shortcut">w</string>
A questo punto creaimo un nuovo file descrittore XML /res/layout/where.xml per la generazione del layout della nuova Activity:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:layout_height="wrap_content" android:text="La locazione rilevata è:" android:layout_width="fill_parent" android:textSize="16px" android:textStyle="bold" android:gravity="center_horizontal" android:layout_marginTop="10dip" android:layout_marginBottom="10dip" /> <EditText android:layout_height="wrap_content" android:text="" android:layout_width="fill_parent" android:id="@+id/editTextShowLocation" android:lines="10" android:gravity="top"/> <Button android:layout_height="wrap_content" android:layout_width="fill_parent" android:id="@+id/buttonGetLocation" android:text="Carica Locazione" android:textSize="15px" android:textStyle="bold" android:layout_marginTop="10dip" /> </LinearLayout>
Il layout è molto semplice: è composto da una posto da una TextView per la visualizzazione di un testo introduttivo, un widget EditText per la visualizzazione dei dati e un Button per il caricamento dei dati. Il risultato sarà simile al seguente:
Fig. 19: Interfaccia con i dati di localizzazione
Come rappresentato nella figure 19, nel widget EditText visualizziamo la data odierna, lo strumento che ci fornisce i dati (GPS) e alcune informazioni relative alla posizione rilevata: longitudine, latitudine, altitudine, accuratezza.
Vediamo in dettaglio il codice della classe Where.java che implementa l'Activity:
package html.it.examples.com; import android.app.Activity; import android.content.Context; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; import android.text.format.DateFormat; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; public class Where extends Activity { private EditText editText; private Button buttonLoad; private LocationManager lManager; private LocationListener lListener; private Location location; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.where); editText = (EditText) findViewById(R.id.editText); buttonLoad = (Button) findViewById(R.id.buttonLoad); buttonLoad.setOnClickListener(new OnClickListener() { public void onClick(View v) { buttonLoadClick(); } }); } private void getCurrentLocation() { lManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); lListener = new LocationListener() { @Override public void onStatusChanged(String provider, int status, Bundle extras) { // TODO Auto-generated method stub } @Override public void onProviderEnabled(String provider) { // TODO Auto-generated method stub } @Override public void onProviderDisabled(String provider) { // TODO Auto-generated method stub } @Override public void onLocationChanged(Location loc) { // TODO Auto-generated method stub location = loc; } }; lManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, lListener); } private void buttonLoadClick() { this.getCurrentLocation(); if ( location != null ) { lManager.removeUpdates(lListener); String provider = location.getProvider(); String data = DateFormat.format("dd/MM/yyyy", location.getTime()).toString(); String longitude = "Longitudine: " + location.getLongitude(); String latitude = "Latitudine: " + location.getLatitude(); String altitudine = "Altitudine: " + location.getAltitude(); String accuracy = "Precisione: " + location.getAccuracy(); editText.setText("Oggi è il " + data + "nn" + "Informazioni fornite da: " + provider + "nn" + longitude + "n" + latitude + "n" + altitudine + "n" + accuracy + "n"); } else { editText.setText("Impossibile determinare la locazione!"); } } }
Recuperiamo i riferimenti delle risorse EditText e Button, ed impostiamo su quest'ultima un listener per gestire l'azione associata al clic del bottone:
editText = (EditText) findViewById(R.id.editText); buttonLoad = (Button) findViewById(R.id.buttonLoad); buttonLoad.setOnClickListener(new OnClickListener() { public void onClick(View v) { buttonLoadClick(); } });
Ogni volta che premiamo il bottone Carica Locazione vengono caricati i dati che simuliamo ed inviamo attraverso la scheda di Eclipse per la gestione dei controlli dell'emulatore, chiamata appunto Emulator Control.
Per aprire la vista Emulator Control entriamo nella perspective DDMS (Dalvik Debug Monitor Server) che fornisce strumenti utili per il debugging delle applicazioni Android. Per selezionare la perspective DDMS dobbiamo seguire dal menu di Eclipse il percorso Window / Open Perspective / Other / DDMS.
Il risultato sarà simile al seguente:
Fig. 20: La perspective DDMS e i controlli di location
Come mostrato in figure 20, nella scheda Devices selezioniamo l'emulatore al quale vogliamo inviare i dati GPS simulati (chiaramente nel caso in cui fosse collegato e opportunamente configurato un device reale potremmo anche selezionare questo). Una volta selezionato il device si attiveranno i controlli di locazione per l'inserimento dei dati: nella scheda a tab denominata appunto Emulator Control selezioniamo l'opzione Decimal, inseriamo le coordinate per la longitudine, quella per la latitudine, e premiamo il pulsante Send per inviare i dati appena inseriti nel device selezionato precedentemente.
A questo punto torniamo sull'emulatore: premiamo il pulsante Carica Locazione. Il risultato sarà molto simile a quanto rappresentato in figure 20.
Ogni volta che premiamo e attiviamo il pulsante Carica Locazione viene richiamata la funzione buttonLoadClick(). Questa funzione utilizza i metodi messi a disposizione dalla classe Location per recuperare le informazioni che rappresentiamo nel widget EditText:
String provider = location.getProvider(); String data = DateFormat.format("dd/MM/yyyy", location.getTime()).toString(); String longitude = "Longitudine: " + location.getLongitude(); String latitude = "Latitudine: " + location.getLatitude(); String altitudine = "Altitudine: " + location.getAltitude(); String accuracy = "Precisione: " + location.getAccuracy(); editText.setText("Oggi è il " + data + "nn" + "Informazioni fornite da: " + provider + "nn" + longitude + "n" + latitude + "n" + altitudine + "n" + accuracy + "n");
Richiamiamo il metodo setText() del controllo EditText al quale passiamo i dati raccolti, come ad esempio location.getLongitudine() e location.getLatitudine(), che sono proprio le coordinate che abbiamo inserito nella scheda Emulator Control.
78. Android e le applicazioni di rete
Le applicazioni che svilupperemo in ambito mobile saranno quasi sempre dotate di accesso ad Internet: per consultare notizie, per informarsi sul tempo atmosferico, per leggere le email, per rimanere in contatto con i proprio amici attraverso i social network o effettuare chiamate VoIP per risparmiare sulle telefonate.
In questa lezione vedremo come fornire alla nostra applicazione d'esempio la connettività ad Internet e come sfruttare la piattaforma Android per implementare un semplice lettore di feed RSS (Really Simple Syndication).
Per accedere ai servizi di rete su un device Android dobbiamo avere i permessi appropriati. Una app Android infatti può utilizzare la maggior parte dei servizi di rete solo se nel proprio file di configurazioni AndroidManifest.xml vi sono le appropriate impostazioni.
La prima cosa che facciamo dunque è modificare il file AndroidManifest.xml della nostra applicazione d'esempio aggiungendo i permessi per l'utilizzo di Internet:
<uses-permission android:name="android.permission.INTERNET"
Aggiungiamo una voce di menu al menu principale dell'applicazione per poter accedere al servizio RSS. Modifichiamo quindi il file /res/menu/menu.xml aggiungendo il seguente codice:
<item android:id="@+id/rss" android:title="@string/rss_label" android:alphabeticShortcut="@string/rss_shortcut" />
Aggiungiamo le nuove risorse stringa nel file /res/values/strings.xml per la voce di menu appena inserita:
<string name="rss_label">RSS</string> <string name="rss_title">RSS Feed</string> <string name="rss_shortcut">r</string>
Il risultato che otterremo sarà simile a quanta rappresentato in figure 16:
Fig. 21: L'applicazione con un pulsante per leggere gli RSS
L'implementazione del lettore di RSS è divisa in due Activity diverse: la prima (RSS.java) è quella in cui impostiamo il feed da leggere e il layout principale, la seconda (RSSItem.java) è quella che genera la lista di elementi pubblicati nel feed con il relativo layout.
Vediamo in dettaglio il codice della prima Activity RSS.java:
package html.it.examples.com; import android.app.Activity; import android.os.Bundle; import java.util.ArrayList; import android.widget.ArrayAdapter; import android.widget.ListView; public class RSS extends Activity { String feedUrl = ""; ListView rssListView = null; ArrayList<RSSItem> RSSItems = new ArrayList<RSSItem>(); ArrayAdapter<RSSItem> array_adapter = null; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.rssmain); feedUrl = "http://feeds.feedburner.com/Blogrammazione/"; refreshRSSList(); rssListView = (ListView) findViewById(R.id.rssListView); array_adapter = new ArrayAdapter<RSSItem>(this, R.layout.list_item, RSSItems); rssListView.setAdapter(array_adapter); refreshRSSList(); } private void refreshRSSList() { ArrayList<RSSItem> newItems = RSSItem.getRSSItems(feedUrl); RSSItems.clear(); RSSItems.addAll(newItems); } }
Inizialmente definiamo nella classe alcune variabili:
String feedUrl = ""; ListView rssListView = null; ArrayList<RSSItem> RSSItems = new ArrayList<RSSItem>(); ArrayAdapter<RSSItem> array_adapter = null;
La variabile feedUrl di tipo stringa ci servirà per memorizzare l'URL del feed da leggere; rssListView è la variabile che conterrà il riferimento al widget di tipo ListView contenuto nel descrittore XML che definisce il layout della lista di elementi letti dal feed; RSSItems è una struttura di tipo ArrayList che conterrà la lista degli elementi del feed (ognuno caratterizzato da un titolo, una descrizione, una data di pubblicazione e un link); array_adapter è una variabile che conterrà un oggetto di tipo ArrayAdapter che, come vedremo più in dettaglio tra qualche istante, permette di legare degli oggetti anche di tipo arbitrario contenuti in un array con un determinato layout più o meno complesso.
All'interno del metodo principale dell'Activity onCreate(), impostiamo il feed che leggeremo, richiamiamo il metodo refreshRSSList() e creiamo l'Array Adapter passando come parametri il contesto attuale (mediante il riferimento this), il file descrittore XML per il layout della lista (R.layout.list_item) ed infine il riferimento che conterrà la lista di oggetti RSSItem:
feedUrl = "http://feeds.feedburner.com/Blogrammazione/"; refressRssList(); rssListView = (ListView) findViewById(R.id.rssListView); array_adapter = new ArrayAdapter<RSSItem>(this, R.layout.list_item, RSSItems); rssListView.setAdapter(array_adapter); refreshRSSList();
La funzione refreshRSSList recupera e aggiorna la lista di item letti dal feed e aggiunge questi elementi all'ArrayList che contiene tutte le voci del feed:
private void refreshRSSList() { ArrayList<RSSItem> newItems = RSSItem.getRSSItems(feedUrl); RSSItems.clear(); RSSItems.addAll(newItems); }
Il file descrittore XML /res/layout/rssitem.xml per il layout di questa Activity è davvero molto semplice, visto che contiene solo un widget di tipo ListView:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <ListView android:id="@+id/rssListView" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout>
La seconda Activity, RSSItem.java, legge ogni elemento del feed e costruisce la lista di item che vengono visualizzati nella precedente RSS.java:
package html.it.examples.com; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; public class RSSItem { private String title; private String description; private Date date; private String link; public RSSItem(String title, String description, Date pubDate, String link) { this.title = title; this.description = description; this.date = pubDate; this.link = link; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Date getPubDate() { return date; } public void setPubDate(Date pubDate) { this.date = pubDate; } public String getLink() { return link; } public void setLink(String link) { this.link = link; } @Override public String toString() { SimpleDateFormat sdf = new SimpleDateFormat("hh:mm - MM/dd/yy"); String result = getTitle() + " ( " + sdf.format(this.getPubDate()) + " )"; return result; } public static ArrayList<RSSItem> getRSSItems(String feedUrl) { ArrayList<RSSItem> rssItems = new ArrayList<RSSItem>(); try { URL url = new URL(feedUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { InputStream is = conn.getInputStream(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document document = db.parse(is); Element element = document.getDocumentElement(); NodeList nodeList = element.getElementsByTagName("item"); if (nodeList.getLength() > 0) { for (int i = 0; i < nodeList.getLength(); i++) { Element entry = (Element) nodeList.item(i); Element _titleE = (Element)entry.getElementsByTagName("title").item(0); Element _descriptionE = (Element)entry.getElementsByTagName("description").item(0); Element _pubDateE = (Element) entry.getElementsByTagName("pubDate").item(0); Element _linkE = (Element) entry.getElementsByTagName("link").item(0); String _title = _titleE.getFirstChild().getNodeValue(); String _description = _descriptionE.getFirstChild().getNodeValue(); Date _pubDate = new Date(_pubDateE.getFirstChild().getNodeValue()); String _link = _linkE.getFirstChild().getNodeValue(); RSSItem rssItem = new RSSItem(_title, _description, _pubDate, _link); rssItems.add(rssItem); } } } } catch (Exception e) { e.printStackTrace(); } return rssItems; } }
Questa Activity genera gli item dell'RSS impostato precedentemente: ogni oggetto RSSItem ha un titolo, una descrizione, una data di pubblicazione e un link; per ogni attributo sono inoltre disponibili i classici metodi setter e getter.
Il cuore dell'Activity è il metodo getRSSItem(): qui apriamo una connessione URL con il server e recuperiamo i dati dell'RSS. Se la connessione è andata a buon fine allora utilizziamo il DocumentBuilderFactory e il DocumentBuilder per effettuare il parsing dell'XML:
URL url = new URL(feedUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { InputStream is = conn.getInputStream(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document document = db.parse(is); Element element = document.getDocumentElement();
Con il metodo getElementsByTagName() della classe Element recuperiamo tutti gli elementi item contenuti nell'RSS (corrispondenti ai tag <item></item> nei dati XML) e li salviamo in un oggetto di tipo NodeList. Effettuiamo un ciclo for su tutti i nodi dell'oggetto NodeList creando un oggetto RSSItem per ogni elemento presente, recuperiamo le informazioni dell'item (titolo, descrizione, data di pubblicazione e link) e lo aggiungiamo all'ArrayList:
NodeList nodeList = element.getElementsByTagName("item"); if ( nodeList.getLength() > 0 ) { for ( int i = 0; i < nodeList.getLength(); i++ ) { Element entry = (Element) nodeList.item(i); Element _titleE = (Element)entry.getElementsByTagName("title").item(0); Element _descriptionE = (Element)entry.getElementsByTagName("description").item(0); Element _pubDateE = (Element) entry.getElementsByTagName("pubDate").item(0); Element _linkE = (Element) entry.getElementsByTagName("link").item(0); String _title = _titleE.getFirstChild().getNodeValue(); String _description = _descriptionE.getFirstChild().getNodeValue(); Date _pubDate = new Date(_pubDateE.getFirstChild().getNodeValue()); String _link = _linkE.getFirstChild().getNodeValue(); RSSItem rssItem = new RSSItem(_title, _description, _pubDate, _link); rssItems.add(rssItem); } }
Infine viene restituito l'oggetto ArrayList con tutti gli item del feed RSS analizzati e memorizzati.
Anche il file descrittore XML per il layout da utilizzare per ogni item dell'RSS è molto semplice:
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:padding="10dp" android:textSize="13sp" android:autoLink="all" > </TextView>
Utilizziamo solo un widget TextView per la visualizzazione dei dati di ogni item dell'RSS. A questo proposito possiamo notare che abbiamo sovrascritto il metodo toString() per gli oggetti RSSItem, in modo che la stampa di un elemento di questo tipo permetta di visualizzarne il titolo e la data di pubblicazione, come mostrato nella figura seguente:
79. Pubblichiamo la nostra app
Ora che abbiamo imparato i fondamenti dello sviluppo di applicativi Android rimane un ultimo passaggio per chiudere il cerchio: il rilascio e la pubblicazione dell'app. In questa lezione vedremo come preparare e “confezionare” un'applicazione Android per il rilascio nella principale area di pubblicazione per applicazioni Android: l'Android Market.
Preparare ed eseguire il packaging di un'applicazione per la pubblicazione è un processo conosciuto come il nome di release process.
Il release process è il processo attraverso il quale passa la nostra app per diventare la release build, ovvero la versione ufficiale pronta per essere pubblicata. Prima di diventare la release build, e quindi di essere disponibile per l'utente finale, l'applicazione è la release candidate build: solo dopo attente e rigorose verifiche e test l'app diventa la release build.
Il processo per la pubblicazione di una applicazione Android si divide in 5 passi:
- Preparare una release candidate build della nostra applicazione.
- Testare attentamente e minuziosamente la release candidate.
- Generare un package Android e ufficializzarlo con una firma digitale.
- Testare attentamente e minuziosamente il package pronto per il rilascio.
- Pubblicare l'applicazione.
Prima di effettuare i test definitivi sulla nostra app dobbiamo preparare la release candidate build. Per fare questo dobbiamo aver implementato e testato tutte le funzionalità utili al corretto funzionamento dell'applicazione, sistemato tutti i bug e rimosso tutto il codice utilizzato per la diagnostica per evitare che incida nelle performance della app.
Per avere una release candidate ufficiale dobbiamo anche effettuare qualche modifica al file di configurazione AndroidManifest.xml . Alcune modifiche sono imposte dai marketplace come l'Android Market mentre altre sono dettate dal buon senso e da linee guida comunemente accettate. Vediamo dunque come dobbiamo preparare l'AndroidManifest.xml per una app che vogliamo ufficializzare a release candidate:
- Innazittutto verifichiamo che l'icona dell'applicazione sia impostata correttamente: questa icona sarà visualizzata agli utenti e verrà utilizzata dai marketplace per presentare l'app, quindi è importante che sia d'impatto e perfettamente configurata;
- Verificare che il nome dell'applicazione sia appropriato e correttamente impostato: questo sarà il nome con cui l'applicazione si presenterà agli utenti finali;
- Verificare la corretta impostazione del testo indicante la versione dell'applicazione;
- Controllare che il codice della versione dell'applicazione sia impostato correttamente: questo è un codice che la piattaforma Android utilizza per gestire gli aggiornamenti della nostra app;
- Verificare l'impostazione del uses-sdk dell'applicazione: possiamo impostare il massimo, il minimo e il consigliato SDK Android supportato dall'applicazione. L'Android Market filtra le applicazioni disponibili per uno specifico utente in base alle informazioni fornite da ogni file di configurazione AndroidManifest.xml, comprese quelle relative all'Android SDK;
- Verificare di aver disabilitato l'opzione debuggable;
- Verificare che tutti i permessi richiesti dall'applicazione siano appropriati per un corretto funzionamento della app: richiediamo solo i permessi davvero necessari e assicuriamoci di aver richiesto tutto il necessario indipendentemente da come si potrebbe comportare il device senza di essi.
A questo punto possiamo passare alla fase di testing. In realtà questa fase non richiede particolari interventi, più che altro richiede particolari attenzioni e accorgimenti: dobbiamo testare la nostra applicazione il più rigorosamente possibile e verificare che l'app rispetti i criteri richiesti dall'Android Market.
Se durante i test dovessimo riscontrare dei bug o delle problematiche di qualche genere sarà nostro compito valutare quanto siano importanti e serie, considerarne la gravità ed eventualmente valutare anche la possibilità di interrompere il processo di rilascio per iniziarlo nuovamente una volta sistemate le problematiche riscontrate.
Se la nostra applicazione ha superato la fase di testing, allora abbiamo ufficialmente la nostra release canditate build. A questo punto dobbiamo generare il package Android, nella pratica un file con estensione .apk, e ufficializzarlo con la firma digitale. Per completare facilmente questi passaggi il plugin di Android per Eclipse mette a disposizione un wizard che semplifica notevolmente il nostro lavoro.
Per lanciare il wizard è sufficiente cliccare con il tasto destro del mouse sul nostro progetto e selezionare la voce Export. Nella finestra che compare selezioniamo l'opzione Android e poi Export Android Application come mostrato nella seguente figura:
Fig. 23: Esportare un'app Android
Clicchiamo su Next, controlliamo che il progetto selezionato sia quello effettivamente di nostro interesse (altrimenti ne possiamo selezionare un altro dopo aver cliccato sul bottone Browse.), clicchiamo nuovamente su Next e arriviamo alla schermata per la selezione della Keystore:
Fig. 24: La scelta della chiave keystore
Scegliamo l'opzione Create new keystore (se possediamo già una keystore precedentemente creata possiamo anche valutare di utilizzare questa scegliendo l'opzione Use existing keystore) e nel campo Location inseriamo il percorso del file in cui vogliamo memorizzare la chiave. Inseriamo anche la password per la gestione della chiave, la confermiamo, e clicchiamo su Next.
Accediamo dunque alla schermata Key Creation in cui dobbiamo inserire alcune informazioni dettagliate sulla chiave, come mostrato dalla seguente figura:
Fig. 25: La creazione della keystore
Il team di Android suggerisce per il campo Validity di inserire un valore almeno uguale a 25: l'Android Market infatti rifiuta qualsiasi applicazione che non sia dichiarata valida fino alla data del 22 ottobre 2033.
Clicchiamo su Next per accedere alla schermata Destination and key/certificate checks, selezioniamo la destinazione per il file .apk ed infine clicchiamo su Finish. Abbiamo così creato un application package file pronto per la pubblicazione: prima però di renderlo disponibile all'utente finale attraverso i market come l'Android Market è consigliabile eseguire ancora qualche test sull'installazione del pacchetto .apk che abbiamo appena creato.
Proviamo dunque ad installarlo e disinstallarlo sia nell'emulatore sia su uno o più device reali, in modo da verificare che il processo di installazione dell'.apk che vogliamo pubblicare sia completo e vada a buon fine.
Conclusi anche questi test sul file .apk possiamo iniziare il procedimento per pubblicare la nostra app nell'Android Market. Per pubblicare applicazioni nell'Android Market dobbiamo essere registrati come sviluppatori. Per registrarci è necessario accedere al link https://play.google.com/apps/publish/signup:
Fig. 26: La pagina di registrazione dell'Android Market
Possiamo eseguire il login con l'account Google che preferiamo: se non disponiamo ancora di un account Google possiamo crearne uno seguendo il link Sign up disponibile nella pagina.
Una volta entrati la prima cosa che dobbiamo fare è completare il profilo dello sviluppatore con le informazioni richieste (Developer name, Email Address, ecc.). Compilato il form con i dati dello sviluppatore possiamo andare avanti cliccando sul link Continue: a questo punto ci viene richiesto di pagare una piccola somma per la registrazione (25 dollari) e di fornire le informazioni per l'account Google Checkout Merchant. L'ultimo passaggio per concludere la registrazione è dare l'assenso per collegare la nostra carta di credito e il nostro account di registrazione all'Android Market Developer Distribution Agreement.
A questo punto pubblicare un'app è un processo davvero semplice: accediamo da loggati alla pagina principale dell'Android Market, clicchiamo sul pulsante Upload Application e compiliamo il form che ci viene presentato con le informazioni richieste. Dovremo ad esempio caricare il file .apk creato durante la procedura di release, due screenshot dell'applicazione ed eventualmente un'immagine promozionale, e ancora dovremo specificare il tipo e la categoria dell'applicazione, eventualmente il prezzo, e dare il consenso ai termini delle correnti Android Content Guidelines.
Dopo aver compilato correttamente il form per la pubblicazione e aver cliccato sul pulsante Publish, l'applicazione comparirà immediatamente nell'Android Market: da questo momento potremo vedere le statistiche relative alla app, come ad esempio quante volte è stata visitata, scaricata ed installata, ed anche i punteggi ricevuti dagli utenti.
Conclusioni
La piattaforma Android permette di entrare nel mondo dello sviluppo mobile in una delle realtà attualmente più interessanti ed in continua evoluzione, quella degli applicativi Android, appunto.
Gli strumenti e le possibilità offerte da questa piattaforma ci permettono di lavorare in uno dei contesti più tecnologicamente avanzati del momento: ora che conosciamo i fondamenti di questa piattaforma possiamo iniziare lo sviluppo delle nostre app dando libero sfogo alla nostra fantasia ed immaginazione.
Lo sviluppo di applicativi mobile non è solo divertimento, ma rappresenta anche un'interessante occasione di guadagno e offre la possibilità di entrare in un mercato ancora tutto da scoprire e formare. I market offrono l'occasione di distribuire i nostri lavori a milioni di utenze a costi decisamente contenuti, motivo per cui i nostri sforzi nello studio di questa tecnologia potrebbe essere ripagati in pochissimo tempo.










































