Scarica la Tesi in PDF
Transcript
Scarica la Tesi in PDF
FACOLTÀ DI SCIENZE DELLA COMUNICAZIONE CORSO DI LAUREA IN MANAGEMENT E COMUNICAZIONE D’IMPRESA INDIRIZZO PUBBLICITÀ E MARKETING Tesi di laurea in Programmazione e scrittura del web IL COMPUTER NEL TASCHINO, IL NUOVO MONDO DELLE APP LAUREANDO RELATORE Alessandro Angiolla Chiar.mo Prof. Luca Tallini Matr. 69425 Anno Accademico 2013-2014 2 3 4 IL COMPUTER NEL TASCHINO, IL NUOVO MONDO DELLE APP Come sviluppare un App Android INTRODUZIONE L’informatica è una scienza in continua evoluzione, così come la tecnologia. Un tempo i computer erano enormi macchinari grandi come una stanza difficili da programmare ed utilizzare, oltre che costosissimi, ora invece sono sempre più piccoli, potenti, intuitivi, semplici da programmare ed economici, ormai quasi ogni cosa di uso quotidiano risponde alle leggi dell’informatica, dalle carte di credito ai frigoriferi, ormai quasi tutto contiene un chip elettronico, ed i computer ormai si sono evoluti nei dispositivi portatili che usiamo tutti i giorni, mi riferisco naturalmente agli smartphone e ai tablet, dispositivi che oggi usiamo praticamente per tutto e a cui spesso affidiamo i nostri dati più sensibili, infatti oggi possiamo utilizzare uno smartphone anche per fare acquisti online, o direttamente pagare con esso come fosse una carta di pagamento, ciò che prima si faceva con i normali computer, adesso lo si può fare anche con uno smartphone o un tablet, per cui essere in grado di programmare applicazioni per questi nuovi dispositivi, è di vitale importanza per un aspirante programmatore, dato che il mobile ormai rappresenta il futuro del mercato. Questo naturalmente non significa che i normali computer sono destinati a sparire, in quanto smartphone e tablet hanno anch’essi i loro limiti, soprattutto riguardo lo spazio di archiviazione, ben più limitato rispetto ad un comune pc, sia per quanto riguarda la potenza grezza, dato che anche se c’è da dire che questi dispositivi montano componenti sempre più all’avanguardia, un tablet di fascia alta rimane comunque inferiore come prestazioni ad un pc di fascia alta, ma comunque in un ambito in cui portabilità ed efficienza energetica sono molto importanti, un dispositivo mobile risulta decisamente un ottimo compromesso e quindi un buon sostituto del pc. 5 CAPITOLO 1 Il nuovo mercato 1.1 I contendenti Il primo quesito che ci dobbiamo porre prima dell’acquisto di un dispositivo mobile, tablet o smartphone che sia, è sicuramente il sistema operativo che esso monta, dato che esso va ad influenzare direttamente sia l’offerta di software, che la nostra esperienza con il dispositivo, i tre principali concorrenti, possiamo dire essere gli stessi del mercato dei pc, infatti se i principali sistemi operativi per i computer sono Windows, OSX e Linux, i principali sistemi operativi per tablet e smartphone sono Windows RT, IOS e Android, dove Windows RT sta a Windows, IOS sta ad OSX ed Android sta a Linux, poiché proprio di versioni mobili di tali sistemi operativi si tratta, ma analizziamoli un attimo più nel dettaglio. Windows RT, ovvero la versione mobile del diffusissimo sistema operativo di casa Microsoft, dei tre a differenza del suo fratello maggiore non è il più diffuso, ed è installato principalmente sugli smartphone Nokia, la quale ha ormai da tempo abbandonato il sistema Symbian e sui tablet, l’esperienza d’uso è molto simile a quella che si ha con Windows 8, anche se c’è da dire che è il sistema operativo del pc che si è uniformato con quello mobile e non il contrario, un chiaro segnale di quanto sia importante il nuovo mercato, la versione tablet soprattutto è praticamente indistinguibile dalla versione per pc, salvo che per la compatibilità dei programmi. IOS invece è il sistema operativo mobile di casa Apple, ha un interfaccia grafica piuttosto sobria, e proprio come OSX, punta principalmente all’usabilità e all’immediatezza a scapito della possibilità di personalizzazione, il sistema perfetto per chi non è un grande esperto di tecnologia, ma piuttosto limitato per chi vuole controllare ogni aspetto del proprio dispositivo, inoltre essendo un sistema operativo di casa Apple, sottostà alle rigorose regole della casa di Cupertino, quindi esso è installabile solo sui dispositivi Apple, ed è possibile installare solo la versione decisa da Apple per tale dispositivo, inoltre ci sono diverse limitazioni volute da Apple, tra cui l’impossibilità di installare applicazioni al di fuori dell’Apple store, l’impossibilità di trasferire dati tramite il modulo Bluetooth e la necessità di utilizzare iTunes per far interfacciare il dispositivo con il pc. Infine abbiamo Android, sistema operativo open source del colosso Google, basato su kernel Linux, nonostante Android sia decisamente più intuitivo di Linux, proprio come possiamo definire Linux l’opposto concettuale di OSX, anche Android è 6 l’opposto concettuale di IOS, se IOS puntava infatti alla semplicità di utilizzo a discapito della personalizzazione, Android invece punta alla personalizzazione, anche a discapito dell’usabilità, certo non ai livelli di Linux magari, ma c’è da dire che l’utente finale ha molto controllo sul sistema, e volendo senza troppi problemi, se sa quello che fa, più anche ottenere pieno controllo sul sistema ottenendo i privilegi di root, ma anche senza i privilegi di root, già dal momento in cui togliamo il dispositivo dalla scatola, possiamo già installare un applicazione creata da noi. Il sistema operativo quindi non è assolutamente una caratteristica da sottovalutare in quanto determina l’usabilità generale del dispositivo, il nostro spazio di intervento e la disponibilità di software. 1.2 Davide contro Golia, ARM contro x86 La battaglia tra Davide e Golia è forse ciò che rappresenta meglio il mondo della tecnologia, un mondo in cui vince il più piccolo, transistor sempre più piccoli garantiscono maggior efficienza, ma se questo è vero dal punto di vista della componentistica interna, non sempre è vero però se parliamo delle dimensioni generali degli smartphone, infatti uno smartphone troppo piccolo, può risultare scomodo nell’utilizzo, mentre uno smartphone troppo grande può risultare troppo ingombrante, per questo infatti esistono smartphone delle più svariate dimensioni, per soddisfare varie esigenze. Parlando sempre di dimensioni ciò che salta subito all’occhio è la differenza tra i computer e i tablet, ormai molto simili ai computer portatili, ma quali sono le differenze quindi? In primo luogo la potenza, poiché un tablet è di norma meno potente di un notebook di pari fascia come un notebook è meno potente di un desktop di pari fascia, inoltre un tablet è più sottile e leggero, ha una miglior autonomia della batteria e non necessita di ventoline per il raffreddamento, ma ha anche uno spazio di archiviazione nettamente inferiore ed un parco software diverso, queste differenze, sono date dalla differente architettura del processore e dal tipo di memoria di massa utilizzato. I normali computer montano infatti processori x86 con supporto ad istruzioni x64, mentre i tablet e gli smartphone utilizzano processori con architettura ARM, la principale differenza è che i primi sono più potenti, ma anche meno efficienti dal punto di vista energetico, cosa che si traduce in calore da dissipare, i processori ARM anche se meno potenti sono decisamente più efficienti dal punto di vista energetico, generando quindi pochissimo calore, non necessitando quindi di un sistema di raffreddamento attivo, ciò si traduce quindi in maggior durata della batteria, massima silenziosità, dimensioni e peso ridotti, ma c’è comunque da dire che ARM non supporta i programmi compilati per x86 e viceversa. 7 Un'altra importata differenza è data dall’unità di archiviazione di massa, ovvero la memoria non volatile in cui possiamo memorizzare i nostri files, i normali computer utilizzano prevalentemente memorie di tipo magnetico, ovvero i classici Hard Disk, da 3.5 pollici nei computer desktop e 2.5 pollici nei portatili, sono memorie molto capienti che possono arrivare contenere anche qualche Tera Byte (1 TB = 1024 GB) di dati, sono abbastanza veloci sia in lettura che in scrittura, ma non sono molto efficienti dal punto di vista energetico, inoltre soffrono del problema della frammentazione dei dati, in quanto trattandosi di un disco a tutti gli effetti con tracce e settori, bisogna scorrerlo alla ricerca ti tutti i frammenti dei files che vengono memorizzati spezzettati nei vari spazi vuoti lasciati dai files eliminati, i computer più costosi possono in alternativa o nel caso di desktop in aggiunta, montare una memoria di tipo SSD a stato solido, un tipo di memoria molto più costosa dei normali Hard Disk magnetici, ma anche molto più veloce ed energeticamente efficiente, inoltre non soffrono il problema della frammentazione, ma comunque hanno un costo elevato e minor capacità di memorizzazione rispetto ad un comune Hard Disk, gli SSD più grandi infatti non superano qualche centinaia di Giga Byte, infine abbiamo le memorie di tipo Flash, ovvero il tipo di memoria principalmente utilizzato per smartphone, tablet, memorie USB e memory card. Le memorie di tipo Flash sono molto compatte ed energeticamente efficienti, ma non sono molto veloci in fase di scrittura inoltre non sono molto capienti, e raramente superano i 64GB, ma di solito hanno tagli decisamente più piccoli (1, 2, 4, o 8 GB), spesso però è possibile espandere la memoria interna del dispositivo con una memory card esterna, ma non è possibile su tutti i dispositivi, per cui non sempre un tablet può sostituire un computer al 100%, alcuni tablet hanno anche un ingresso USB, ma comunque supportano solo memorie formattate in FAT o FAT32 mentre le memorie più grandi di 32GB sono di norma formattate in NTFS quindi resta comunque difficile gestire grandi quantità di dati senza un normale computer, anche se comunque è questa la direzione verso cui ci stiamo muovendo. 8 1.3 Il Cloud, i nostri files ovunque Abbiamo già realizzato che il principale problema dei nuovi dispositivi smart è dato dalle dimensioni della memoria di massa e dalla difficoltà di espansione della tale, ma c’è anche da dire che oggi più che mai Internet non è mai stato così accessibile, soprattutto se abbiamo uno smartphone o un tablet, sicuramente dotato di Wi-Fi e connettività alle reti mobili 3g e 4g, quasi tutti i gestori di telefonia mobile ormai offrono nelle proprie promozioni oltre a chiamate ed SMS anche la connessione ad internet su rete 3g/4g, inoltre sempre più comuni stanno iniziando ad offrire copertura Wi-Fi gratuita in alcune zone, seppur con alcune limitazioni, basta quindi avere uno smartphone o un tablet per poter navigare in Internet anche se non si ha un abbonamento grazie appunto alle Wi-Fi zone gratuite. Per ovviare almeno in parte al problema della memorizzazione dei dati, si è deciso quindi di sfruttare la connettività dei suddetti dispositivi per poter memorizzare i dati in eccesso o che comunque si vuole avere disponibili su più dispositivi, nasce quindi il Cloud. Ma cos’è il Cloud? Il Cloud altro non è che un server remoto su cui ci viene riservato dello spazio che potremmo utilizzare per memorizzare i nostri files per poterli poi recuperare da qualsiasi dispositivo connesso ad Internet semplicemente accedendo al server con il nostro account, metodo che ci permette di salvare memoria preziosa e rende inoltre disponibili i dati anche su altri dispositivi, posiamo anche utilizzare il Cloud come backup dei nostri dati più importanti, ma anche se sulla carta il Cloud presenta diversi vantaggi, non sono comunque esente da svantaggi, prima di tutto per poter salvare o recuperare i dati dal server è necessario l’accesso ad Internet inoltre a meno che non stiamo utilizzando la linea di casa, praticamente ogni gestore mobile o Wi-Fi gratuito pongono dei limiti alla mole dei dati trasferibili tramite la connessione, ad esempio molti gestori mobile permettono un traffico di 1GB o 2GB di dati mensili, come somma di tutto il traffico in entrata e in uscita, dobbiamo poi considerare anche la velocità della nostra connessione ad Internet, ma soprattutto anche la velocità del server, che non di rado potrebbe rappresentare un non indifferente collo di bottiglia e rendere quindi il recupero o il caricamento di files di grosse dimensione decisamente lento, inoltre dobbiamo comunque sempre tenere in mente che si tratta comunque di server remoti e che quindi siamo comunque legati ad essi e anche se dal punto di vista della privacy sono sicuri in quanto i principali servizi di Clouding criptano i nostri dati, comunque siamo sempre in balia delle sorti dei server, se ci dovesse quindi essere un malfunzionamento o un problema tecnico, i nostri dati sarebbero inaccessibili fintanto che i server sono offline, inoltre se i server per qualche motivo dovessero chiudere, perderemmo anche i nostri dati. 9 CAPITOLO 2 Introduzione alla programmazione Android e Java 2.1 Perché Android? Come abbiamo potuto vedere nel capitolo precedente, il mercato degli smartphone e dei tablet è diviso tra IOS, Windows e Android, è benché l’architettura dei processori montati nei dispositivi sia comunque la stessa indipendentemente dal sistema operativo installato, comunque le applicazioni compilate non sono compatibili con altri sistemi operativi se non con quello per cui è stata compilata l’applicazione, un po’ quello che accade anche sui pc tradizionali, in quanto un programma compilato ad esempio per Windows, non è compatibile per OSX o Linux e viceversa, ed anche se i sorgenti dei software sono comunque gli stessi la differenza è data dalla compilazione, ovvero la traduzione da parte del compilatore del codice sorgente scritto in linguaggio di programmazione al linguaggio macchina. Per quanto riguarda i linguaggi di programmazione più comunemente usati abbiamo il C# o C++ su Windows RT, Objective C su IOS e Java su Android. Per quanto riguarda la produzione di applicazioni anche solo ad uso personale, la nostra scelta ricadrà sicuramente su Android, poiché è il sistema meno rigido, infatti per poter anche solo installare un applicazione su IOS dovremmo comunque firmarla con un account da sviluppatore che ha un costo annuo di 79€ ma che ci permette anche di pubblicare l’App sullo store, ma dobbiamo comunque rispettare le linee guida di Apple per poter firmare l’applicazione, Microsoft è meno rigida, e ci permette di installare App non firmate a patto di installare una licenza da sviluppatore gratuita sul dispositivo della durata di 30 giorni, la licenza può essere rinnovata gratuitamente e senza limiti, ma Microsoft comunque controllerà l’utilizzo che faremo della nostra licenza, inoltre se vogliamo pubblicare e firmare la nostra App avremmo bisogno di un account da sviluppatore dal costo di 37€ annui per i privati, Android ha invece le politiche meno rigide, in quanto possiamo installare anche applicazioni esterne allo store semplicemente disattivando un impostazione di sicurezza, accorgimento necessario solo in fase di installazione, per distribuire le App sullo store però avremmo comunque bisogno di un account da sviluppatore, dal costo di 25$ una tantum. 10 2.2 L’ambiente di sviluppo ed il linguaggio Per creare la nostra prima App Android, avremmo bisogno di due cose, un ambiente di sviluppo e una conoscenza anche di base del linguaggio di programmazione. Il linguaggio di programmazione principalmente utilizzato in ambito Android è il linguaggio Java, mentre i due principali ambienti di sviluppo sono Eclipse ed Android Studio, esiste anche un alternativa, ovvero Corona SDK un ambiente di sviluppo semplificato, disponibile in versione starter gratuitamente o in versioni più complete, pagando un canone mensile, Corona SDK utilizza un suo linguaggio di programmazione chiamato Luna, e la versione per MAC consente anche la creazione di App IOS, ma comunque non mi soffermerò oltre su Corona SDK in quanto prenderemo in analisi Eclipse ed il linguaggio Java. Prima abbiamo menzionato due ambienti di sviluppo Java, Android Studio ed Eclipse, e a dire il vero non ci sono molte differenze tra i due ambienti di sviluppo, la principale differenza sta nel fatto che Android Studio è un ambiente di sviluppo dedicato solo ed esclusivamente alla programmazione Android, mentre Eclipse è un ambiente di sviluppo Java completo, che però necessita di un plug-in detto ADT (Android Devolpement Tools) che ci permetterà di creare ed esportare applicazioni per Android, come già detto in precedenza non fa molta differenza scegliere l’uno o l’altro ambiente di sviluppo, in quanto analoghi, comunque sia io mi riferirò sempre ad Eclipse, in quanto ambiente di sviluppo Java completo ed anche se non realizzeremo alcuna applicazione che non sia per Android, con Eclipse avremo comunque la possibilità di poter sviluppare applicazioni Java al di fuori del mondo Android. Spendiamo ora qualche parola su Java, esso è un linguaggio di programmazione molto diffuso sviluppato dalla Oracle, quasi tutto il software di apparecchi di uso comune quali ad esempio navigatori GPS, decoder e televisori stessi è scritto in Java, questo perché Java è un linguaggio estremamente portabile, nel paragrafo precedete abbiamo menzionato il compilatore, ovvero il software che traduce il linguaggio di programmazione ad alto livello in linguaggio macchina, ma con il Java la situazione è leggermente diversa, in quanto il compilatore compila il sorgente Java non in linguaggio macchina, ma in Bytecode, ovvero un linguaggio intermedio tra il linguaggio di programmazione ad alto livello ed il linguaggio macchina, il dispositivo su cui verrà eseguito il programma deve avere installato un software detto Java Virtual Machine o JVM, che ha il compito o di interpretare il Bytecode o di compilarlo al volo al momento dell’esecuzione con un compilatore Just-In-Time, il 11 compilatore Just-In-Time è sicuramente un metodo più efficiente dell’interprete, ma comunque meno efficiente di un linguaggio propriamente compilato, ma con il vantaggio però di rendere le applicazioni estremamente portabili in quanto vengono compilate al momento dell’esecuzione e quindi sono meno hardware dipendenti, ciò rende quindi il Java il linguaggio forse più diffuso al mondo, un motivo in più quindi per iniziare ad approcciarsi con Eclipse ed il Java. 2.3 Installiamo l’SDK Prima di poter iniziare la nostra avventura nel mondo del Java e della programmazione per Android, dobbiamo prima procurarci alcuni software gratuiti ma indispensabili, prima di tutto abbiamo bisogno del Java Devolpement Kit o JDK, reperibile gratuitamente sul sito dell’Oracle al seguente indirizzo: http://www.oracle.com/technetwork/java/javase/downloads/jdk7-downloads-1880260.html E scaricare la versione del JDK relativa al sistema operativo del computer da noi utilizzato, dopo aver installato il JDK, avremmo bisogno anche di un ambiente di sviluppo o SDK, qui la scelta è tra Android Studio, reperibile al seguente indirizzo: https://developer.android.com/sdk/installing/studio.html ed Eclipse, reperibile invece qui: http://www.eclipse.org/downloads/ i due ambienti di sviluppo sono molto simili quindi non ci dovrebbero essere grandi differenze, comunque sia prenderemo in analisi Eclipse. Ora se avete scelto Eclipse come vostro SDK, prima di poter iniziare a creare App per Android, dovremmo procurarci anche il plug-in Android Devolpement Tools o ADT che può comunque essere scaricato ed installato direttamente da Eclipse, avviando Eclipse e andando su Help>Install New Software… comparirà una finestra su cui dovremmo cliccare su Add in alto a destra, come indirizzo inseriremo https://dl-ssl.google.com/android/eclipse/ confermiamo ed installiamo l’Android Devolpement Tools dalla lista, se invece abbiamo optato per Android Studio, possiamo ignorare quest’ultimo passaggio in quanto Android Studio è un SDK pensato esclusivamente per lo sviluppo di applicazioni Android, quindi non necessita di plug-in aggiuntivi, comunque sia anche se il linguaggio Java rimane comunque lo stesso, come pure la struttura di un programma Android, d’ora in avanti onde evitare confusione ci riferiremo solo ad Eclipse, comunque sia ciò non preclude l’uso di Android Studio, in quanto molto simile ad Eclipse. 12 2.4 La nostra prima App Ora che abbiamo a disposizione tutto il necessario, non ci resta che creare la nostra prima applicazione. Prima di tutto avviamo Eclipse e se non lo abbiamo già fatto definiamo il nostro Workspace, ovvero la cartella in cui Eclipse andrà a salvare tutti i files relativi ai nostri progetti. Dopo aver avviato il programma e definito il nostro Workspace, clicchiamo su File > New > Other e selezionate Android > Android Application Project, vi apparirà la seguente finestra: Application Name – Sarà il nome dell’applicazione, quindi il nome che apparirà sotto l’icona dell’applicazione una volta installata, possiamo scegliere qualsiasi nome, Eclipse comunque ci darà un warning se iniziamo con una lettera minuscola. Project Name – Sarà invece il nome del progetto che verrà salvato, non influisce in alcun modo sull’applicazione finale in quanto è un parametro relativo solo alla fase di programmazione, comunque deve essere diverso dal nome dei nostri altri progetti. 13 Package Name: – È invece il nome relativo al pacchetto, ed è comunque il vero identificativo dell’applicazione, quindi deve essere univoco per ogni applicazione, l’installazione di un’applicazione con lo stesso Package Name, verrà vista da Android come un aggiornamento e quindi sovrascriverà l’altra applicazione, la sintassi è di solito com.nomeazienda.nomeapp ma può anche essere a.b oppure a.b.c.d.e dove ad ogni lettera corrisponde una parola. Minimum Required SDK: – La versione minima di Android necessaria per eseguire il Software, impostare un valore più basso migliora la compatibilità, ma riduce le funzionalità, Eclipse consiglia di impostare su Froyo (API 8) per rendere l’applicazione compatibile anche con i vecchi device nulla però vieta di impostare un livello minimo più alto se si devono utilizzare funzionalità non supportate dai vecchi API. Target SDK: – La versione di Android di riferimento, cioè la versione per cui è pensato il nostro software, dovrebbe essere la versione su cui andremo poi a testare l’applicazione, quindi quella del nostro smartphone, tablet o del nostro emulatore, Eclipse suggerisce l’utilizzo dell’ultima versione disponibile, ma non è comunque necessario. Compile With: – La versione dell’SDK che utilizzeremo come compilatore, se non selezionabile dovremmo prima installarne una dall’SDK Manager, consiglio comunque di installare l’ultima versione disponibile, in quanto ha più funzionalità e può comunque essere usata anche per compilare applicazioni destinate a sistemi più vecchi. Dopo aver inserito tutte le informazioni vi verrà chiesto se creare un activity ed un icona personalizzata, possiamo premere direttamente Next> in quanto le caselle da spuntare saranno già spuntate. Verrà poi proposta la creazione di un’icona personalizzata, possiamo anche lasciare l’icona predefinita, ma è comunque sempre meglio creare un icona personalizzata, come icona possiamo utilizzare una qualsiasi immagine, ma comunque consiglio di utilizzare un immagine PNG a 32Bit con trasparenza per avere la miglior qualità grafica, ma si può anche utilizzare un icona predefinita cliccando su Clipart oppure utilizzare un testo. Dopo aver creato l’icona dovremmo creare anche la prima activity, ossia la prima pagina della nostra applicazione, verranno proposti tre tipi di activity, scegliamo Blank activity, andando avanti ci ritroveremo la seguente finestra… 14 Activity Name – È il nome della nostra activity, deve essere diverso dal nome delle altre activity del progetto o di altre Classi Java presenti nel progetto in quanto un activity è a tutti gli effetti una Classe Java, il nostro progetto può avere anche più di un activity, ma deve avere almeno un activity, questa sarà l’activity che verrà avviata al lancio dell’applicazione. Layout Name – Il nome del layout collegato all’activity il layout è la parte grafica dell’activity, il suo nome è importante solo ai fini della programmazione e non verrà mostrato all’utente finale. Fragment Layout Name – Il nome del fragment del layout, il fragment è una parte del layout introdotta con l’API 11 Honeycomb, come per il layout dobbiamo definirne un nome, in alternativa come per il nome del layout anche il nome del fragment non verrà mai mostrato all’utente finale. 15 Navigation Type – Il tipo di navigazione utilizzato nell’activity, lasciamolo pure su none. Il Progetto ora dovrebbe risultare così: Src – è la cartella in è contenuto il pacchetto (il Package Name che abbiamo scelto) con all’interno le varie Activity e Classi, per ora dovremmo avere solo la nostra prima Activity. Res – Contiene invece tutte le risorse grafiche dell’applicazione e non solo, si divide a sua volta in: \\ Drawable – Contiene le icone e la grafica dell’applicazione, si tratta in realtà di cinque cartelle diverse (hdpi, ldpi, mdpi, xhdpi, xxhdpi) che contengono le stesse immagini a risoluzioni diverse se disponibili, volendo possiamo anche mettere un immagine in una sola cartella, e verrà utilizzata per tutte le risoluzioni. \\ Layout – Contiene i layout e i fragment del progetto. \\ Menù – Contiene invece i dati relativi ai menù delle varie activity, di norma richiamabili con il tasto menù. \\ Raw – Non presente di base, andrà creato manualmente se necessario, ha funzioni simili a drawable, ma non è diviso per risoluzioni, qui possiamo mettere le immagini con una sola risoluzione, anche se possiamo comunque metterle in una cartella drawable senza problemi. \\Values – Contiene invece tutti i valori relativi alle stringhe ai numeri interi e alle dimensioni collegate alle relative parole chiave, utile soprattutto per localizzare le applicazioni in più lingue. AndroidManifest.xml – Forse il file più importante, contiene tutte le informazioni relative all’applicazione, tra cui nome, versione, API minimo, activity principale ed altro, se vogliamo cambiare uno dei valori inseriti alla creazione dell’applicazione, possiamo farlo qui. Ora se abbiamo fatto tutto e stiamo utilizzando una versione recente dell’SDK, andando ad aprire il file Java della nostra activity, che possiamo trovare nella cartella src, dentro il nostro pacchetto, che in questo caso abbiamo chiamato com.test.test, 16 con il nome che abbiamo dato alla nostra activity seguito da .java, nel nostro caso MainActivity.java, dovremmo trovarci dinanzi al seguente codice auto generato: package com.test.test; import import import import import import import import import import android.support.v7.app.ActionBarActivity; android.support.v7.app.ActionBar; android.support.v4.app.Fragment; android.os.Bundle; android.view.LayoutInflater; android.view.Menu; android.view.MenuItem; android.view.View; android.view.ViewGroup; android.os.Build; 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 PlaceholderFragment()) .commit(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } /** * A placeholder fragment containing a simple view. */ public static class PlaceholderFragment extends Fragment { public PlaceholderFragment() { } 17 @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main, container, false); return rootView; } } } Può sembrare complicato, ma analizzando brevemente le parti più importanti avremo un quadro generale della situazione, iniziamo con la prima parte del codice, ovvero il package e gli import: package com.test.test; import import import import import import import import import import android.support.v7.app.ActionBarActivity; android.support.v7.app.ActionBar; android.support.v4.app.Fragment; android.os.Bundle; android.view.LayoutInflater; android.view.Menu; android.view.MenuItem; android.view.View; android.view.ViewGroup; android.os.Build; Package è il nostro PackageName, in questo caso com.test.test, da notare il ; alla fine di ogni istruzione, componente vitale della sintassi Java, in quanto indica il termine di un istruzione, infatti potremmo anche scrivere due o più istruzioni sulla stessa riga, separate solo dal ; e non avremmo comunque problemi, ma omettere il ; porterà ad un errore. Dopo il package, abbiamo gli import, ovvero l’elenco di tutti gli oggetti che andremo ad utilizzare nel nostro progetto, trattandosi di una programmazione ad oggetti infatti, dovremmo andare ad importare ogni oggetto che andremo ad utilizzare, sia esso una casella di testo, un pulsante, un immagine od un semplice testo, più avanti analizzeremo più nel dettaglio i vari oggetti, per ora Eclipse ha importato solo gli oggetti essenziali. Andiamo ora ad esaminare il cuore del codice ovvero la classe: 18 public class MainActivity extends ActionBarActivity { // Dichiarazione Variabili @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .add(R.id.container, new PlaceholderFragment()) .commit(); } // Codice } … } Ho aggiunto due annotazioni, le annotazioni in Java sono precedute da // oppure contenute tra /* e */ la prima annotazione è // Dichiarazione Variabili, ovvero è la zona in cui dovremmo andare a dichiarare le nostre variabili, ma ne parleremo dopo, mentre // Codice indica la zona in cui dovremmo andare a scrivere il grosso del nostro codice, ora analizziamo brevemente il codice auto generato. public class MainActivity extends ActionBarActivity { Questa è la definizione della nostra classe, ovvero la nostra activity, extends ActionBarActivity invece indica che la classe estende un'altra classe ovvero la classe dell’action bar, ossia la barra che troviamo tra la barra delle notifiche e l’applicazione vera e proprio, infine abbiamo { che racchiude tutto il codice della classe fino all’ultimo } preceduto da … ovvero il resto del codice abbiamo poi : @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .add(R.id.container, new PlaceholderFragment()) .commit(); } // Codice } Questa parte va a definire tutto ciò che accade all’avvio dell’activity, ovvero onCreate , prima di tutto abbiamo @Override, ovvero andremo ad eseguire un override del metodo onCreate della classe madre ActionBarActivity e non un overloading, è buona prassi utilizzare @Override prima di un metodo contenuto in una classe che estende un’altra classe, salvo nei casi in cui l’overloading sia voluto. 19 Protected definisce l’accessibilità del metodo, e ne riparleremo quando parleremo della definizione delle variabili, void invece indica che il metodo non restituirà alcun risultato, in questa porzione di codice si fa riferimento ad alcuni metodi, questi non sono istruzioni Java di base, ma metodi della classe madre ActionBarActivity, che è appunto la classe che siamo andati ad estendere all’inizio, ed è proprio l’estendere la classe ActionBarActivity che fa della nostra classe un activity Android, permettendoci quindi di andare a richiamare i metodi di tale classe, altro metodo molto importante è setContentView, che ci permette di richiamare il layout xml della nostra activity in questo caso activity_main.xml, ora possiamo notare la sintassi per il richiamo di una risorsa, ovvero R.cartella.risorsa senza estensione, in questo caso quindi R.layout.main_activity, il tutto tra parentesi in quanto si tratta di un parametro del metodo, più avanti entreremo maggiormente nel dettaglio per quanto riguarda i metodi, infine abbiamo un costrutto o statement, ovvero l’if, che analizzeremo ben presto nel dettaglio in quanto parte importantissima di tutti i linguaggi di programmazione. Ora sempre all’interno del metodo onCreate, dopo aver generato la View, possiamo scrivere il codice della nostra prima activity, ma non prima di aver analizzato il resto del codice auto generato, ed aver imparato almeno alcune basi di programmazione. Dopo aver chiuso il metodo onCreate, subito dopo la parentesi graffa } che chiude onCreate, possiamo andare a definire i nostri metodi, ma andiamo prima ad analizzare il resto del codice: @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.action_settings) { // Codice return true; } return super.onOptionsItemSelected(item); } Questa parte è invece relativa al menù che verrà visualizzato alla pressione del tasto menù del nostro dispositivo, i files relativi ai menù, sono presenti in res\menu, ogni activity ha il suo file di menù, ma nulla vieta di condividere lo stesso file con più activity, ciò può essere fatto andando a modificare: getMenuInflater().inflate(R.menu.main, menu); 20 Sostituendo R.menu.main, con il file di menù desiderato. Questa parte di codice contiene l’override di alcuni metodi della classe madre, noi non stiamo quindi creando un metodo da zero, ma stiamo facendo l’override di metodi ereditati dalla classe madre, ecco perché è vitale estendere la classe ActionBarActivity, in alternativa sarebbe anche possibile estendere la classe Activity, ma ciò non porterebbe alcun vantaggio, la seconda parte del codice: @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.action_settings) { // Codice return true; } return super.onOptionsItemSelected(item); } Indica invece cosa accade al click di un opzione, le opzioni possono essere create nel relativo file di menù, di default abbiamo l’opzione action_settings, ma possiamo crearne di altre, dove ho annotato // Codice, dobbiamo inserire il codice che vogliamo eseguire al click sulla voce del menù, se vogliamo inserire altre voci, dobbiamo aggiungere degli else if relativi alle nuove voci, ad esempio: if (id == R.id.action_settings) { // Codice 1 return true; } else if (id == R.id.codice2) { // Codice 2 return true; } else if (id == R.id.codice3) { // Codice 3 return true; } Così facendo selezionando diverse opzioni verranno eseguiti diversi codici, infine abbiamo la parte relativa al fragment: public static class PlaceholderFragment extends Fragment { public PlaceholderFragment() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main, container, false); return rootView; } } Per ora possiamo ignorarla. 21 2.5 Il Layout e i Fragments Dopo esserci fatti un idea di come funzioni la classe di un activity, andiamo ad analizzare la parte grafica dell’activity, ovvero il layout. Andando in res\layout troveremo due file xml auto generati, uno per il layout base ed uno per il fragment, con i nomi che gli abbiamo assegnato in fase di creazione, se non abbiamo modificato nulla, di default dovrebbero essere activity_main.xml e fragment_main.xml, il fragment, avrà al suo interno una TextView, ossia un testo semplice, con scritto Hello World, naturalmente prima di creare una qualsiasi applicazione dovremmo eliminarla, ora andando ad aprire il file xml, ci ritroveremo difronte ad un interfaccia diversa da quella relativa al file java. Interfaccia tipica della programmazione ad oggetti, sulla sinistra abbiamo i vari oggetti, al centro l’output grafico vero e proprio mentre a destra abbiamo in alto i vari oggetti inseriti ed in basso le proprietà dell’oggetto selezionato, inserire oggetti nel layout, non ne richiede l’importazione nell’activity, ma è comunque necessaria se l’activity va in qualche modo ad interagire con tali oggetti, quindi se inseriamo delle TextView solo allo scopo di visualizzare una testo, ma la nostra activity non interagirà mai con esse, non dobbiamo importare l’oggetto TextView, ma se invece dobbiamo in qualche modo interagire con esso, allora andrà obbligatoriamente importato, gli oggetti vanno importati solamente una volta, per cui anche se abbiamo più di una TextView, noi importeremo solamente una volta il widget. Anche se Android ci propone un Layout principale ed un fragment, non è detto che noi non possiamo creare più fragments, i fragment sono molto importanti, ma il tutto potrebbe complicare il codice, o in alcuni casi anche semplificarlo, per ora lavoriamo sul layout principale, in seguito introdurremo i fragments. Analizziamo ora i principali oggetti che abbiamo a disposizione: 22 TextView – Un semplice testo, l’utente non ha interazione diretta con esso, ma questo non significa che l’activity non possa andare a modificare tale testo o interagire con esso, ed è infatti pratica comune. EditText – A differenza della TextView, si tratta di una casella di input, in cui l’utente può inserire un testo o in alternativa un numero o una password, bisogna però ricordare che indipendentemente dal tipo di input, l’EditText restituirà sempre un valore di tipo String, anche se inseriremo un numero, al click sulla casella apparirà una tastiera virtuale, se non disponibile una tastiera fisica. Button – Il classico pulsante, per poterlo utilizzare abbiamo bisogno di impostare un OnClickListener, che ci permetterà di eseguire del codice alla pressione del tasto. ImageButton – Uguale a Button, ma con un immagine al posto del testo. ImageView – Un immagine, può essere utilizzata anche come pulsante personalizzato con un OnClickListener. Ma ne abbiamo molte altre ancora. Ora, ci troviamo dinnanzi a due file xml molto simili, activity_main e fragment_main, ma dove andare a lavorare? È buona pratica andare a lavorare su fragment_main, tenendo però in mente che il codice che andremo a scrivere nella classe MainActivity, è relativo ad activity_main, mentre il codice relativo a fragment_main è legato alla classe PlaceholderFragment autogenerata, ora se sia che andiamo ad aggiungere oggetti nel activity_main, che nel fragment_main, essi verranno tutti visualizzati contemporaneamente all’esecuzione del programma, questo perché il programma carica il layout activity_main e poi successivamente aggiunge nel FrameLayout container il fragment PlaceholderFragment, questo avviene grazie a questa parte di codice autogenerata: if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .add(R.id.container, new PlaceholderFragment()) .commit(); } Infatti se andiamo ad aprire activity_main, noteremo che è presente un solo oggetto, ovvero un FrameLayout con l’id container, il FrameLayout è un layout elementare, di norma utilizzato per contenere un solo oggetto, in questo caso il fragment, l’id invece è l’identificativo della risorsa, tramite il quale la potremmo richiamare, in questo caso con R.id.container, naturalmente per richiamare il fragment abbiamo bisogno di creare una classe relativa ad esso, Eclipse come sempre ci viene incontro creandocene una in automatico, ovvero la classe PlaceholderFragment, cioè la seguente: 23 public static class PlaceholderFragment extends Fragment { // Dichiarazione Variabili public PlaceholderFragment() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main, container, false); // Codice Fragment return rootView; } // Metodi } Come possiamo notare, la classe PlaceholderFragment è molto simile alla classe MainActivity, con la differenza che essa estende la classe Fragment, e non la classe ActionBarActivity, e sempre come per MainActivity, abbiamo una zona dove definire le variabili, una zona dove inserire il nostro codice ed una zona dove poter creare i nostri metodi, lavorare con i fragment, ci permetterà di avere diverse schermate nella stessa Activity, altrimenti dovremmo iniziare una nuova Activity ogni volta che vogliamo cambiare schermata, inoltre l’uso dei fragments ci permette di creare con più facilità applicazioni compatibili sia con smartphone che con tablet, in quanto sui tablet potremmo visualizzare più fragments insieme, date le maggiori dimensioni dello schermo. 2.6 La dichiarazione delle variabili In informatica un elemento fondamentale è dato proprio dalla dichiarazione delle variabili, e tale pratica è presente in qualsiasi linguaggio di programmazione, in quanto le variabili sono componenti essenziali di qualsiasi applicazione. La dichiarazione di una variabile può essere fatta in due modi, come variabile globale all’inizio della classe, oppure come variabile locale al momento dell’utilizzo, un esempio di variabile globale: public class MainActivity extends ActionBarActivity { private int a = 0; … 24 Una variabile globale è una variabile dichiarata all’inizio della classe ed è quindi valida in tutta la classe, una variabile locale invece è una variabile dichiarata all’interno della classe e quindi valida solo dopo l’esecuzione di tale parte di codice, una variabile locale segue le stesse regole delle variabili globali, cambia solo la posizione, altro punto da tenere in considerazione, è la visibilità delle variabili, ovvero la prima parte della dichiarazione, nel nostro caso private, esistono quattro tipi di visibilità: Private, default, protected e public. private – Una variabile private, è visibile solo all’interno della classe in cui viene definita. default – A differenza delle altre visibilità, default non va specificato, è la visibilità che viene assegnata omettendo la dichiarazione della visibilità, default rende visibile la variabile a tutte le classi contenute nel pacchetto, ma non all’esterno di esso, un esempio di dichiarazione di variabile default è: int a = 0; e non default int a = 0; protected – Rende la variabile visibile a tutte le classi all’interno del pacchetto e a tutte le sottoclassi, anche se esterne al pacchetto. public – Rende la variabile visibile a tutte le classi, interne o esterne al pacchetto. Le stesse regole di visibilità si applicano anche alla definizione delle classi o dei metodi e non solo alle variabili. Dopo aver definito la visibilità, dobbiamo definire il tipo di variabile, ossia i dati che essa può contenere, i principali tipi di variabili sono: boolean – Variabile di tipo booleano, può assumere solamente i valori true o false. byte – Variabile lunga 8bit, può contenere valori interi compresi tra -128 e 127 inclusi, variabile elementare, che ci permette di salvare spazio se abbiamo a che fare con valori piccoli, inoltre non supporta operazioni matematiche tra variabili, supporta però ++ e -- es: byte a = 5; byte b = a + 5; // Errore a++; //Ok a = 6 a--; //Ok a = 5 short – Variabile lunga 16bit, può contenere valori interi compresi tra -32768 e 32767 compresi, come byte non supporta operazioni tra variabili. int – Variabile lunga 32bit, è la variabile più comunemente utilizzata per quanto riguarda i numeri interi, può contenere valori compresi tra -231 e 231-1 compresi, 25 supporta le operazioni tra variabili, purché non coinvolgano variabili a virgola mobile o variabili non numeriche, ne operazioni che coinvolgano numeri non interi, ad esempio: byte a = 10; float b = 5; int c; c c c c = = = = a a a a + * * / b; // Errore b è una variabile float 5; // Ok c = 50 0.5 // Errore 0.5 non è un numero intero 3 // Ok 3 è un numero intero c verrà approssimato c = 3 long – Variabile lunga 64bit può contenere valori interi compresi tra -263 e 263-1 compresi, segue le stesse regole di int. float – Variabile a virgola mobile lunga 32bit, serve a contenere numeri naturali, quindi anche numeri decimali, supporta ogni genere di operazione. double – Variabile a virgola mobile lunga 64bit, offre più precisione delle variabili float, indicata per valori con molte cifre decimali, come float supporta ogni genere di operazione. char – Variabile lunga 16bit, destinata a contenere un singolo carattere Unicode, un carattere va definito tra virgolette singole, ad esempio: char a = 'a'; CharSequence – Variabile utilizzata per contenere una sequenza di caratteri, e quindi un testo, il testo va definito tra doppie virgolette, ad esempio: CharSequence t = "test"; String – Simile a CharSequence, ma un po’ meno flessibile, una variabile String non accetta valori CharSequence, mentre una variabile CharSequence accetta valori String ad esempio: String t = "test"; CharSequence a = t; //Ok Ma non: CharSequence t = "test"; String a = t; //Errore Impossibile convertire CharSequence in String Le variabili possono anche contenere oggetti o classi, ad esempio: TextView Text; //Oggetto TextView PlaceholderFragment fragment = new PlaceholderFragment(); //Classe PlaceholderFragment 26 È molto importante definire le variabili in modo corretto, ed evitare che esse possano andare a dover contenere valori non supportati, altrimenti incorreremmo in un buffer overflow, ovvero stiamo cercando di contenere all’interno di una variabile una valore fuori dalla gamma di valori supportati ad esempio: byte a = 128; // Errore Buffer Overflow Java però gestisce bene gli Overflow, ciclando i valori, quindi ad esempio non incorreremmo in buffer overflow facendo: byte a = 127; a++; //Ok a = -128 Inoltre Eclipse, quando nota un errore, blocca la compilazione dell’eseguibile, per cui è difficile creare applicazioni che incorrano in questi tipi di errori, ma questo non significa che Eclipse identifichi tutti gli errori. Le variabili possono anche essere lasciate vuote, oppure definite a blocchi es: private int a, b, c, d; // Non inizializzate protected int e = 1, f = 2, g = 3; // Inizializzate Oltre alle normali variabili, possiamo definire anche costanti, array e variabili statiche. Variabili statiche – Le variabili statiche sono definite dal modificatore static, ad esempio: protected static int stat = 0; Una variabile statica non è una constante, ma una variabile unica, di cui ci può essere una sola istanza, per cui se caricassimo più istanze della stessa classe la nostra variabile esisterebbe soltanto una volta es: public static class test { public static int stat = 0; // Variabile Statica public int nor = 0; // Variabile non Statica } public class test2 { private final test Test1 = new test(); // Prima istanza classe test private final test Test2= new test(); // Seconda istanza classe test public test2(){ Test1.nor = 1; Test2.nor = 2; Test1.stat = 3; Test2.stat = 4; CharSequence b = ("Valori: " + Test1.nor + " " + Test2.nor + " " + Test1.stat + " " + Test2.stat ); // b = "Valori: 1 2 4 4" } } 27 Come possiamo notare le due istanze della classe test hanno restituito due valori distinti per la variabile nor (1 e 2), ma un solo valore per la variabile stat (4), in quanto essendo essa statica può esistere solamente una volta, quindi la seconda istanza ha sovrascritto la prima, per cui il valore 3 è stato sovrascritto dal valore 4, inoltre Eclipse ci darà dei warning, in quanto le variabili statiche andrebbero richiamate in modo statico, e non tramite un istanza, per cui il modo più corretto di lavorare con una variabile statica è il seguente: test.stat = 4; // classe.variabile Le variabili statiche quindi sono anche più facili da richiamare, in quanto possono essere richiamate in modo statico, senza quindi creare istanze, static non si applica solo alle variabili, ma anche ai metodi e alle classi, le variabili locali non possono essere definite static, ma solo le variabili globali. Costanti – Le costanti, non sono vere e proprie variabili, in quanto non possono variare, possono essere meglio definiti come dei riferimenti ad alcuni dati ricorrenti, ma che non possono mutare all’interno del codice, le costanti si definiscono con il modificatore final, e in caso di costanti globali è anche possibile aggiungere il modificatore static, ci sono però alcune differenze tra una costante final ed una final static, la costante final static è la costante vera a propria, deve essere inizializzata alla dichiarazione con un valore costante, e non può avere altri valori, se non quello definito durante la dichiarazione ad esempio: public final static int KB = 1024; // Costante, KB varrà sempre 1024 Una costante final invece è leggermente diversa, in quanto può anche non essere inizializzata, purché venga comunque inizializzata una sola volta e non lasciata vuota, può essere anche inizializzata tramite una variabile ad esempio: public final int KB; // Costante non inizializzata int a = 256, b = 4; KB = a * b; // Costante inizializzata correttamente tramite due variabili KB = 1024; // Errore KB è stato già inizializzato Una costante non static quindi lascia un po’ più di libertà, ma se dobbiamo definire una costante globale inizializzata in fase di dichiarazione, conviene dichiararla final static e non semplicemente final, in caso di costanti locali, è permesso solo final, è consuetudine scrivere i nomi delle costanti completamente in maiuscolo, ma non è obbligatorio, serve solo a rendere le costanti più riconoscibili, final può essere usato anche per definire una classe o un metodo, una classe final non può avere sottoclassi e quindi non può essere estesa, e non è possibile fare l’override di un metodo final. Array – Gli array sono particolari variabili, o anche costanti, che possono contenere più informazioni, un po’ come se fossero un insieme di variabili, gli array sono 28 probabilmente il tipo di variabile più complicato, ma anche uno dei più utili, gli array possono essere mono dimensionali o multi dimensionali, gli ultimi li possiamo vedere come delle matrici, gli array possono essere utilizzati per contenere valori che altrimenti necessiterebbero di più variabili ad esempio: int a = 1, int[] ar = int tot1 = int tot2 = b = 2, c = 3; {1, 2, 3}; // a + b + c; // ar[0] + ar[1] // Definizione di tre variabili distinte Definizione di un array monodimensionale con tre valori tot1 = 6 + ar[2]; //tot2 = 6 Utilizzando un array, abbiamo ottenuto lo stesso risultato che abbiamo ottenuto utilizzando tre variabili distinte, e benché i vantaggi in questo semplice esempio non saltino all’occhio, gli array sono molto utili soprattutto nei cicli, inoltre possiamo scorrere gli array tramite una variabile, ad esempio: int a = 1, b = 2, c = 3; int[] ar = {1, 2, 3}; int tot = ar[a-1] + ar[b-1] + ar[c-1]; //tot = 6 Il fatto di poter selezionare i valori degli array tramite variabili, ci permette di creare codici che restituiscano valori diversi in base agli input dell’utente, cosa che con le variabili normali sarebbe molto più difficile, in quanto necessiterebbe di costrutti. Gli array possono anche non essere inizializzati, oppure possono esserne definite solo le dimensioni ad esempio: int[] a; // Array non inizializzato int[] b = new int [3]; // Array che conterrà tre valori, anche se ancora vuoto Gli array possono essere utilizzati anche per tutti i tipi di variabili e costanti es: final static int[] A = {1, 2, 3}; // Array costante Button[] b = new Button [3]; // Array che può contenere tre Button Oltre agli array monodimensionali esistono anche gli array multidimensionali, gli array multidimensionali sono più complessi, e permettono di utilizzare quindi più variabili, per accedere ai suoi dati, ad esempio: int Riga = 2, Colonna = 3, Casella = 5, Valore = 9; // Definiamo quattro variabili int[][][] Sudoku = new int [3][3][9]; // Definiamo l’array Sudoku Sudoku[Riga][Colonna][Casella] = Valore; // Tramite le variabili, assegniamo il valore Ora immaginando di ricevere i valori di Riga, Colonna , Casella e Valore in input dall’utente, tramite un array multidimensionale ci risulterà facile assegnare il valore alle giuste coordinate, senza usare un array, avremmo dovuto definire 81 variabili diverse al posto dell’array, inoltre trovare e modificare la variabile corretta ricevendo da input i dati dall’utente risulterebbe quasi impossibile, o perlomeno estremamente complicato. 29 Gli array multidimensionali, come quelli semplici, possono essere inizializzati e possono essere utilizzati anche per le costanti, ad esempio: final static int[][][] A = {{{1,2},{3,4}},{{5,6},{7,8}},{{9,10},{11,12}}}; // Array costante int[3][2][2] int [][] b = {{1,2,3,4},{5,6,7,8},{9,10,11,12}}; // Array int[3][4] Ricordandoci sempre che gli array partono da zero, se noi andassimo recuperare i valori dell’array otterremmo ad esempio: int c = A[2][0][1]; // c = 10 int d = b[1][3]; // d = 8 Naturalmente, possiamo avere anche più di tre dimensioni, ma naturalmente più dimensioni ha l’array e più complicata sarà l’inizializzazione, c’è da dire però che non è obbligatorio inizializzare gli array in fase di dichiarazione, inoltre possiamo anche solo definirne la dimensione, per poi riempirlo in seguito. Gli array possono anche essere irregolari, ad esempio: int [][] a int [][] b b[0] = new b[1] = new int [][] c = {{1,2,3,4},{5,6,7},{8,9,10,11,12}}; // Ok = new int [2][]; // Ok int[4]; // Ok, l’array ha il seguente formato {0,1,2,3},{} int[3]; // Ok, l’array ha il seguente formato {0,1,2,3},{0,1,2} = new int [][2]; // Errore 2.7 I costrutti (statements) Altro aspetto comune a tutti i linguaggi di programmazione sono i costrutti o statements, essi sono infatti componenti vitali di tutte le nostre applicazioni, gli statements ci permettono di far variare il nostro codice al variare ad esempio delle variabili, esistono due tipi di statements, lo statement if e lo statement switch: if : È lo statement più comune, ed esegue il codice al suo interno solamente se una o più condizioni sono vere, ad esempio: int a = 5; if (a > 3){ a = 3; // L’istruzione verrà eseguita solo se a > 3 } In questo caso la condizione è vera, essendo a = 5 e quindi maggiore di 3, è possibile anche prevedere più condizioni o gruppi di condizioni ad esempio: int a = 5, b = 6; if ((a > 3 && b > 3) || b == 10){ a = 3; // Le istruzioni verranno eseguite solo se a > 3 e b > 3 oppure b == 10 b = 3; } 30 In questo caso abbiamo più condizioni, il codice verrà eseguito solo se sia a che b, sono maggiori di 3, ma verrà comunque sempre eseguito se b == 10, anche se a <= 3, da notare che per la condizione di uguaglianza abbiamo usato == e non = , infatti = serve ad assegnare un valore ad una variabile, mentre == serve a controllare l’uguaglianza, una condizione utilizza degli operatori logici, per restituire un valore booleano, mentre lo statement if richiede un valore booleano true per essere eseguito, per determinare le condizioni vengono utilizzati i seguenti operatori logici: && || == < <= > >= != ! in alternativa possono anche essere utilizzati gli operatori bitwise & | ^, tra cui l’operatore XOR ^ non presente tra gli operatori logici, gli operatori sono i seguenti: AND && Restituisce un valore true solo se tutte le condizioni sono vere altrimenti restituisce false, è possibile anche utilizzare l’operatore bitwise & ad esempio: boolean bol = true && true && true; // bol = true, tutte le condizioni sono vere bol = true && true && false; // bol = false, non tutte le condizioni sono vere bol = true & true; // bol = true, l’operatore bitwise & viene considerato come && OR || Restituisce una valore true se almeno una delle condizioni è vera, se tutte sono false restituisce false, è possibile utilizzare anche l’operatore bitwise | ad esempio: boolean bol = true || true || false; // bol = true, almeno una condizione è vera bol = false || false || false; // bol = false, tutte le condizioni sono false bol = false || ( true && false); // bol = false, entrambe le condizioni sono false bol = true | false; // bol = true, l’operatore bitwise | viene considerato come || XOR ^ Non esiste un operatore logico XOR, ma possiamo comunque utilizzare l’operatore bitwise XOR ^, XOR restituisce un valore true solo se una sola delle condizioni sono vere, se più di una condizione è vera, oppure tutte le condizioni sono false restituirà false, ad esempio: boolean bol bol = false bol = false bol = (true = ^ ^ & true ^ true ^ false; // bol = false, più di una condizione è vera false ^ false; // bol = false, tutte le condizioni sono false true ^ false); // bol = true, solo una condizione è vera false) ^ ( true | false); // bol = true, solo una delle condizioni è vera EQUAL == Restituisce true se i due valori sono uguali, altrimenti restituisce false, può essere usata su qualsiasi tipo di valore, sia esso booleano, numerico o stringa: String str = "Stringa"; int a = 5; boolean bol = a == 6 // bol = false, a diverso da 6 bol = bol == false; // bol = true, bol era falso bol = str == "stringa"; // bol = false "stringa" diverso da "Stringa” bol = str == "Stringa"; // bol = true "Stringa" uguale a "Stringa" bol = a == 5 && str == "Stringa"; // bol = true LOWER THAN < Restituisce true se il primo valore è minore del secondo, altrimenti restituisce false, esempio: 31 int a = boolean bol = a bol = a 5; bol = a < 6 // bol = true, a minore di 6 < 4; // bol = false, a maggiore di 4 < 5; // bol = false a uguale a 5 non minore LOWER THAN OR EQUAL TO <= Restituisce true se il primo valore è minore o uguale al secondo, altrimenti restituisce false, esempio: int a = boolean bol = a bol = a 5; bol = a <= 6 // bol = true, a minore di 6 <= 4; // bol = false, a maggiore di 4 <= 5; // bol = true a uguale a 5 GREATER THAN > Restituisce true se il primo valore è maggiore del secondo, altrimenti restituisce false, esempio: int a = boolean bol = a bol = a 5; bol = a > 6 // bol = false, a minore di 6 > 4; // bol = true, a maggiore di 4 > 5; // bol = false a uguale a 5 non maggiore GREATER THAN OR EQUAL TO >= Restituisce true se il primo valore è maggiore o uguale al secondo, altrimenti restituisce false, esempio: int a = boolean bol = a bol = a 5; bol = a => 6 // bol = false, a minore di 6 => 4; // bol = true, a maggiore di 4 => 5; // bol = true a uguale a 5 NOT EQUAL != Restituisce true se i due valori sono diversi, restituisce false se invece sono uguali, esempio: int a = 5; boolean bol = a != 6 // bol = true, a diverso da 6 bol = a != 5; // bol = false, a uguale a 5 NOT ! Operatore booleano, restituisce true se la condizione è falsa e restituisce false se la condizione è vera, ad esempio: int a = 5; boolean bol = !(a => 6) // bol = true, la condizione è falsa bol = a != 4; // bol = false, la condizione è vera bol = !false; // bol = true bol = !bol; // bol = false, bol era true bol = !a; // Errore a non è una variabile booleana Come abbiamo già visto, possiamo anche combinare più operatori e gruppi di condizioni per creare condizioni complesse, come ad esempio: 32 if (a == 5 && (b != 5 || c < 6)) /* in questo caso la condizione è vera se la prima condizione è vera e almeno una delle altre due condizioni è vera */ Peculiarità dello statement if, è quella di poter prevedere condizioni alternative, oppure quella di eseguire un codice diverso in caso la condizione non venga rispettata, ciò è possibile attraverso l’utilizzo di else if e/o else. else if – Può essere utilizzato per prevedere l’esecuzione di un codice alternativo, se non dovessero venir rispettate le condizioni precedenti, a patto di rispettare una nuova condizione, è possibile utilizzare anche più di un else if, esempio: if (condizione1){ //Codice 1 }else if(condizione2){ //Codice 2 }else if(condizione3){ //Codice 3 } Nel sopracitato esempio, verrà eseguito il Codice 1 solo se condizione1 è vera, altrimenti verrà controllata condizione2, e se dovesse risultare vera verrà invece eseguito il Codice 2, se invece dovesse risultare falsa, si controllerà la condizione3, e se dovesse risultare vera verrà eseguito invece il Codice 3, se invece dovesse essere anche essa falsa, non verrà eseguito alcun codice, se una condizione dovesse risultare vera, verrà eseguito solo il relativo codice e vengono ignorate le condizioni successive, ad esempio se condizione2 e condizione3 sono vere, mentre condizione1 è falsa, verrà eseguito solo Codice 2, se vogliamo eseguire tutti i codici per cui le condizioni siano state rispettate dobbiamo utilizzare invece diversi if statement, es: if (condizione1){ //Codice 1 }if(condizione2){ //Codice 2 }if(condizione3){ //Codice 3 } In questo caso vengono sempre controllate tutte e tre le condizioni ed eseguito il codice di ogni statement che vede le proprie condizioni soddisfatte, ricordandoci quindi che si tratta di tre statements diversi però, a differenza dell’esempio precedente, in cui invece avevamo un solo statement più complesso, se sostituissimo il terzo if con un else if avremmo allora due statements e non più tre, in quanto solo if è uno statement e non else if, nel qual caso la condizione3 sarebbe stata controllata solo se la condizione2 fosse risultata falsa, ma anche se la condizione1 fosse risultata vera, in quanto parte di uno statement differente. 33 else – Da non confondere con else if, else infatti non prevede alcuna condizione, e più essere inserito solo alla fine dello statement, quindi sempre dopo gli eventuali else if, a differenza di else if, è permesso un solo else per statement, il codice di else verrà eseguito solo se tutte le condizioni dello statement fossero risultate insoddisfatte, es: if (condizione1){ //Codice 1 }else if(condizione2){ //Codice 2 }else{ //Codice 3 } In questo caso viene prima controllata la condizione1, se falsa viene invece controllata la condizione2, se falsa anche essa viene eseguito il Codice 3, naturalmente è anche possibile utilizzare più else if prima dell’else, oppure non utilizzarne alcuno, purché else sia sempre nella parte finale dello statement, e non vi sia più di un else per statement. È anche possibile inserire statements negli statement ad esempio: if (condizione1){ // Codice 1 if (condizione2){ // Codice 2 }else if (condizione3){ // Codice 3 if (condizione4){ // Codice 4 } } }else{ //Codice 5 } In questo esempio più complesso, se la condizione1 dovesse risultare vera verrà eseguito il Codice 1 e controllata la condizione2, se dovesse risultare anche essa vera verrà eseguito anche il Codice 2 altrimenti verrà controllata la condizione3 e se dovesse risultare vera verrà eseguito anche il Codice 3 e verrà controllata anche la condizione4, che se dovesse risultare anche essa vera, farà eseguire anche il Codice 4, se invece la condizione1 dovesse risultare falsa verrà eseguito invece solo il Codice 5 per cui le possibilità di esecuzione in questo caso sono: (Cod1) ^ (Cod1 & Cod2) ^ (Cod1 & Cod3) ^ (Cod1 & Cod3 & Cod4) ^ (Cod5) Questo ci permette di creare del codice che possa reagire in maniera sempre diversa, a seconda delle condizioni che vengono rispettate, se uniamo questo al fatto che per 34 ogni statement è possibile definire un numero illimitato di condizioni, allora le possibilità di sviluppo diventano infinite. Come abbiamo detto prima però if non è l’unico statement, ne esiste infatti un altro, ovvero lo statement switch. switch: Si tratta di uno statement abbastanza differente dallo statement if, esso infatti non prevede delle condizioni, ma una variabile e dei casi, la sitassi è la seguente: switch (variabile){ case valore1: // Codice 1 case valore2: // Codice 2 case valore3: // Codice 3 } A differenza di if, qui abbiamo una variabile, che può essere di qualsiasi tipo, anche di tipo String, definita questa variabile, vengono poi definiti dei casi, ad ogni caso viene confrontato il valore della variabile con quello del caso, e se coincidono, verrà eseguito il codice del caso in esame, e di tutti i casi successivi, per cui se: variabile variabile variabile variabile = = = = valore1; valore2; valore3; altro; // // // // Vengono eseguiti Codice 1, Codice 2 e Codice 3 Vengono eseguiti Codice 2 e Codice 3 Viene eseguito solo Codice 3 Non viene eseguito nulla È anche possibile definire un default, ovvero un caso valido quando tutti gli altri casi non sono validi, ad esempio: switch (variabile){ case valore1: // Se variabile = valore1 vengono eseguiti Codice 1, Codice 2 e Codice 3 // Codice 1 case valore2: // Se variabile = valore2 vengono eseguiti Codice 2 e Codice 3 // Codice 2 default: // se variabile = altro viene eseguito solo Codice 3 // Codice 3 } È possibile anche bloccare l’esecuzione dei casi in certi punti, utilizzando break, es: switch (variabile){ case valore1: // Se // Codice 1 break; case valore2: // Se // Codice 2 case valore3: // Se // Codice 3 break; default: // se // Codice 4 } variabile = valore1 viene eseguito solo Codice 1 variabile = valore2 vengono eseguiti Codice 2 e Codice 3 variabile = valore3 viene eseguito solo Codice 3 variabile = altro viene eseguito solo Codice 4 35 I case non devono avere per forza del codice definito, questo ci permette di eseguire lo stesso codice per più case, ad esempio: switch (variabile){ case valore1: // Se variabile = valore1 viene eseguito solo Codice 1 // Codice 1 break; case valore2: case valore3: // Se variabile = valore2 oppure valore3 viene eseguito solo Codice 2 // Codice 2 break; default: // se variabile = altro viene eseguito solo Codice 3 // Codice 3 } In questo modo possiamo definire una moltitudine di casi con codice comune, codice che comunque potremmo scrivere anche utilizzando if in questo modo: if (variabile == valore1){ // Codice 1 } else if (variabile == valore2 || variabile == valore3){ // Codice 2 } else { // Codice 3 } Questo codice ha le stesse funzioni del codice precedente, per cui molti switch statement, possono essere convertiti in if statement, ma non il contrario, inoltre non tutti gli switch statement possono essere convertiti facilmente in if, non senza ripetizioni, ad esempio: switch (variabile){ case valore1: // Codice 1 break; case valore2: // Codice 2 case valore3: // Codice 3 break; default: // Codice 4 } Potrebbe essere tradotto in: if (variabile == valore1){ // Codice 1 } else if (variabile == valore2){ // Codice 2 // Codice 3 } else if (variabile == valore3){ // Codice 3 Ripetizione } else { // Codice 4 36 } In questo caso onde evitare di ripetere la scrittura di Codice 3, sarebbe più opportuno utilizzare lo switch statement invece dell’if statement. Altra cosa molto importante da dire per quanto riguarda lo switch statement, è il fatto che per i vari casi accetta solamente valori di tipo costante, e non variabile, inoltre non sono accettate ripetizioni, ad esempio: int variabile = 0, var = 0; final int cost = 1; switch (variabile){ case var: // Errore, le variabili non sono accettate // Codice 1 break; case cost: // Ok, cost è dichiarato final, quindi è accettato // Codice 2 break; case 1: // Errore, cost vale 1 quindi è già presente // Codice 3 break; case 2: // Ok, 2 è un valore costante e non è già presente // Codice 4 break; default: // Codice 5 } In sostanza quindi possiamo affermare che lo switch statement, sia più limitato dell’if statement, ma in alcuni casi potrebbe essere preferibile. 2.8 I cicli Un altro cardine dell’informatica è dato dai cicli, anche essi presenti in tutti i linguaggi di programmazione, essi ci permettono di ripetere delle operazioni un numero definito di volte, oppure affinché una o più condizioni vengano rispettate. Esistono due tipi di cicli in Java, il ciclo for ed il ciclo while, che comprende anche la variante do while. Il ciclo for – Il ciclo for, si utilizza principalmente quando si vuole eseguire una determinata porzione di codice, un numero definito di volte, la sintassi è la seguente: for (variabile = base; variabile < massimo; variabile += incremento){ // Codice } In questo caso variabile, partirà dal valore base, ed incrementerà di incremento ad ogni esecuzione del ciclo, finché non avrà raggiunto come valore massimo, quindi ad esempio: 37 int variabile, i = 0; final int base = 2, massimo = 10, incremento = 2; int[] numeripari = new int[massimo/2]; for (variabile = base, variabile <= massimo, variabile += incremento){ numeripari[i] = variabile; i++; } // alla fine del ciclo, numeripari[] varrà: 2, 4, 6, 8, 10 In questo caso il ciclo andrà a riempire un array, con tutti i numeri pari compresi tra 2 e 10, naturalmente possiamo anche sostituire alle costanti che abbiamo definito delle variabili, oppure non definire alcuna costante e scrivere ad esempio: for (variabile = 2, variabile <= 10, variabile += 2){ /* Codice */ } Inoltre è anche possibile eseguire altre operazioni oltre agli incrementi, purché si stia attenti a non creare cicli infiniti, come ad esempio: for (variabile = 2, variabile <= 10, i += 2){ } //Errore variabile sarà sempre 2 Il ciclo while – Ciclo che permette l’esecuzione di una porzione di codice fintanto che una condizione risulta vera, ha una struttura molto simile agli if statement, la sitassi è la seguente: while (condizione){ // Codice } Bisogna naturalmente ricordarsi di introdurre una parte di codice che possa modificare la veridicità della condizione, altrimenti avremo un ciclo infinito. Un esempio di ciclo while: int a = 0, b = 9; int[][] c = new int [4]; while (a < 5 && b > 5){ c[a][0] = a; // c[a][0] varrà 0,1,2,3 c[a][1] = b; // c[a][1] varrà 9,8,7,6 a++; b--; } In questo caso abbiamo due condizioni da rispettare, perché il ciclo continui, quando una delle due diviene falsa il ciclo si interrompe, in questo caso b > 5, anche se a < 5, dato che il ciclo terminerà con b = 5, a = 4, ma avremmo potuto utilizzare anche l’operatore || or e quindi il ciclo sarebbe continuato fino a che almeno una delle condizioni fosse stata vera, e quindi avremmo concluso il ciclo con b = 4, a = 5, esiste anche una variante del ciclo while, ovvero il ciclo do while, simile al ciclo while, ma con la differenza che prima viene eseguito il codice, e poi viene controllata la condizione, per cui il codice verrà eseguito sempre almeno una volta, e poi eventualmente verrà ripetuto se la condizione dovesse risultare vera, la sintassi è: 38 do { // Codice } while(condizione); In questo caso, il codice verrà eseguito e poi eventualmente ripetuto fintanto che condizione è vera, il codice verrà eseguito comunque almeno una volta, anche se condizione dovesse essere falsa già prima dell’esecuzione del ciclo, ad esempio: int a = 5; do { a++; // il codice viene eseguito almeno una volta } while(a <= 4); // a = 6, il ciclo non viene ripetuto do { a--; // il codice viene eseguito almeno una volta } while(a > 0); // il ciclo viene ripetuto finché a <= 0 Nel primo caso a è già maggiore di 4 in quanto è stata inizializzata a 5, ma comunque il codice viene eseguito almeno una volta, in quanto siamo in un ciclo do while e la condizione viene controllata alla fine, quindi a diventa 6, viene eseguito il codice del secondo ciclo e a diventa 5, la condizione è a > 0, quindi è rispettata e il codice viene eseguito di nuovo e quindi abbiamo a = 4, quindi ancora > 0, il codice viene eseguito finché a scende a 0 e quindi la condizione diventa falsa. Come per gli statements, anche i cicli possono contenere al loro interno altri cicli, anche di diverso tipo, ad esempio: int a, b = 0; int[][] c = new int [10][10]; for (a = 1; a <= 10; a++){ while (b < 10){ b++; c[a-1][b-1] = a*b; } } In questo caso abbiamo un ciclo while all’interno di un ciclo for, l’intero ciclo ci restituirà l’array c contenente una tavola pitagorica, dove c[a][b] = (a+1)*(b+1). Altra peculiarità dei cicli è quella di poter essere interrotti in toto o in parte, tramite le istruzioni break e continue, inoltre i cicli possono anche essere etichettati, questo ci servirà in caso di cicli contenenti sotto cicli, in quanto le istruzioni break e continue contenute in un sotto ciclo, possono essere riferite anche ai cicli superiori, se sono stati etichettati, la sintassi per etichettare un ciclo è la seguente: int a, b = 0, c = 0; etichetta1: for (a = 0; a < 10; a++){ etichetta2: while (b < 10){ b++ etichetta3: do{ c++; }while(c < 10); } } 39 L’etichetta va prima dell’inizio del ciclo, ed ha valore solo all’interno del ciclo, come detto in precedenza lo scopo delle etichette, è quello di poter riferire le istruzioni break e continue contenute nei sotto cicli, anche ai cicli superiori, ma analizziamo prima le istruzioni break e continue. break – l’istruzione break, l’abbiamo già trovata nel costrutto switch, e serviva ad interrompere l’esecuzione del costrutto, ed ha la stessa funzione anche all’interno di un ciclo, break, interrompe l’esecuzione del ciclo ed esce da esso, se contenuto all’interno di un sotto ciclo, break si riferisce al sotto ciclo in cui è contenuto, break può anche essere seguito da un etichetta, in tal caso si riferirà invece al ciclo superiore a cui fa riferimento l’etichetta, considerando che le etichette sono visibili solo all’interno dei cicli, quindi break non potrà riferirsi ad un ciclo separato dal ciclo in cui si trova break, inoltre break non può riferirsi ad un ciclo più interno alla sua posizione, ecco alcuni esempi: int a = 0, b, c = 0; while (a < 5) if(a == 0){ break; // Il ciclo si interrompe qui, a++ non viene eseguito } a++; } C1: while (a < 5){ C2: for(b = 0; b < 5; b++){ if (a == 1){ break; // Si esce dal ciclo C2, continua il ciclo C1, a++ viene eseguito, c++ no } else if (a == 2) { break C1; //Si esce dal ciclo C1, a++ e c++ non vengono eseguiti } c++; } a++; } //alla fine del ciclo a = 2, b = 0, c = 5 In questo caso a parte con valore 0, si esce direttamente dal primo ciclo, in quanto se a = 0 eseguiamo un break, successivamente il secondo ciclo, C1 viene eseguito, e viene eseguito anche il sotto ciclo C2, alla fine del primo loop abbiamo a = 1, b = 5, c = 5, inizia il secondo loop, siccome a = 1, viene eseguito break all’interno di C2, quindi si esce da C2 e viene eseguito solo il resto di C1, quindi c++ viene saltato e alla fine del secondo loop abbiamo: a = 2, b = 0, c = 5, inizia il terzo loop, viene eseguito nuovamente il sotto ciclo c2, ma questa volta a = 2, quindi viene eseguito break C1, quindi si esce non dal sotto ciclo C2, come con il break precedente, ma si esce dal ciclo superiore C1, quindi il ciclo termina prima di eseguire a++ e di conseguenza il ciclo termina con i seguenti valori, a = 2, b = 0, c = 5, ricordandoci comunque che le etichette valgono solo all’interno del loro ciclo, per cui se avessimo: 40 int a = 0, b = 0; C1: while (a < 10){ b = 0; C2: while (b < 5){ b++; if (a == 5){ break C2; // Ok, all’interno di C2 }else if (a == 8){ break C1; // Ok, all’interno di C1 } } break C2; // Errore, all’esterno del ciclo C2 } In questo caso abbiamo un break C2, ed un break C1 all’interno del sotto ciclo C2, essendo quindi C2 all’interno di C1, entrambi i break sono validi, in quanto siamo sia all’interno di C1 che di C2, inoltre per il break C2, avremmo potuto omettere anche l’etichetta, in quanto il ciclo più prossimo all’istruzione è proprio il ciclo C2, il secondo break C2 invece ci restituirà un errore, in quanto si trova all’esterno di C2, quindi non può riferirsi ad esso, ma solo a C1, in quanto interno solo ad esso. continue – Segue le stesse regole di break, ma ha una diversa funzione, continue infatti termina il loop attuale, ma non esce dal ciclo, per cui se la condizione per il loop è ancora soddisfatta esegue un nuovo loop, altrimenti esce dal ciclo, come di norma, anche continue può essere seguito da un etichetta e segue le stesse regole sintattiche di break, con la differenza che continue non può essere usato negli switch statement, ma solo nei cicli, vediamo un esempio: int a = 0, b = 0; int[] arr = {0, 0, 0}; C1: while (a < 5){ a++ b = 0; C2: while (b < 5){ b++; arr[0]++; if (a == 2){ continue; // arr[1]++ non viene eseguito. }else if (a > 2){ continue C1;} // arr[1]++ e arr[2]++ non vengono eseguiti, si esce dal ciclo C2. arr[1]++; } arr[2]++; } // alla fine del ciclo arr[] varrà 25, 5, 2 In questo caso il continue, non ci fa uscire dal ciclo, come faceva il break, ma ci fa semplicemente ricominciare il ciclo, come se il codice al suo interno fosse terminato, quindi si rivaluta la condizione e se vera, si esegue normalmente il ciclo, altrimenti si esce dal ciclo come di consueto, nel caso di continue C1 invece, semplicemente si ricomincia il ciclo C1, quindi di conseguenza si esce dal sotto ciclo C2, questo però non equivale da un break C2 però, in quanto non viene eseguito nemmeno il resto del 41 ciclo C1, quindi non viene eseguito arr[2]++, se avessimo avuto un break C2, arr[2]++ sarebbe stato eseguito, per cui alla fine abbiamo arr[0] = 25, in quanto arr[0]++ si trovava all’interno del sotto ciclo, prima dei continue, per cui viene eseguito tutte e 25 le volte che viene eseguito il ciclo C2, poi abbiamo arr[1] = 5, questo perché arr[1]++ si trova all’interno del sotto ciclo C2, ma dopo i continue, quindi viene eseguito solo fintanto che a < 2, quindi viene eseguito solo i primi 5 cicli di C2, quindi solo durante il primo ciclo di C1, infine abbiamo arr[2] = 2, questo in quanto arr[2]++ si trova all’esterno del sotto ciclo C2 e dopo il continue C1, quindi viene eseguito solo fintanto che a <= 2, quindi solo durante i primi 2 cicli di C1. Altra cosa di cui tener conto è che sia break che continue dovrebbero essere inseriti solo alla fine di costrutti, dato che includere un continue o un break fissi all’interno di un ciclo non avrebbe molto senso, inoltre inserire un break o un continue fissi, prima della fine del ciclo ci restituirà un errore come ad esempio: int a = 0; while (a < 5){ if (a == 1){ a++; continue; // Ok, alla fine dello Statement. }else if (a == 2){ continue; // Errore, non alla fine dello statement a++; // Errore, Unreachable Code } break; // Errore, non alla fine del ciclo a++; //Errore, Unreachable Code } a = 0; while (a < 5){ a++; break; // Ok, ma il ciclo non verrà mai eseguito più di una volta } while (a < 5){ a++; continue; // Ok, ma completamente inutile } Inserire un break alla fine di un ciclo while, farà comportare il ciclo esattamente come un if statement, ossia verrà eseguito una volta se la condizione è vera o nessuna se la condizione è falsa, inserire invece un continue alla fine di un ciclo invece è completamente inutile, in quanto non sortirebbe alcun effetto, dato che alla fine del ciclo viene comunque ricominciato il ciclo controllando la condizione, mentre precedere una parte di codice con un continue od un break fissi, e quindi inevitabili, ci darà come errore: Unreachable Code, in quanto sarà impossibile eseguire quella parte di codice, dato che verrà sempre inevitabilmente saltata. 42 2.9 Le classi Le classi, sono una parta essenziale del linguaggio Java, un programma Java, è strutturato in pacchetti che contengono le varie classi, le classi sono vere e proprie parti di codice. Esistono vari tipi di classi, le classi normali, final, static, abstract e le interfacce. Classi normali – Sono il tipo di classe più comune, una classe può estendere, una sola classe, e se non ne estende alcuna, estenderà la classe Object di default, mentre potrà implementare un qualsiasi numero di interfacce, estendere una classe, significa diventare una sottoclasse, e quindi avere accesso a tutti i metodi e le variabili a noi visibili, inoltre ci permette di eseguire l’ovverride dei metodi ereditati, o di riferirci alla classe madre tramite l’istruzione super, una classe può avere al suo interno uno o più costruttori, una classe normale può essere istanziata tramite l’istruzione new classe( ), vediamo un esempio di classe normale: public class Classe1{ //Classe madre o Super Classe int b, c; public Classe1(int a){ //Costruttore Classe1 b = a; c = 5; } } public class Classe2 extends Classe1{ // Classe figlia o Sotto Classe int c; public Classe2(){ //Costruttore di default Classe2 super(9); // Inizializzazione del costruttore della classe madre c = b; // c = 9 in quanto b = a e a è dato dal costruttore inizializzato con 9 b = 8; // b non è stato definito in Classe2 quindi ci riferiamo a b di Classe1 super.c = 4; // c è stato definito, quindi per riferirci al c di Classe1 usiamo super } // Alla fine le variabili valgono… Classe1: b = 8, c = 4 Classe2: c = 9 } Nell esempio abbiamo due classi, una classe madre o superclasse (Classe1), ed una classe figlia o sotto classe (Classe2), la Classe2 diventa sotto classe di Classe1 nel momento in cui la estende, da notare che si è comunque all’esterno della classe madre, quindi sono accessibili solamente le risorse visibili, quindi non potremmo accedere ad una variabile o ad un metodo dichiarato private, o comunque non visibile, inoltre le classi per essere istanziate necessitano di un costruttore, le classi possono avere anche più di un costruttore, oppure nessun costruttore se non devono essere istanziate, esistono tre tipi di costruttore: Il costruttore normale: È il tipo più comune di costruttore, un normale costruttore, con dei parametri, che dovremmo definire nel momento in cui andremo ad istanziare la classe, un esempio di costruttore normale è il seguente: 43 public class Classe1{ int i; CharSequence cs; public Classe1(int a, CharSequence b){ // Costruttore Normale i = a; cs = b; } } public class Classe2{ int i2; CharSequence cs2; Classe1 c1 = new Classe1(1,"Istanza 1"); // Istanziamo la classe Classe1 c2 = new Classe1(2,"Istanza 2"); // Seconda istanza public Classe2(){ i2 = c1.i; // Prendiamo le variabili dalla classe istanziata cs2 = c2.cs; } // Alla fine avremo: i2 = 1, cs2 = "Istanza 2" } Grazie al costruttore possiamo istanziare Classe1 all’interno di Classe2 e non solo, possiamo anche istanziarla più volte con parametri diversi, ogni istanza può essere associata ad una variabile del tipo della classe istanziata, nel caso volessimo istanziare la classe Classe1, la variabile sarebbe dichiarata col tipo Classe1, nel caso una classe madre, dovesse non avere alcun costruttore di default, quindi senza parametri, saremmo costretti ad inizializzare il costruttore della classe madre, all’inizio di ogni costruttore di tutte le sotto classi, con l’istruzione super(parametri). Il costruttore di default: Anche esso molto comune, si tratta di un costruttore senza parametri, che ci permette di istanziare una classe, senza passarle alcun parametro, in una classe vi possono essere più costruttore, purché con parametri diversi, siccome il costruttore di default non ha parametri, è permesso un solo costruttore di default per ogni classe, però è comunque possibile avere un costruttore di default insieme ad altri costruttori diversi, la classe verrà istanziata alla stessa maniera di un normale costruttore, ma senza parametri: public class Classe1{ int a; CharSequence cs; public Classe1(){ // Costruttore di default a = 5; } } public class Classe2{ int i, i2; Classe1 c1 = new Classe1(); // Istanziamo la classe public Classe2(){ i = c1.a; c1.a = 10; i2 = c1.a; } // Alla fine avremo: i = 5, i2 = 10 } 44 L’esempio è simile al precedente, ma in questo caso non abbiamo inserito alcun parametro, possiamo comunque andare a cambiare il valore di una variabile dei una determinata istanza, ma se essa non è stata definita static, interesserà solamente quell’istanza, se una classe madre ha un costruttore default, non dovremmo inizializzare obbligatoriamente il costruttore con super, ma possiamo comunque utilizzarlo per inizializzare altri costruttori della classe, ma possiamo utilizzarlo anche solo in alcuni costruttori o sottoclassi, in assenza di super, verrà inizializzato il costruttore di default. Il costruttore di copia: Si tratta sicuramente del costruttore più raro, il costruttore di copia è un costruttore che ha come parametro un istanza di una classe, o anche della classe stessa, nulla vieta di specificare altri parametri, tra cui anche istanze di altre classi, un esempio di costruttore di copia è il seguente: public class Classe1{ int i; public Classe1(int a){ // Costruttore normale i = a; } public Classe1(Classe1 cls1){ // Costruttore copia i = cls1.i * -1; } public class Classe2{ int i1, i2; Classe1 c1 = new Classe1(3); // Istanziamo la classe Classe1 c2 = new Classe1(c1); // Istanziamo il costruttore copia public Classe2(){ i1 = c1.i; i2 = c2.i; } // Alla fine avremo: i1 = 3, i2 = -3 } Come abbiamo potuto osservare nell’esempio, il costruttore copia utilizza come parametro un istanza della stessa classe, per cui noi prima dobbiamo istanziare la classe tramite il costruttore normale, e poi passare i parametri al costruttore copia, questo ci può servire in quanto non vi possono essere due costruttori con gli stessi parametri, inoltre ci permette di utilizzare i parametri di più classi già istanziate, per cui ci può sempre tornare utile. Classi final – Le classi final sono molto simili alle normali classi, ma con la differenza che non possono essere estese, e quindi non possono diventare super classi ed avere quindi sotto classi, ma possono però diventare sotto classi loro stesse, inoltre possono comunque essere istanziate normalmente come le normali classi oppure, per dichiarare una classe final basta aggiungere il modificatore final, una classe final va dichiarata nel seguente modo: public final class nomeclasse{ } 45 Le classi final seguono tutte le regole delle normali classi, e nonostante non possono essere estese, possono comunque estendere altre classi ed essere implementate normalmente, il modificatore final, serve solo ed esclusivamente ad impedire l’estensione della classe, un esempio pratico: public final class Classefinal extends Classenonfinal { // Ok, può estendere una classe int kb = 0; public Classefinal(int a){ kb = a * 1024; } Public class Classe1 extends Classefinal{ // Errore, Classefinal non può essere estesa } Public class Classe2{ Classefinal cf = new Classefinal(2); // Ok, Classefinal può essere istanziata int i; Public Classe2(){ i = cf.kb; // i = 2048 } Classi static (Nested class) – Possono essere definite static solamente le nested class, ovvero le classi contenute all’interno di altre classi, dette outer class, da non confondere però con le sotto classi in quanto non sono la stessa cosa, un nested class infatti non estende automaticamente la classe in cui è contenuta, ma ha comunque accesso alle variabili definite private, in quanto comunque visibili all’interno della classe, una nested class, può essere definita static, una nested class non static, è detta invece inner class, infatti non può avere al suo interno metodi o variabili static, una nested class static invece può avere variabili e metodi sia static che non static, inoltre una nested class static non può richiamare direttamente metodi o variabili non static, senza istanziare l’outer class, un inner-class invece può richiamare direttamente metodi e variabili non static dell’outer class, inoltre non è possibile istanziare inner class, senza istanziare prima l’outer class, inoltre non è possibile istanziare inner class all’esterno dell’outer class mentre possiamo istanziare una nested class static direttamente ed anche all’esterno dell’outer class. Ecco alcuni esempi: public class Outer{ //Non può essere dichiarata static int i; static int = s; public Outer(){ i = 1; s = 2; } public class Inner{ // Inner Class int i1, s1; // Ok int static s0; // Errore le inner class non possono avere variabili static. public Inner(){ i1 = i; // Ok le inner class possono accedere in modo statico alle variabili dell’outer s1 = s; Nested nest = new Nested(5);//Ok, le nested class possono essere istanziate normalmente i1 = nest.i2; } 46 public static class Nested{ // Nested Class int i2; // Ok int static s2; // Ok la classe è static public Nested(int a){ i2 = i + a; // Errore le nested, devono istanziare l’outer per le variabili non static i2 = new Outer().i; // Ok, Outer è stato istanziato s2 = s; // Ok s è static } public Nested(){ Inner inn0 = new Inner(); // Errore, bisogna istanziare anche l’outer class; Inner inn1 = new Outer().new Inner(); // OK, siamo all’interno di Outer; i2 = inn1.s1; } } // Chiudiamo la classe Outer import pkg.name.Outer.Nest // Importiamo la classe Nest public class Esterna{ public Esterna(){ Nested nest0 = new Nested(2); // Ok, se imporiamo la classe, altrimenti errore Outer.Nested nest1 = new Outer.Nested(8); // Ok, non è necessario l’import Inner inn0 = new Inner(); // Errore, siamo all’esterno dell’outer class Inner inn1 = new Outer().new Inner(); // Errore, siamo all’esterno dell’outer class } } Per cui se abbiamo bisogno di istanziare la classe all’esterno dell’outer class, oppure abbiamo bisogno di variabili e metodi statici all’interno della classe, ne faremo una nested class, dichiarandola static, altrimenti se ci servirà solo all’interno dell’outer class, e non contiene ne variabili, ne metodi statici, possiamo farne invece un inner class. Classi astratte – Le classi astratte, sono un particolare tipo di classe, definito tramite il modificatore abstract, le classi abstract, possono essere viste come l’opposto delle classi final, dato che non possono essere istanziate direttamente, ma solamente estese, al contrario delle classi final, che possono essere solo istanziate, ma non estese, una classe abstract, può contenere porzioni di codice, e nonostante non possa essere inizializzata, può comunque avere dei costruttori, in quanto può essere inizializzata dalla sotto classe tramite super( ), le classi astratte possono anche contenere metodi normali, o astratti, una classe contenente un metodo astratto deve essere per forza di cose definita astratta, parleremo poi dei metodi, per ora diremo solo che un metodo astratto è un metodo non implementato, che deve obbligatoriamente essere implementato dalle sotto classi, ecco un esempio di classe astratta: public abstract class Astratta{ // Definiamo la classe abstract int i = 0; public Astratta(int a){ // Creiamo un costruttore, anche se non può essere istanziata i = a; } } 47 public class Figlia extends Astratta{ //Estendiamo la classe Astratta public Figlia(){ super(8); // Inizializziamo il costruttore Astratta Astr = new Astratta(4); // Errore, Astratta non può essere istanziata int a = i // a = 8; } } Possiamo utilizzare le classi astratte per due motivi, il primo, è quello di creare classi che non possono essere istanziate, ma solo estese, il secondo è quello di creare classi con metodi astratti, esistono due tipi di classi astratte, le normali classi astratte, e le classi astratte pure, le classi astratte pure, contengono solo metodi astratti, di cui parleremo a breve. Le interfacce - Sono un particolare tipo di classe, molto simile alle classi astratte pure, le interfacce infatti, non sono normali classi, non possono essere ne estese ne direttamente istanziate, ma solo implementate, inoltre non possono contenere ne codice, ne costruttori, ma solo metodi astratti, i metodi astratti contenuti all’interno delle interfacce, non devono essere definiti abstract, in quanto sottointeso, poiché le interfacce possono contenere solo metodi abstract, l’implementazione di un interfaccia, è molto simile all’estensione di una classe abstract pura, ma con la differenza, che non c’è limite al numero di classi che possono essere implementate, mentre è possibile estendere una sola classe, inoltre è possibile implementare una o più interfacce anche se è già stata estesa una classe, solo le interfacce possono essere implementate, e non le classi, la sintassi dell’implementazione è la seguente: public class Classe extends Madre implements Interfaccia1, Interfaccia2 { Mentre un esempio di interfaccia è il seguente: public interface Interfaccia1{ int a = 0; // Viene considerata costante static final, anche se non definita tale public int metodo1(int b); // Metodo Astratto } Le interfacce possono contenere solamente costanti e metodi astratti, per cui definire una variabile, ci porterà alla definizione di una costante static final, anche omettendo uno o entrambi i modificatori, i metodi verranno definiti astratti anche omettendo il modificatore abstract, inoltre le interfacce non possono avere costruttori, ne codice differente dalla definizione di costanti e metodi abstract, le interfacce sono un ottima alternativa alle classi abstract pure, in quanto possono essere implementate senza alcun limite, a differenza delle classi astratte, che vanno invece estese, le interfacce sono comunque più limitate delle normali classi astratte, e possono essere viste più come uno scheletro, in quanto definiscono solamente metodi abstract, per definizione, 48 una classe che implementa un interfaccia, deve definire obbligatoriamente tutti i metodi dell’interfaccia, come deve anche definire tutti i metodi astratti della super classe astratta, le interfacce per cui hanno lo scopo di obbligare le classi a definire tutti i metodi in esse contenute, anche se tali metodi non essendo definiti, non hanno ancora utilità, le interfacce quindi sono come delle linee guida, ma nulla di più, ci permettono quindi di passare costanti alle classi che le implementano, oppure definire quali metodi le classi che le implementano devono avere, un esempio di interfaccia: public interface Convertitore { /** Unità di Misura */ double Centimetri = 1, Pollici = 0.393700787, Metri = 0.01, Piedi = 0.032808399; /** Metodo da implementare */ public double Converter(double base, double unità1, double unità2); } public class Classe1 implements Convertitore { /** 0 - Centimetri | 1 - Pollici | 2 - Metri | 3 - Piedi*/ private static final double[] Unità = {Centimetri,Pollici,Metri,Piedi}; public static double Risultato; public Classe1(double base, int a, int b){ if (a > 3 || b > 3 || a < 0 || b < 0){ Risultato = 0; }else{ Risultato = Converter(base, Unità[a], Unità[b]) } } @Override //Metodo implementato public double Converter(double base, double u1, double u2){ return base/u1*u2; } In questo esempio, abbiamo un interfaccia con alcune costanti di tipo double, e un metodo astratto, con dei parametri, la classe che implementa l’interfaccia, può richiamare le costanti in essa contenute, per cui ad esempio avremmo potuto scrivere ad esempio: double m = base * Metri; Invece di: double m = base * 0.01; Per cui, se ad esempio il rapporto tra centimetri e pollici dovesse variare, ci basterà cambiare solamente il valore della costante nell’interfaccia, poiché tutti i riferimenti ad essa punterebbero di conseguenza al nuovo valore, e quindi basterebbe andare a modificare solamente la costante pollici dell’interfaccia, senza andare minimamente a toccare le classi che la implementano, questo ci aiuta molto in fase di manutenzione. Nell’esempio abbiamo utilizzato anche un tipo particolare di commento, ovvero: /** Commento */ Si tratta di uno speciale commento, che viene visualizzato, in fase di programmazione, passando il mouse su di un richiamo alla parte di codice successiva al commento, per cui se abbiamo definito delle variabili, utilizzando tali variabili in un'altra parte di codice, passandoci il mouse sopra leggeremo il commento, può essere usato ovunque, non solo nelle interfacce. 49 Per concludere quindi questa parte riguardate le classi, ogni outer class, deve essere definita nel suo file .java, un file di classe, deve contenere il nome del pacchetto, gli import e la classe, sono ammesse classi o interfacce all’interno della classe, ma non all’esterno, alla chiusura dell’outer class infatti deve finire anche il file, per semplificare, abbiamo mostrato più outer class insieme, ma vanno comunque considerate come files separati, per cui la sintassi corretta sarebbe: Classe1.java package pkg.name; import pacchetto.da.importare1; import pacchetto.da.importare2; public class Classe1{ public Classe1(){ //Codice } } // Fine del file Classe2.java package pkg.name; import pacchetto.da.importare1; import pacchetto.da.importare2; public class Classe2 extends Classe1{ public Classe2(){ //Codice } } // Fine del file I file contenuti all’interno di pacchetti differenti, devono prima essere importati, mentre i files contenuti nello stesso pacchetto, non devono essere importati, per cui ad esempio dovremmo fare: Classe1.java package pacchetto.primo; public class Classe1{ public Classe1(){ // Codice } } // Fine del file 50 Classe2.java package pacchetto.secondo; import pacchetto.primo.Classe1; public Classe2 extends Classe1{ public Classe2(){ // Codice } } // Fine del file Per cui per estendere o istanziare una classe, oppure implementare un interfaccia o richiamare una variabile all’esterno del pacchetto, bisogna eseguire necessariamente l’import, inoltre non possono essere importati due file con lo stesso nome, anche se in pacchetti diversi, ad esempio: import alpha.beta.Omega; // Ok, Omega non è stato già importato. import alpha.beta.Epsilon; // Ok, Epsilon non è stato già importato. import delta.gamma.Omega; // Errore, un file di nome Omega è stato già importato Quindi, possiamo importare anche più files dallo stesso pacchetto, o da pacchetti che hanno nomi simili, ma non due files con lo stesso nome, anche se sono completamente diversi, per cui se importiamo il file Omega dal pacchetto alpha.beta, non possiamo importare nessun altro file chiamato Omega, anche se diverso dal primo, per cui importare anche il file Omega contenuto nel pacchetto delta.gamma, ci darà errore, possiamo importare un qualsiasi file contenuto nel nostro file di progetto, ma non files contenuti in altri progetti, per importare files esterni, dobbiamo o copiarli all’interno del nostro progetto, oppure aggiungere un file .jar, contenente tali files, un file .jar, è essenzialmente un progetto compilato, esso infatti contiene i files delle classi in formato .class, e non .java, la differenza è data dal fatto che un file .java contiene codice sorgente, mentre un file .class, contiene un file compilato, i files .class possono comunque essere estesi ed istanziati, se permesso, come normali classi .java, ma non possono essere modificati, ne si può avere accesso al sorgente di tali files, per importare un file .jar, ci basterà copiarlo nella cartella libs, e dopodiché, potremo fare l’import dei files in esso contenuti direttamente nelle classi del progetto, è inoltre possibile anche importare tutti i files di un determinato pacchetto, tramite *, digitando ad esempio: import alpha.beta.*; Importeremo tutti i files del pacchetto, creare un progetto Android piuttosto che un progetto Java, ci genererà un progetto con all’interno molte librerie Android importate, e che possiamo poi utilizzare per creare la nostra app, si tratta infatti di classi che automatizzano molte operazioni relative ad Android, come ad esempio le già menzionata classe ActionBarActivity. 51 2.10 I metodi Abbiamo già accennato ai metodi, parlando delle classi astratte e delle interfacce, ma non siamo entrati molto nel dettaglio. Un metodo è una determinata parte di codice che può essere o richiamata in quanto codice, utile quindi se si deve utilizzare la stessa parte di codice in più parti del progetto, inoltre i metodi possono anche essere ereditati dalle super classi o richiamati dalle classi istanziate, inoltre i metodi possono essere utilizzati anche per restituire determinati valori, i metodi vanno definiti all’esterno dei costruttori della classe, ma comunque sempre all’interno della classe, la sintassi è simile a quella dei costruttori, ma con la differenza, di dover definire un tipo di ritorno, se il metodo non deve restituire nessun valore, il tipo di ritorno deve essere definito come void, ecco alcuni esempi di sintassi di metodi: public class Classe1{ int a = 0; public Classe1(){ // Costruttore a = Metodo1(6,4); // a = 10 Metodo2(7); // a = 7 a = Metodo3(); // a = 14 public void Metodo0(){ // Errore, siamo all’interno del costruttore } } // Fine del costruttore public double Metodo1(int arg0, double arg1){ // Metodo con ritorno double return arg0+arg1; // return è obbligatorio se il metodo non è void } protected void Metodo2(int arg0){ // Metodo void, return non necessario a = arg0; } protected int Metodo3(){ // Metodo int senza parametri return a*2; // return obbligatorio } public int Metodo4(){ a = 8; } // Errore, manca il return public int Metodo5(Double arg0){ return arg0; // Errore, metodo int, double non può essere convertito in int } public double Metodo5(int arg0){ Return arg0; // Ok, metodo double, int può essere convertito in double } public Metodo6(){ //Errore, bisogna specificare un tipo di ritorno, se non ha ritorno va specificato void } } // Fine della classe 52 I metodi non void, devono necessariamente restituire un valore del loro tipo, i metodi void non possono restituire alcun valore, ma possono avere comunque l’istruzione return, purché non riporti nulla, il return funziona in modo analogo al break, quindi non può essere eseguito codice dopo di esso, l’esecuzione di un metodo non void deve sempre terminare con un return, il return può essere inserito anche all’interno di uno statement, purché sia sempre previsto un return in ogni caso, quindi ad es: … public int Metodo1(int a){ if (a < 15){ return a; // Ok }else{ return 15; // Ok } } public int Metodo2(int a){ if (a < 15){ return a; // Ok, se a < 15 il metodo termina qui } return 15; // Ok, se a >= 15 deve venire comunque eseguito un return } public int Metodo3(int a){ if (a < 15){ return a; } } // Errore, se a >= 15 non verrà eseguito alcun return public int Metodo4(int a){ return a; a = 15; // Errore, codice irraggiungibile } public void Metodo5(int a){ int b = 15 if (a >= 15){ return; // Ok, se a >= 15 il metodo termina qui } b = a; // Ok, Metodo void, return non necessario } Per cui se creiamo metodi non void, dobbiamo fare in modo che il metodo termini sempre con un return, qualsiasi condizione dovesse verificarsi, quindi ad esempio se mettiamo un return alla fine di un if, dobbiamo metterlo anche alla fine del metodo, in quanto se la condizione dell’if non dovesse venir soddisfatta, il metodo terminerebbe senza un return, inoltre dobbiamo evitare di creare codice irraggiungibile, ovvero sempre dopo un return, un po’ come succedeva per il break, il tipo di ritorno del metodo, determina il valore che restituirà il metodo, un certo tipo di metodo restituirà sempre un certo tipo di valore, per cui un metodo double, restituirà sempre un valore double, anche se gli facciamo ritornare una variabile di tipo int, la variabile verrà convertita in double, di norma una variabile più complessa non può essere convertita in una più semplice, per cui ci darà errore un metodo int che cerca di fare il return di una variabile double, ma è sempre possibile convertire una variabile 53 più semplice in una più complessa, quindi un metodo double può facilmente convertire una variabile int, in alternativa è anche possibile forzare la conversione di un tipo più complesso di dato ad uno più semplice, con possibile perdita di dati, tramite il cast è quindi possibile convertire valori più complessi in valori più semplici, nel caso di valori decimali convertiti in valori interi, verranno eliminate eventuali cifre decimali, convertendo invece valori fuori dalla gamma del tipo di variabile in cui convertiamo, ci porterà ad un “roll over”, ovvero una volta giunti al valore più alto, si ricomincerà con il valore più basso o viceversa, la sintassi per eseguire un cast è la seguente: int a = (int) 14.6; // a = 14 Possiamo fare il cast sia di valori costanti, che di variabili, è anche possibile convertire una CharSequence in una String, ma non è possibile tramite cast però convertire un valore non numerico in numerico e viceversa, ecco alcuni esempi: byte b; int i = 129; double d = 130.67; String s; CharSequence cs = "Testo"; b = (byte) i; // Ok, ma b = -127 Roll Over: 127 = 127 128 = -128 129 = -127 130 = -126 i = (int) d; // Ok, I = 130, vengono eliminati I decimali i = (byte) d; // Ok, ma b = -126, int accetta cast in byte b = (int) d; // Errore, byte non accetta cast in int i = 115; b = i; // Errore, necessario cast, variabile int, anche se nella gamma. b = (byte) i; // Ok, e senza perdita di dati, b = 115 s = cs; // Errore, cast necessario, String non accetta CharSequence s = (String) cs; // Ok, è possibile fare il cast cs = s; // Ok, cast non necessario CharSequence accetta valori String s = (String) b; // Errore, non è possibile convertire un numero in testo s = "192"; // È considerata una stringa di testo, non un valore numerico i = (int) s; // Errore, non è possibile convertire testo in numeri Il cast quindi ci può tornare utile per convertire valori più complessi in valori più semplici, soprattutto se la perdita di precisone non è un problema, ad esempio convertendo un numero decimale in numero intero, inoltre convertire un numero già all’interno della gamma della variabile in cui andiamo a convertire, non ci porterà a perdere dati, mentre se il valore sarà fuori dalla gamma, incorreremo in un roll over. I metodi quindi, possono essere utilizzati o per eseguire operazioni matematiche, oppure per contenere parti di codice richiamabile, sia dalla stessa classe, che dalle altre classi che ne hanno la visibilità, un metodo private, può essere richiamato solo all’interno della classe, così come le classi, anche i metodi possono avere diversi modificatori, e quindi esistono diversi tipi di metodi. Vediamo ora le differenze tra i vari tipi di metodi: 54 Metodi normali – Si tratta di normali metodi senza modificatori, possono essere richiamati dalla propria classe direttamente, e dalle sotto classi se visibili, inoltre possono essere richiamati dalle classi che istanziano la classe, nel seguente modo: new CostruttoreClasse(Parametri).Metodo(Parametri); Possono avere riferimenti a risorse non statiche, ma non possono essere richiamate in maniera statica, quindi non è possibile utilizzare: NomeClasse.Metodo(Parametri); Un esempio di metodo normale è il seguente: public int NomeMetodo(int arg0){ int a = arg0; return a; } Metodi static – Un metodo static, è definito tale dal modificatore static, similarmente alle classi static, non può richiamare direttamente variabili non static contenute nella classe, ma deve necessariamente istanziare la classe, cosa che non va necessariamente fatta con i metodi normali, ma a differenza delle classi normali, le classi static possono essere richiamate in modo statico, quindi senza istanziare la classe, per cui ci è possibile utilizzare: NomeClasse.Metodo(Parametri); Un esempio di metodo static: public static int NomeMetodo(int arg0){ int a = arg0; return a; } I metodi static sono parte della classe e non istanze, quindi non possono essere soggetti ad Override, ma possono essere ridichiarati. Metodi final – Un metodo di tipo final, si comporta come un normale metodo, ma con un unica differenza, ossia l’impossibilità di Override, abbiamo già accennato sia all’Override, che all’overload, l’Override di un metodo, è la riscrittura del metodo, a differenza dei metodi static però, un metodo final non può essere neanche ridichiarato, un metodo final, un metodo static, può essere dichiarato in aggiunta anche final, in tal caso non potrà neanche essere ridichiarato, in seguito approfondiremo override, overload e ridichiarazione. 55 Metodi abstract – Abbiamo già menzionato i metodi astratti, parlando delle interfacce e delle classi astratte, i metodi astratti, sono definiti dal modificatore abstract, i metodi astratti, sono metodi da implementare, di cui ne viene definita solamente la visibilità, il tipo di ritorno, il nome e i parametri, ma non ne viene definito il corpo, un metodo astratto termina con ; e non ha un corpo, quindi non ha parentesi graffe { } i metodi astratti possono essere contenuti solo in classi astratte o interfacce e devono necessariamente essere implementati nelle sotto classi, salvo che nelle sottoclassi astratte o nelle sotto interfacce, nel qual caso però, le eventuali sottoclassi della sottoclasse astratta, dovrà implementare i metodi della super classe astratta e della super classe astratta madre della super classe astratta e così via, lo stesso discorso vale per le interfacce, i metodi abstract, possono essere definiti anche static, ma non final, dato che vanno obbligatoriamente implementati, per cui bisogna eseguirne l’override, ecco un esempio di metodo astratto: public interface Sistema{ public double miglia = 0.621371192; // È considerata static final public String scala(); // Nelle interfacce non è necessario definire abstract } public abstract class Velocità implements Sistema{ // La classe è astratta, non dobbiamo necessariamente implementare scala() public abstract double speed(double arg0); // Qui, è necessario definire abstract } public class Italia extends Velocità{ public Italia(){ } @Override // Annotazione di Override, non obbligatoria, ma segnala eventuali errori public String scala(){ return "Km/h"; // Dobbiamo implementare anche i metodi dell’interfaccia Sistema } @Override public double speed(double s){ return s; } } public class America extends Velocità{ public America(){ } @Override public String scala(){ return "Mph"; // Dobbiamo implementare i metodi in tutte le sotto classi } @Override public double speed(double s){ return s * miglia; // Possiamo offrire diverse implementazioni tra le classi } } 56 Nell’esempio abbiamo due classi distinte che estendono la classe astratta Velocità, che a sua volta implementa l’interfaccia Sistema, essendo velocità astratta, non deve fornire l’implementazione dei metodi dell’interfaccia Sistema, ma implementandola è come se si aggiungesse alla classe, quindi tutte le classi che estenderanno Velocità, dovranno implementare sia i metodi di Sistema che i metodi di Velocità, ogni classe però può offrire una diversa implementazione dei metodi, per l’implementazione dei metodi abbiamo utilizzato l’annotazione @Override, per cui richiamare le due classi, porterà a risultati diversi, ad esempio: public class Auto{ Italia it = new Italia(); America us = new America(); String[] str = new String [2]; public Auto(double n){ str[0]="La mia auto raggiunge " + it.speed(n) + " " + it.scala(); str[1]="La mia auto raggiunge " + us.speed(n) + " " + us.scala(); } } Per cui dando in input al costruttore della classe Auto, ad esempio il valore 200, otterremo le seguenti stringhe: La mia auto raggiunge 200.0 Km/h La mia auto raggiunge 124.2742384 Mph Potremmo anche implementare i metodi ex novo, senza utilizzare i metodi astratti, ma essi comunque ci garantiscono una certa omogeneità del codice, segnalandoci quindi anche alcuni eventuali errori di battitura, per cui ad esempi se nella classe America avessimo scritto Speed(double s) il compilatore ci avrebbe dato errore, poiché il metodo da implementare era speed e non Speed, stessa cosa se avessimo scritto speed(int s) in quanto il metodo astratto ha come parametro una variabile double e non int, questi errori, non sarebbero saltati all’occhio creando metodi ex novo, in quanto non ci sarebbero metodi da implementare, l’uso dei metodi astratti benché non aggiungano nulla da un punto di vista strettamente logico matematico, aggiungono un importante strumento di controllo dal punto di vista umano, in quanto permettono di identificare con facilità errori che potrebbero passare inosservati. Parlando dei metodi, abbiamo accennato ai concetti di override e overload, nonché di ridichiarazione e implementazione, concetti che ora andremo ad analizzare. Implementazione – L’implementazione è un operazione tipica dei metodi abstract, essi devono essere obbligatoriamente implementati, ovvero bisogna definirne un corpo, l’implementazione è compatibile con l’annotazione @Override. 57 Overload – L’overload avviane quando definiamo un metodo con lo stesso nome di un altro metodo, ma con parametri diversi, il nuovo metodo può avere anche un tipo di ritorno diverso, ma deve avere parametri diversi, alcuni esempi di overload: public int Metodo(){ return 0; } public boolean Metodo(boolean arg0){ // Ok, stesso nome ma parametri diversi return !arg0; } public int Metodo(double arg0){ // Ok, stesso nome ma parametri diversi return (int) arg0; } public int Metodo(double arg0, boolean arg1){ // Ok, diverso numero di parametri if (arg1){ arg0 /= 2;} return (int) arg0; } public double Metodo(double d){ // Errore, stesso nome e parametri di un altro metodo return d; } Abbiamo un metodo int senza parametri, in seguito abbiamo l’overload del metodo con un diverso tipo di ritorno, ma con parametri diversi ossia ora abbiamo un parametro di tipo boolean, il secondo overload, ha lo stesso tipo di ritorno del primo metodo, ma parametri diversi, in questo caso un parametro double, quindi l’overload è accettato, il terzo overload è anche corretto, in quanto anche se abbiamo gli stessi tipi di parametri, abbiamo comunque un numero diverso di parametri, il quarto override invece ci darà errore, in quanto ha gli stessi parametri del secondo overload, infatti entrambi hanno come unico parametro una variabile double, il nome della variabile ed il tipo di ritorno non contano. Nel momento in cui andremo a richiamare il metodo, il compilatore capirà subito che metodo andare ad utilizzare, in base ai parametri che andremo a specificare, indi per cui non è ammesso l’overload di metodi con gli stessi parametri, in quanto si genererebbe ambiguità ed il compilatore non saprebbe quale metodo andare a richiamare, l’overload può essere fatto sia all’interno della stessa classe, che all’interno di una sotto classe. Override – L’override, è la riscrittura di un metodo, contrariamente all’overload, l’override di un metodo deve avere gli stessi parametri del metodo da riscrivere, nonché lo stesso nome, lo stesso tipo di ritorno ed una visibilità non inferiore, per cui di un metodo dichiarato con visibilità default, possiamo fare un override con visibilità protected, public o default, ma non private, i nomi delle variabili dei parametri possono anche differire, purché abbiano gli stessi tipi, e si abbiano lo stesso numero di parametri, l’override può essere eseguito all’interno delle sottoclassi, ma non 58 all’interno della classe stessa, inoltre non può essere eseguito l’override di metodi definiti static o final, o di metodi non visibili, può essere utilizzata l’annotazione @Override per controllare la correttezza dell’override, ecco alcuni esempi di override public class Veicolo{ public Veicolo(){ } public return } public return } } int Ruote(){ 0; boolean Motore(){ false; public class Automobile extends Veicolo{ public Automobile(){ } @Override public int Ruote(){ return 4; } @Override public boolean Motore(){ return true; } } public class Bicicletta() extends Veicolo{ public Bicicletta(){ } @Override public int Ruote(){ return 2; } } public class Classe1{ Veicolo v; String str; public Classe1(int flag){ if (flag == 0){ v = new Automobile(); }else if (flag == 1){ v = new Moto(); }else{ v = new Automobile(); } str = "Ruote: " + v.Ruote() + " Ha il motore: " + v.Motore(); } } 59 In questo esempio abbiamo due classi distinte che estendono la classe Veicolo, tale classe ha al suo interno definiti due metodi, le sottoclassi di Veicolo possono eseguire l’override o l’overload dei metodi contenuti in essa, per fare l’override, basta definire un metodo con lo stesso tipo di ritorno, nome, parametri ed una visibilità pari a superiore, di un metodo visibile non static e non final della super classe, per eseguire l’override, non è necessario utilizzare l’annotazione @Override, ma è comunque consigliato, in quanto l’uso di tale annotazione, ci restituirà un errore, se non stiamo eseguendo l’override di un metodo, e quindi stiamo ad esempio eseguendo un overload, poiché non abbiamo scritto correttamente il nome del metodo, o magari abbiamo sbagliato il tipo di un parametro, errori che senza l’utilizzo di @Override passerebbero inosservati. La classe Automobile, esegue l’override dei metodi Ruote( ) e Motore ( ), mentre la classe Bicicletta esegue l’override solamente del metodo Ruote( ), ora andando ad istanziare le sottoclassi tramite una variabile del tipo della super classe, nel nostro caso la variabile v di tipo Veicolo, otterremo quindi risultati diversi a seconda della classe che istanziamo per cui in base al valore con cui inizializzeremo il costruttore, la stringa str potrà assumere i seguenti valori: //flag Ruote: //flag Ruote: //flag Ruote: = 4 = 2 = 0 0 Ha il motore: true 1 Ha il motore: false altro Ha il motore: false Istanziando la classe Automobile eseguiremo l’override di entrambi i metodi, istanziando invece la classe Bicicletta verrà eseguito l’override solo del metodo Ruote( ), per cui richiamando v.Motore() verrà richiamato il metodo originale contenuto nella super classe Veicolo, istanziando invece la classe Veicolo, verranno eseguiti i metodi originali in essa contenuta. Possiamo quindi istanziare una classe in una variabile del tipo della superclasse, così facendo possiamo richiamare tutti i metodi contenuti all’interno della super classe e tutti gli override dei metodi della super classe, ma non gli overload contenuti nella sotto classe, o i metodi non contenuti nella super classe, l’annotazione @Override, ci segnalerà eventuali problemi nell’override, come ad esempio: public public } public return } } class SuperClasse{ SuperClasse(){ double Metodo1(double arg0){ arg0; 60 public class Classe1 extends SuperClasse{ public Classe1(){ } @Override public double metodo1(double arg0){ // Errore, dovrebbe essere Metodo1, non metodo1 return arg0*2; } @Override Public double Metodo1(int arg0){ // Errore, il parametro era double, non int return arg0*2; } @Override public double Metodo1(double arg0){ // Ok, tutto coincide return arg0*2; } public class Classe2 extends SuperClasse{ public Classe2(){ } public return } public return } public return } } double metodo1(double arg0){ // Ok, ma nuovo metodo, nessun override arg0*2; double Metodo1(int arg0){ // Ok, ma overload, non override arg0*2; double Metodo1(double arg0){ // Ok, @Override non obbligatoria, tutto coincide arg0*2; Nell’esempio abbiamo una super classe, con al suo interno il metodo Metodo1, e due sottoclassi, la prima tenta di fare l’override di Metodo1 utilizzando l’annotazione @Override, la seconda senza tale annotazione, nella prima classe quindi gli errori nell’override del metodo vengono segnalati dal compilatore, in quanto Classe1 cerca di fare l’override di un metodo inesistente, ovvero metodo1, mentre la super classe contiene Metodo1 con la M maiuscola, inoltre tenta anche di eseguire un overload di Metodo1, ma l’annotazione @Override ci da errore, in quanto deve essere un override e non un overload, infine il terzo override è corretto in quanto tutto coincide con il metodo di cui vogliamo eseguire l’override, il codice di Classe2, è identico a quello di Classe1, ma non utilizza però l’annotazione @Override, il compilatore quindi nonostante il codice sia identico, non ci darà alcun errore, ma nel primo caso, creerà un nuovo metodo, che non ha nulla a che fare con Metodo1, nel secondo caso, eseguirà l’overload di Metodo1 con un parametro int, anziché double, mentre nel terzo caso, l’override avverrà in maniera corretta, anche senza utilizzare l’utilizzo di @Override. 61 Ridichiarazione – la ridichiarazione, avviene nel momento in cui si tenta di eseguire l’override di un metodo static, ma non final, non è una vera e propria riscrittura del metodo, ma più che altro si tratta di una ridefinizione del metodo, per la classe in cui avviene e le eventuali sottoclassi, ciò quindi impedisce alle sottoclassi della classe che esegue la ridichiarazione, di ereditare il metodo originale alle proprie sottoclassi, ciò però non è considerato un override, quindi utilizzare @Override ci darà un errore, vediamo ora un esempio: public class SuperClasse{ public SuperClasse(){ } public static boolean Metodo1(){ return false; } } public class Classe0 extends SuperClasse{ @Override public static boolean Metodo1(){ return true; // Errore, non si può eseguire l’override di metodi statici } } public class Classe1 extends SuperClasse{ public Classe1(){ } public static boolean Metodo1(){ return true; // Ok, viene eseguita una ridichiarazione del metodo statico } } public class Classe2 extends Classe1{ public boolaean a, b, c, d; public SuperClasse s; public Classe1 c1; public Classe2{ a = Metodo1(); // a = true, il metodo originale viene nascosto dal nuovo metodo s = new SuperClasse(); b = s.Metodo1(); // b = false, viene eseguito il metodo originale s = new Classe1(); c = s.Metodo1(); c = false, non viene eseguito l’override del metodo statico c1 = new Classe1(); d = c1.Metodo1(); d = true, viene eseguito il metodo contenuto in Classe1 } } 62 In questo caso abbiamo una super classe, con al suo interno un metodo statico, poi abbiamo due classi che estendono SuperClasse, la prima, tenta un override del metodo statico, utilizzando @Override, che ci restituirà quindi un errore, in quanto non è possibile eseguire l’override di un metodo statico, la seconda classe invece ridichiara il metodo senza quindi utilizzare @Override, abbiamo infine Classe2, che estende Classe1, e non SuperClasse, ma poiché Classe1 eredita i metodi di SuperClasse, Classe2, dovrebbe ereditare quindi sia i metodi di Classe1 che di SuperClasse, se non fosse per il fatto che Classe1 ha ridichiarato il metodo di SuperClasse e quindi verrà ereditato solo il metodo ridichiarato, e non quello originale, quindi Metodo1() ci restituirà true, questo però non è un override, quindi istanziando Classe1 tramite una variabile di tipo SuperClasse, ed invocando il metodo Metodo1, verrà eseguito il metodo originale, e non il metodo contenuto in Classe1, per cui SuperClasse s = new Classe1; c = s.Metodo1(); Verrà comunque eseguito il metodo originale e quindi c sarà false, se invece avessimo dichiarato Metodo1 non static, avremmo potuto eseguirne l’override, e c sarebbe stato true, istanziando però Classe1 tramite una variabile di tipo Classe1, verrà eseguito il metodo ridichiarato, quindi facendo Classe1 c1 = new Classe1; d = c1.Metodo1(); d sarà true e non false, come nell’esempio precedente, possiamo inoltre ridichiarare anche metodi non visibili in quanto magari dichiarati private e quindi non ereditati, essi verranno considerati nuovi metodi a tutti gli effetti, in quanto i metodi non visibili, non vengono ereditati. 2.11 Metodi e classi predefiniti Creando un nuovo progetto Android, verranno creati diversi files e cartelle, tra cui la cartella Android x.x.x dove le x indicano la versione dell’API di riferimento, con al suo interno il file android.jar, file contenente tantissime classi che possiamo richiamare per eseguire alcune operazioni, alcune di queste classi sono basilari del linguaggio java, come ad esempio le classi contenute all’interno del pacchetto java.lang, o dei pacchetti java.* mentre altre sono esclusive di Android, ovvero tutte le classi contenute nei pacchetti android.* abbiamo inoltre le cartelle Andorid Private Libraries e Android Dependencies, che contengono il file relativo ad Appcompat, il file in cui è contenuta la classe ActionBarActivity, estesa dalle nostre Activity, nella parte di codice autogenerata del nostro primo progetto, abbiamo potuto notare svariate righe di codice, molte di esse non erano vere e proprie istruzioni proprie del linguaggio, quali ad esempio return o if, ma richiami o override di metodi contenuti nella super classe ActionBarActivity, tra cui ad esempio l’override del metodo onCreate, da notare quindi anche l’annotazione @Override, prima di tale metodo, per poter quindi realizzare delle buone applicazioni, dobbiamo imparare ad utilizzare 63 questi metodi e queste classi, che ci permettono ad esempio di eseguire operazioni matematiche, oppure di accedere alle funzionalità hardware dei nostri dispositivi, quali ad esempio i sensori, o la fotocamera, inoltre ci permettono anche di gestire gli oggetti quali i pulsanti e le caselle di testo, è possibile infatti controllare queste classi, cliccando sul file Android.jar, aprendo i vari file .class, sarà anche possibile vederne i metodi, tali metodi possono essere richiamati tramite nomeclasse.nomemetodo(parametri); un esempio può essere dato dal metodo sqrt contenuto nella classe Math, che ci restituisce la radice quadrata del numero immesso come parametro, per cui scrivendo ad esempio: double a = Math.sqrt(9); a varrà 3, in questo modo però possono essere richiamati soltanto i metodi definiti statici, ci sono molte classi predefinite, in seguito vedremo più nel dettaglio le classi più importanti e utili, tra cui la sopra citata classe Math, inoltre abbiamo anche classi relative ai tipi di dato, quale ad esempio la classe Integer, e soprattutto classi legate agli oggetti, quali ad esempio la classe Button, classi che ci serviranno anche per gestire l’interfaccia grafica e gli input dell’utente, come ad esempio i click sui pulsanti, oppure l’inserimento di testo in una EditText. 2.12 Il file AndroidManifest.xml Lo si potrebbe definire il cuore delle applicazioni Android, esso contiene tutti i dati relativi alla struttura della nostra applicazione, tra cui il PackageName, il numero di versione della nostra applicazione, l’API minimo necessario, ed eventualmente anche l’API massimo consentito, le varie activity, l’activity principale ed i vari permessi, i permessi infatti sono necessari se si vogliono utilizzare funzionalità hardware, oppure semplicemente accedere ai dati contenuti nel dispositivo, per cui ad esempio se vogliamo creare un applicazione che sfrutti la fotocamera, dovremmo impostare nel file AndroidManifest.xml i permessi relativi alla fotocamera, altrimenti la nostra applicazione non funzionerà, i permessi richiesti dall’applicazione, verranno mostrati in fase di istallazione, e non possono essere negati dall’utente, che però può annullare l’istallazione nel momento in cui non volesse concedere determinati permessi ad una determinata applicazione, l’API minimo invece ci permetterà di impedire l’istallazione dell’applicazione su dispositivi troppo datati, in quanto ad esempio l’App sfrutta funzionalità degli SDK più recenti, che non sarebbero quindi compatibili con le vecchie API, è possibile inoltre impostare anche un API massimo, questo ci può tornare utile quando vogliamo creare una versione alternativa di una nostra applicazione, specificatamente pensata per i dispositivi esclusi dalla versione principale, per cui se abbiamo ad esempio un App che richiede un API minimo di 11, potremmo crearne un'altra versione che abbia un API minimo di 8 ed un API massimo di 10, inoltre con l’introduzione di Android KitKat (API 19) è stato limitato l’accesso da parte delle applicazioni alla scheda di memoria esterna, per cui un 64 applicazione che sfrutti tale risorsa, potrebbe avere un API massimo di 18, in quanto non funzionerebbe correttamente su API 19, del file AndroidManifest.xml, esiste sia un editor grafico, che un editor di codice, un po’ come per i vari files dei layout delle activity e dei fragments, attenzione però ad andare a modificare tale file però, in quanto, un errore nel file AndroidManifest.xml, precluderà il funzionamento dell’intero progetto, per cui mai andare a modificarne parti di cui non si è ben compreso il significato. 2.13 I file di menù Tutti i dispositivi Android, hanno un tasto (fisico o virtuale) di menù, esso ci permette di richiamare il menù dell’applicazione, tale menù viene definito in un file .xml all’interno della sottocartella menu della cartella res, in tal file possiamo definire i vari oggetti del menù, le cui funzionalità andremo poi a definire nel file relativo all’activity, difatti creando una nuova activity, varrà auto generato anche l’override di due metodi della super classe ActionBarActivity, relativi proprio alla gestione dei menù, questa è la parte autogenerata: @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } Il primo override, ovvero onCreateOptionsMenu(Menu menu) non fa altro che aggiungere al menù dell’activity ed eventualmente anche all’action bar gli elementi contenuti nel nostro file di menù, nel nostro caso il file autogenerato main, ma possiamo comunque cambiare R.menu.main con qualsiasi altro file di menù, inoltre, è anche possibile combinare gli elementi di più menù, eseguendo getMenuInflater() più volte, quindi ad esempi facendo: getMenuInflater().inflate(R.menu.main, menu); getMenuInflater().inflate(R.menu.menu1, menu); 65 Premendo il pulsante di menù, appariranno sia gli elementi contenuti nel file res/menu/main.xml, che nel file res/menu/menu1.xml, il secondo override invece definisce ciò che avviene alla selezione delle varie voci del menù, di base troviamo un if statement, ora possiamo sia utilizzare l’if statement, con vari else if, oppure uno switch statement, per cui mettiamo caso di aver creato un menù con all’interno le seguenti voci Raddoppia e Dimezza, con i seguenti id rad e dim, ora mettiamo di aver dichiarato una variabile a di tipo double, potremmo creare ad esempio il seguente codice: @Override public boolean onOptionsItemSelected(MenuItem item) { // Il commento può anche essere eliminato int id = item.getItemId(); if (id == R.id.rad) { a *= 2; return true; }else if (id == R.id.dim) { a /= 2; return true; } return super.onOptionsItemSelected(item); } È anche possibile utilizzare lo switch statement: @Override public boolean onOptionsItemSelected(MenuItem item) { // Usiamo ora lo switch statement int id = item.getItemId(); switch (id){ case R.id.rad: a *= 2; return true; // Utilizziamo il return invece del break case R.id.dim: a /= 2; return true; } return super.onOptionsItemSelected(item); } Il codice funzionerà alla stessa maniera del precedente, naturalmente la scelta tra if e switch, dipende anche dal programma che intendiamo realizzare, ma spesso nel caso della selezione delle voci di menù è indifferente, ma in alcuni casi potrebbe fare la differenza, Eclipse autogenera un if statement, ma nulla ci impedisce di sostituirlo con uno switch statement, gli elementi dei menù possono avere diversi parametri, che ora andremo ad analizzare. Andando a modificare un file di menù, ci ritroveremo difronte la seguente schermata: 66 Prima di tutto abbiamo i pulsanti Add… e Remove… essi ci permettono rispettivamente di aggiungere o rimuovere voci dal menù, mentre i pulsanti Up e Down ci permettono di riordinarle, cliccando su Add… potremo scegliere tra tre differenti tipologie di voci, Item, Group e Sub-Menu. Item – Si tratta di una voce di menù standard, ossia una normale voce di menù selezionabile, esso può contenere anche dei sub menu. Group – Si tratta invece di un gruppo di voci, contenente più sotto voci, le voci contenute in un gruppo, non si comportano diversamente dalle voci singole, ma possono essere gestite più facilmente andando a modificare i parametri del gruppo quali ad esempio la visibilità, andando quindi a modificare tutto il gruppo con una sola riga di codice, per cui ad esempio potremmo nascondere tutti gli elementi di un determinato gruppo senza doverli disattivare uno alla volta. SubMenu – Si tratta di sottomenù che appaiono al click di una determinata voce di menù, un esempio può essere la voce di menù aggiungi, che può prevedere un sottomenù con le voci: Nome, Cognome, Indirizzo e Numero di telefono. Ogni voce di menù, ha delle proprietà che possiamo impostare, direttamente dal editor del menù, oppure direttamente dal nostro programma, inoltre è anche possibile aggiungere e rimuovere voci di menù direttamente dal nostro programma, , inoltre ogni proprietà ha un valore di default, ovvero un valore che verrà assegnato in automatico, se il relativo campo dovesse risultare vuoto, le proprietà principali sono le seguenti : Id – Si tratta dell’identificativo della voce, tramite il quale possiamo gestire la voce stessa, la sintassi è la seguente @+id/idvoce dove idvoce è un id univoco. Valore di default: Restituirà un id pari a 0 67 Title – Si tratta invece del titolo della voce, quello che apparirà sul nostro menù. Valore di default: Stringa vuota Title condensed – Versione abbreviata di title. Valore di default: Stringa vuota Icon – Questa proprietà definisce l’icona dell’action bar, e serve quindi per le voci di menù destinate all’action bar, la sintassi è la seguente @drawable/nome_icona dove nome_icona è il nome di un icona contenuta nelle cartelle res/drawable*, per creare un set di icone, possiamo andare su File > New > Other > Android > Android Icon Set ed utilizzare la stessa interfaccia che abbiamo utilizzato per creare l’icona della nostra applicazione. Valore di default: Nessun icona Checkable – Indica se la voce di menu debba prevedere una checkbox, se ipostato su true prevederà una checkbox. Valore di default: false Checked – Nel caso Checkable sia true, indica se la checkbox sia spuntata (true) oppure no (false), la checkbox comunque non si spunterà automaticamente al click, ma invece dovremmo utilizzare il metodo setChecked(boolean) della classe MenuItem. Valore di default: false Visible – Indica se la voce si visibile o meno, sarà nascosta se impostato su false, altrimenti sarà visibile, si può modificare la visibilità tramite il metodo setVisible(boolean) della classe MenuItem. Valore di default: true Enabled – Indica se la voce sia abilitata o meno, sarà disabilitata se impostato su false, altrimenti sarà abilitata, una voce disabilitata, non può essere selezionata, ed appare di colore grigio, ma comunque viene visualizzata Ameno ché non venga definita anche non visibile si può abilitare o disabilitare una voce tramite il metodo setEnabled(boolean) della classe MenuItem. Valore di default: true On click – Permette di richiamare un metodo al click della voce, il metodo deve essere visibile dalla classe MenuItem, quindi deve essere dichiarato public, inoltre 68 deve avere come unico parametro un MenuItem, ma non deve essere necessariamente utilizzato all’interno del metodo, esso può anche avere un tipo di ritorno, anche se non ci verrà restituito nulla al click del mouse, questo ci permette di specificare un comportamento senza utilizzare onOptionsItemSelected Anzi, in realtà onOptionsItemSelected è il valore di default di onClick, cioè il valore assegnato se lasciato in bianco, se invece ne modifichiamo il valore, esso punterà al nuovo metodo, e non più ad onOptionsItemSelected. La sintassi è: nomemetodo senza parametri dove nomemetodo è appunto il nome del metodo di riferimento Valore di default: onOptionsItemSelected Show as actions – Indica se la voce di menu deve essere visualizzata sull’action bar oppure come voce di menù regolare, nel file xml viene aggiunto come android:showAsActions, ma se abbiamo impostato un API minimo inferiore ad Honeycomb (Api 11) dobbiamo invece utilizzare app:showAsActions, spesso però viene aggiunta la dicitura corretta sotto la voce Unknown XML Attributes, è comunque consigliabile assegnare ad entrambe gli stessi valori, i valori possibili sono i seguenti: never: Non viene visualizzato sull’action bar, ma solo come normale voce di menù always: Viene sempre mostrato sull’action bar e non nel menù. ifRoom: Viene mostrato sull’action bar se c’è spazio, altrimenti viene visualizzato nel menù. withText: Si tratta di un opzione complementare, e va accompagnato da always oppure ifRoom, mostra oltre all’icona, anche il titolo della voce, un esempio è dato da always|withText oppure ifRoom|withText. collspseActionView: Altra opzione complementare, permette di creare una voce riducibile ad un icona, come ad esempio una barra di ricerca riducibile all’icona della lente di ingrandimento, anche esso va accompagnato da always o ifRoom, e può essere accompagnato anche da withText, esempio: ifRoom|collapseActionView|withText Utilizzare la voce always con troppe voci, può portare a glitch grafici, soprattutto su schermi di dimensioni ridotte e/o a bassa risoluzione, utilizzare showAsActions nei gruppi può portare a comportamenti inaspettati. Valore di default: never 69 Ora che abbiamo visto le principali voci di menù, vediamo ora come gestirle. Prima abbiamo creato un menù tramite un file xml onCreateOptionsMenu(Menu) ed inflate nel seguente modo: ed il metodo @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } In realtà potremmo fare a meno del file xml e di inflate usando invece il metodo add, nel seguente modo: @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(0, 1, 1, "Voce 1"); // Voce con id 1 menu.add("Voce 2"); // Voce con id predefinito 0 return true; } Possiamo sia creare voci semplici utilizzando add(CharSequence titolo) con id predefinito 0 oppure voci complete tramite add(int gruppo, int id, int ordine, CharSequence titolo) nel secondo caso potremmo definire più parametri tra cui appunto l’id, anche se comunque non ci sarà necessario dato che comunque potremmo comunque riconoscere le voci tramite il metodo getItem(int index), dove index è la posizione della voce partendo da 0, le voci “semplici” vengono prima delle voci “complesse” se abbiamo specificato un id invece sarà possibile utilizzare anche il metodo findItem(int id) dove id è appunto l’id della voce, applicabile anche per le voci create tramite file xml, in questo caso l’id punterà al file R.java, un esempio potrebbe quindi essere MenuItem i1 = menu.findItem(R.id.item1); oppure nel caso di voce creata tramite add MenuItem i2 = menu.findItem(1); l’index e l’id non sono la stessa cosa, menù invece è un oggetto di tipo Menu. Vediamo ora un esempio di menù che utilizza il metodo add(String): … private Menu m; // Variabile Globale … @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add("Voce 1");// index 0 menu.add("Voce 2");// index 1 menu.add("Voce 3");// index 2 m = menu; // Assegniamo alla variabile globale la variabile locale menu return true; } 70 @Override public boolean onOptionsItemSelected(MenuItem item) { if (item == m.getItem(0)) { // Codice Voce 1 return true; }else if(item == m.getItem(1)){ // Codice Voce 2 return true; }else if(item == m.getItem(2)){ // Codice Voce 3 return true; } return super.onOptionsItemSelected(item); } Così facendo abbiamo ottenuto lo stesso risultato che avremmo ottenuto utilizzando il file .xml ed inflate, onde evitare confusione, possiamo anche creare voci utilizzando il metodo add(int,int,int,String), in questo caso potremmo riferirci direttamente all’id come nel caso del menu xml oppure utilizzare il metodo findItem(int) dando come parametro l’id con gli stessi risultati del metodo getItem(int) con parametro invece l’index. Vediamo ora come utilizzare la proprietà checkable. Capita spesso infatti di dover creare voci di menù spuntabili, come ad esmpio una voce che permetta di abilitare o disabilitare la musica di sottofondo, è possibile fare questo tramite la proprietà checkable, per fare questo possiamo impostare checkable su true dal file di menu xml, oppure è anche possibile farlo senza utilizzare il file di menù ne seguente modo: @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add("Voce 1"); // index 0 menu.getItem(0).setCheckable(true); // Rendiamo la voce Checkable return true; } Così facendo otterremo una voce checkable senza aver utilizzato il file xml, naturalmente possiamo farlo anche utilizzando findItem se ne abbiamo definito un id, bisogna però ricordarsi però che cliccare su una voce checkable, non ne cambierà automaticamente lo stato, bisogna quindi prevederlo al click, ecco un esempio di codice: private Menu m; … @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add("Voce 1"); // index 0 menu.add("Voce 2"); // index 1 menu.getItem(0).setCheckable(true); // Rendiamo la voce Checkable m = menu; return true; } 71 @Override public boolean onOptionsItemSelected(MenuItem item) { if (item == m.getItem(0)) { if(item.isChecked()){// Se la voce è spuntata verrà eseguito Codice1 // Codice 1 } else { // Se la voce non è spuntata verrà eseguito Codice 2 // Codice 2 } item.setChecked(!item.isChecked()); //Cambiamo lo stato della spunta return true; }else if(item == m.getItem(1)){ // Codice 3 return true; } return super.onOptionsItemSelected(item); } In questo caso abbiamo due voci, di cui una checkable, eseguiamo un controllo con il metodo isChecked, che ci restituirà il valore della proprietà Checked della voce, quindi true se spuntata o false se non spuntata, così facendo possiamo eseguire due codici diversi a seconda del fatto che la voce sia spuntata o meno, infine dopo l’if statement, cambiamo lo stato della voce con item.setChecked(!item.isChecked()) questo non farà altro che assegnare alla proprietà Checked il valore inverso al quello corrente in quanto abbiamo utilizzato l’operatore booleano NOT ! Questa semplice riga di codice ci permette di cambiare lo stato della voce automaticamente ogni volta che si seleziona la voce, naturalmente è possibile cambiare lo stato della voce anche all’esterno di essa o anche cambiarne la proprietà Checkable, ad esempio: @Override public boolean onOptionsItemSelected(MenuItem item) { if (item == m.getItem(0)) { if(!item.isCheckable()){ // Se non Checkable viene eseguito Codice 1 // Codice 1 return true; } else if (item.isChecked()){ // Codice 2 } else { // Codice 3 item.setChecked(!item.isChecked()); //Cambiamo lo stato della spunta return true; }else if(item == m.getItem(1)){ m.getItem(0).setCheckable(!m.getItem(0).isCheckable); return true; } return super.onOptionsItemSelected(item); } In questo caso selezionando Voce 2, invertiremo il valore della proprietà Checkable di Voce 1 inoltre ora Voce 1 prevede l’esecuzione di tre diversi codici, infatti se la voce non è Checkable verrà eseguito Codice 1, altrimenti a seconda del fatto che sia 72 spuntata o meno verranno eseguiti Codice 2 o Codice 3 inoltre verrà anche cambiato lo stato della spunta, ma solo se la voce è Checkable in quanto dopo Codice 1 abbiamo previsto un return. Da notare inoltre che mentre nel codice riferito al click su Voce 1 possiamo riferirci ad essa tramite la variabile locale item, per riferirci a Voce 1 tramite il click di Voce 2 o comunque all’esterno del codice relativo al click su Voce 1 dobbiamo invece utilizzare il metodo getItem oppure findItem, questo perché il metodo onOptionsItemSelected ha come parametro una variabile di tipo MenuItem, contenente appunto il riferimento alla voce di menù cliccata, per cui la variabile locale item, o comunque noi la vogliamo chiamare avrà sempre il valore della voce su cui abbiamo cliccato, item è comunque il nome della variabile del parametro, e quindi può anche essere cambiato, mentre MenuItem è il tipo di parametro, per cui ad esempio posiamo anche fare: @Override public boolean onOptionsItemSelected(MenuItem pippo) { if (pippo == m.getItem(0)) { // Codice 1 return true; } return super.onOptionsItemSelected(pippo); } Oltre al metodo add, abbiamo anche il metodo removeItem(int id) che ci permette invece di rimuovere una voce tramite il suo id abbiamo inoltre il metodo clear() che ci permette invece di svotare completamente un menù. Abbiamo già visto i sub menù all’interno del file di menu xml, ma è anche possibile crearli senza di esso, tramite il metodo addSubMenu(CharSequence titolo) oppure addSubMenu(int gruppo, int id, int posizione, CharSequence titolo) similarmente al metodo add, possiamo poi usare il metodo add per aggiungere voci al sottomenù in questo modo: @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add("Voce 1"); // index 0 di menu SubMenu s1 = menu.addSubMenu("Sub 1"); // Creiamo il primo SubMenu SubMenu s2 = menu.addSubMenu("Sub 2"); // Creiamo il secondo SubMenu s1.add("Voce 2"); // index 0 di s1 s1.add("Voce 3"); // index 1 di s1 s2.add("Voce 4"); // index 0 di s2 s2.add("Voce 5"); // index 1 di s2 s2.getItem(1).setCheckable(true); // Rendiamo Checkable Voce 5 return true; } Naturalmente se vogliamo gestire i click sulle varie voci, dovremmo definire le variabili s1 e s2 come globali e non locali, vediamo ora un esempio: 73 private SubMenu s1, s2; private Menu m; … @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add("Voce 1"); // index 0 di menu s1 = menu.addSubMenu("Sub 1"); // Creiamo il primo SubMenu s2 = menu.addSubMenu("Sub 2"); // Creiamo il secondo SubMenu s1.add("Voce 2"); // index 0 di s1 s1.add("Voce 3"); // index 1 di s1 s2.add("Voce 4"); // index 0 di s2 s2.add("Voce 5"); // index 1 di s2 s2.getItem(1).setCheckable(true); // Rendiamo Checkable Voce 5 m = menu; // assegnamo alla variabile globale m il valore di menu return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item == m.getItem(0)) { // Voce 1 // Codice 1 return true; }else if(item == s1.getItem(0)){ // Voce 2 // Codice 2 return true; }else if(item == s1.getItem(1)){ // Voce 3 // Codice 3 return true; }else if(item == s2.getItem(0)){ // Voce 4 // Codice 4 return true; }else if(item == s2.getItem(1)){ // Voce 5 if (item.isChecked()){ // Codice 5 }else{ // Codice 6 item.setChecked(!item.isChecked()); return true; } return super.onOptionsItemSelected(item); } Gli ogetti di tipo SubMenu funzionano in maniera analoga agli ogetti Menu in quanto la classe SubMenu estende la classe Menu e ne eredita I metodi, inoltre anche se nel file xml non possiamo definire SubMenu interni ad altri SubMenu, questo limite non sussiste se li creiamo manualmente, in quanto la classe SubMenu eredita anche il metodo addSubMenu per cui potremmo ad esempio avere: private SubMenu s1, s2; … @Override public boolean onCreateOptionsMenu(Menu menu) { s1 = menu.addSubMenu("Sub 1"); // Creiamo il primo SubMenu s1.add("Voce 1"); // index 0 di s1 s2 = s1.addSubMenu("Sub 2"); // Creiamo SubMenu interno a s1 s2.add("Voce 2"); // index 0 di s2 s2.add("Voce 3"); // index 1 di s2 return true; } 74 Così facendo avremo un SubMenu Sub 2 all’interno del SubMenu Sub 1, cosa non permessa dall’editor grafico del file di menu. Parliamo ora dei gruppi, abbiamo infatti visto come creare dei gruppi tramite il file xml, è però possibile anche definirli e gestirli anche senza di esso, infatti non è neanche necessario creare dei gruppi, basta infatti definire l’id di un gruppo come primo parametro del metodo add(int gruppo, int id, int posiz, CharSequence titolo); Possiamo poi gestire i gruppi tramite vari metodi, ricordandoci però che creare voci di menu con add(CharSequence titolo); le collegherà al gruppo con id 0. Creando un gruppo nel editor xml potremmo definirne alcune proprietà, le più importanti sono: id – l’id del gruppo relativo al file R.java usa la seguente sintassi +@id/nome dove nome è in nome dell’id. Valore predefinito: id pari a 0. Checkable behavior – indica se il gruppo deve avere o meno dei CheckBox o dei RadioButton, le possibilità sono: none: Si tratta di un gruppo di normali voci. all: Tutte le voci avranno dei CheckBox e saranno considerate Checkable. single: Tutte le voci avranno invece dei RadioButton, sono quindi considerate comunque Checkable, ma con la differenza che il Check di una voce rimuoverà il Check da tutte le altre, inoltre i RadioButton appaiono visivamente diversi dai CheckBox. Valore predefinito: none. Visible – Mostra o nasconde tutte le voci del gruppo. Valore predefinito: true. Enabled – Abilita o disabilita tutte le voci del gruppo. Valore predefinito: true. È naturalmente possibile anche modificare questi parametri manualmente e senza utilizzare l’editor xml, inoltre i gruppi creati dall’applicazione, avranno tutti i valori, id a parte impostati sui valori predefiniti, valori che però possiamo andare a modificare tramite l’utilizzo di alcuni metodi. 75 Per impostare la proprietà Checkable behavior possiamo utilizzare il metodo setGroupCheckable(int id, boolean checkable, boolean exclusive); Dove id è l’id del gruppo, checkable indica se il gruppo deve essere checkable oppure no mentre exclusive indica se si tratta di CheckBox nel caso di false o di RadioButton nel caso di true, vediamo ora un esempio: @Override public boolean onCreateOptionsMenu(Menu menu) { SubMenu s = menu.addSubMenu("Funzione X") s.add(1, 1, 1 "On"); s.add(1, 2, 2 "Off"); s.setGroupCheckable(1, true, true); s.findItem(2).setChecked(true); // Selezioniamo Off di default return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == 1) { // On // Codice 1 item.setChecked(true); return true; }else if(id == 2){ // Off // Codice 2 item.setChecked(true); return true; } return super.onOptionsItemSelected(item); } In questo caso, selezionare On od Off, porterà all’esecuzione di due codici distinti, ma rimarrà selezionato solamente l’ultimo ad essere stato selezionato, inoltre selezionare una voce già selezionata, non rimuoverà la spunta da essa, ma rieseguirà comunque il codice. Per modificare invece la proprietà Visible possiamo invece utilizzare il metodo setGroupVisible(int id, boolean Visibilità); Dove id è l’id del gruppo e Visibilità è la visibilità da assegnare, un esempio: @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(0, 1, 1 "Voce 1"); // Gruppo 0 menu.add(1, 2, 2 "Voce 2"); // Gruppo 1 menu.add(1, 3, 3 "Voce 3"); // Gruppo 1 menu.setGroupVisible(1, false); // Nasconderà Voce 2 e Voce 3 menu.findItem(2).setVisible(true); // Mostra Voce 2, Voce 3 rimane nascosta return true; } 76 Con menu.setGroupVisible(1, false); Nasconderemo tutte le voci del gruppo con id 1, mentre con menu.findItem(2).setVisible(true); Mostreremo nuovamente la Voce 2 precedentemente nascosta. Per modificare invece la proprietà Enabled utilizzeremo invece il metodo setGroupEnabled(int id, boolean enabled); dove id è l’id del gruppo ed enabled indica invece l’abilitazione del gruppo (true = abilitato), il funzionamento è analogo a setGroupVisible, ma modifica l’abilitazione e non la visibilità, ecco un esempio: @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(0, 1, 1 "Voce 1"); // Gruppo 0 menu.add(1, 2, 2 "Voce 2"); // Gruppo 1 menu.add(1, 3, 3 "Voce 3"); // Gruppo 1 menu.setGroupEnabled(1, false); // Disabiliterà Voce 2 e Voce 3 menu.findItem(2).setEnabled(true); // Abilita Voce 2, Voce 3 rimane disabilitata return true; } Come possiamo notare, il funzionamento è identico a setGroupVisible, l’unica differenza è nel risultato. Parliamo ora invece dell’ActionBar, abbiamo visto come visualizzare icone sull’ActionBar tramite il file xml, ma è anche possibile farlo manualmente, se la nostra applicazione ha un API minimo di 11 (Honeycomb) utilizzeremo il metodo setShowAsAction(int visibilità); Dove visibilità indica la visibilità sull’action bar e funziona in maniera analoga a quanto abbiamo visto sull’editor xml, altrimenti dovremmo richiamare la classe MenuItemCompat, i possibili valori sono: MenuItem.SHOW_AS_ACTION_NEVER – 0: Valore predefinito non mostra la voce sull’ActionBar, ma solo sul menu, il suo valore è 0. MenuItem.SHOW_AS_ACTION_IF_ROOM – 1: Mostra la voce sull’ActionBar solo se c’è spazio, altrimenti la mostra nel menu, il suo valore è 1. MenuItem.SHOW_AS_ACTION_ALWAYS – 2: Mostra sempre la voce sull’ActionBar, anche se non c’è spazio, per cui non viene mai mostrata nel menu, il suo valore è 2. MenuItem.SHOW_AS_ACTION_WITH_TEXT – 4: Mostra anche il titolo della voce oltre all’icona, va accompagnato ad ifRoom o always tramite l’operatore bitwise OR | il suo valore è 4. MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW – 8: Indica il già citato collapseActionView, come per withText va accompagnato ad always o ifRoom dall’operatore | il suo valore è 8. 77 Naturalmente possiamo anche assegnare un icona alla voce, per farlo possiamo utilizzare il metodo setIcon(int idIcona); dove idIcona è l’id di un icona, ad esempio item.setIcon(R.drawable.icona1); Vediamo ora un esempio: @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add("Voce 1"); // index 0 menu.add("Voce 2"); // index 1 menu.getItem(0).setIcon(R.drawable.icona1); // Impostiamo l’icona menu.getItem(0).setShowAsAction(MenuItem.SHOW_AS_ACTIONS_IF_ROOM); menu.getItem(1).setIcon(R.drawable.icona2); menu.getItem(1).setShowAsAction(1); // Equivale a ifRoom return true; } Questo ci mostrerà due icone nell’ActionBar, ma possiamo farlo solamente se utilizziamo un API minimo di almeno 11, altrimenti dobbiamo utilizzare necessariamente l’editor xml, altra cosa da notare è che comunque il parametro di setShowAsAction è un int, difatti SHOW_AS_ACTIONS_IF_ROOM altri non è che una costante static final della classe MenuItem dal valore di 1, per cui possiamo anche rimpiazzarlo con 1, possiamo anche modificare l’icona in qualsiasi momento, come la visibilità sull’ActionBar, ma solo su API >= 11, proviamo ora a cambiare l’icona al click di una voce. private byte b = 0; static final int[] ico = {R.drawable.verde, R.drawable.giallo, R.drawable.rosso}; private Menu m; … @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add("Semaforo"); // index 0 menu.getItem(0).setIcon(ico[0]); // Impostiamo l’icona su verde menu.getItem(0).setShowAsAction(MenuItem.SHOW_AS_ACTIONS_ALWAYS); m = menu; return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item == m.getItem(0)){ b++; if (b > 2) b = 0; item.setIcon(ico[b]); return true; } return super.onOptionsItemSelected(item); } 78 Abbiamo prima creato un array costante static final contenente gli id delle tre icone, poi abbiamo aggiunto la voce Semaforo e le abbiamo impostato l’icona verde e la visibilità sull’action bar su always, al click quindi la variabile b aumenterà di 1 e se supererà 2 tornerà a 0, infine assegnamo l’icona nella posizione b dell’array ico, quindi ad ogni click cambierà l’icona seguendo quest’ordine verde > giallo > rosso > verde > giallo … Ora tutto questo è possibile anche su API < 11, richiamando il metodo MenuItemCompat.setShowAsAction(MenuItem voce, int visibilità); Simile alla prcedente, ma appartenente alla classe MenuItemCompat e non alla classe MenuItem, per cui abbiamo come parametro aggiuntivo il MenuItem, il funzionamento è comunque analogo al metodo precedente, ecco un esempio: @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add("Voce 1"); // index 0 menu.add("Voce 2"); // index 1 MenuItem m1 = menu.getItem(0), m2 = menu.getItem(1); m1.setIcon(R.drawable.icona1); // Impostiamo l’icona MenuItemCompat.setShowAsAction(m1, MenuItemCompat.SHOW_AS_ACTIONS_IF_ROOM); m2.setIcon(R.drawable.icona2); MenuItemCompat.setShowAsAction(m2, 1); // Equivale a ifRoom return true; } Come possiamo vedere il funzionamento è molto simile al metodo “ufficiale” , non è necessario creare una variabile locale contenente il MenuItem, ma rende comunque il codice più pulito, il metodo setIcon rimane comunque funzionante. Con questo abbiamo snocciolato le basi di Eclipse e della programmazione Java in ambito Android, nel prossimo capitolo tratteremo invece argomenti più avanzati. 79 CAPITOLO 3 Lavoriamo con Eclipse 3.1 Gli Intent Abbiamo visto come sia possibile creare delle activity, ma queste activity devono poter anche essere visualizzate, per cui avremmo bisogno degli Intent, ovvero la classe che gestisce l’esecuzione delle activity, in modo predefinito Eclipse avvia l’Intent della nostra prima Activity, che se non l’abbiamo rinominata è MainActivity.java, ma possiamo comunque creare una nuova activity da visualizzare tramite un Intent, la sintassi è la seguente: startActivity(Intent intent); Dove intent è un’istanza della classe Intent, con il seguente costruttore: new Intent(Context contesto, Class<?> activity); Dove contesto è il contesto dell’applicazione e può essere nomeActivity.this; Dove nomeActivity è il nome dell’attuale Activity, ad esempio MainActivity.this; Oppure può essere ottenuto tramite il metodo getApplicationContext(); Per cui ipotizzando di voler avviare l’activity contenuta nel file Activity2.java, dovremmo utilizzare il seguente codice: startActivity(new Intent(getApplicationContext(),Activity2.class)); Bisogna però tenere bene a mente che l’avvio di una nuova activity, non comporterà la chiusura della vecchia activity, ma solo la sospensione, per cui chiudendo la nuova activity, verrà mostrata nuovamente l’ultima activity sospesa allo stato in cui è stata sospesa, per terminare un activity, possiamo utilizzare il metodo finish(); Bisogna inoltre ricordarsi che è possibile anche avviare più Intent contemporaneamente della stessa applicazione, per cui avviando un nuovo intent di un activity già in background, creerà una nuova istanza della stessa activity senza chiudere la precedente, terminando un activity con finish(); o con il tasto Indietro dei dispositivi Android, verrà mostrata l’ultima attività messa in backgroud dell’applicazione, se non ci sono activity in background, l’applicazione termina. Vediamo ora un esempio: public class Activity1 extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); startActivity(new Intent(getApplicationContext(),Activity2.class)); } 80 Questa semplice Activity, non fa altro che richiamare una seconda Activity, è passare in background, ora se dovessimo chiudere Activity2, Activity1 tornerebbe in primo piano, ma non richiamerà nuovamente Activity2 in quanto il metodo onCreate(Bundle savedInstanceState) viene eseguito solo all’avvio dell’intent e non al riavvio. 3.2 Il ciclo di vita di un Activity Ogni activity ha un suo ciclo di vita, che si divide in sette diversi stadi, ad ognuno dei quali è associato un metodo della classe Activity o ActionBarActvity, che tramite override ci permette di eseguire determinate operazioni quando entriamo in un determinato stadio del ciclo di vita, tra cui il famoso metodo onCreate, per far funzionare l’applicazione in modo corretto, l’override di tali metodi deve richiamare anche il metodo originale tramite super.nomeMetodo(); Ecco quindi perché all’inizio dell’override del metodo onCreate(Bundle savedInstanceState) abbiamo sempre super.onCreate(savedInstanceState); Analizziamo ora i vari stadi del ciclo di vita: onCreate – È il primo stadio del ciclo di vita, e si ha nel momento in cui viene istanziato l’intent dell’activity ossia quando utilizziamo il metodo startActivity, oppure quando avviamo l’applicazione, è legato al metodo onCreate(Bundle savedIstanceState); l’Activity entra nello stato Created. onStart – È il secondo stadio del ciclo di vita ed è lo stadio immediatamente successivo ad onCreate, è legato al metodo onStart(); l’Activity entra nello stato Started. onResume – È il terzo stadio del ciclo di vita viene subito dopo onStart, è legato al metodo onResume(); l’Activity entra nello stato Resumed. onPause – Viene chiamato quando l’applicazione viene sospesa, ma rimane comunque attiva, ad esempio un’applicazione entra nello stato onPause quando viene bloccato lo schermo del dispositivo con l’applicazione in primo piano, riportare in primo piano un applicazione nello stadio onPause, la riporterà allo stato onResume e quindi richiamerà anche il metodo onResume(); è legato al metodo onPause(); l’Activity entra nello stato Paused. onStop – Viene chiamato subito dopo onPause, se l’applicazione finisce in background, ad esempio perché è stata avviata una nuova Activity, oppure abbiamo premuto il tasto Home, ed è quindi sempre preceduto dallo stadio onPause, se l’Activity torna in primo piano passa allo stadio onRestart, è legato al metodo onStop(); l’Activity entra nello stato Stopped. 81 onRestart – questo stato si pone subito prima di onStart, ma viene eseguito solo in seguito allo stato onStop, quindi se l’Activity torna in primo piano dopo essere stata in background, passare allo stadio onRestart ci farà automaticamente eseguire anche i metodi onStart(); e onResume(); è legato al metodo onRestart(); l’activity torna allo stato Started. onDestroy – Si tratta dell’ultimo stadio del ciclo di vita dell’applicazione ed è sempre preceduto dagli stati onPause e onStop, entriamo nello stato onDestroy nel momento in cui chiudiamo l’Activity con finish(); oppure con il tasto Back, l’Activity viene terminata e rimossa dalla RAM, per eseguire nuovamente l’Activity è necessario chiamare un nuovo Intent della stessa, ripartendo dallo stadio onCreate, è legato al metodo onDestroy(); l’Activity viene terminate e quindi passa allo stato Destroyed. Nel momento in cui si entra in uno degli stadi del ciclo di vita, viene eseguito il relativo metodo, un Activity può entrare negli stadi onCreate e onDestroy solamente una volta per istanza, mentre pu entrare negli altri stadi un numero indefinito di volte, gli stadi del ciclo di vita vengono raggiunti in modo sequenziale, eccezion fatta per lo stadio onRestart, per cui nel suo ciclo di vita un activity normalmete entrerà sicuramente almeno una volta in ogni stadio del ciclo di vita, ad eccezione dello stadio onRestart, terminare però l’activity nello stadio onCreate, la porterà direttamente nello stadio onDestroy, mentre terminarla nello stadio onStart, la porterà direttamente allo stadio onStop e successivamente onDestroy, Ecco lo schema ufficiale del ciclo di vita di un Activity: 82 Come si evince dallo schema l’applicazione normalmente è nello stadio onResume ed è quindi Resumed (l’apice dello schema) gli stadi onCreate e onStart son invece transitori, come pure lo stato onRestart e onDestroy. Per eseguire delle operazioni durante un particolare stadio del ciclo di vita, dovremmo eseguire l’override del relativo metodo, tenendo in considerazione il fatto che si tratta di metodi void, che devono avere visibilità protected o public e che ad eccezione del metodo onCreate, non hanno parametri, bisogna inoltre ricordarsi di richiamare anche il metodo originale tramite super, per cui se vogliamo creare ad esempio un activity che esegua una determinata operazione nel momento in cui entra nello stato Stopped, potremmo utilizzare il seguente codice: int i = 0; … @Override protected void onStop(){ super.onStop(); // Richiamiamo il metodo originale i++; // Eseguiamo il nostro codice } In questo caso la nostra activity incrementerà la variabile i ogni qualvolta passerà allo stato Stopped, se avessimo fatto invece l’override del metodo onPause, la nostra Activity avrebbe incrementato i ogni qualvolta fosse passata allo stato Paused, ma siccome lo stato Stopped presuppone anche il passaggio dallo stato Paused, anche portare l’activity allo stato Stopped porterà ad un incremento di i. Richiamare il metodo originale è importante, poiché l’override di un metodo lo sovrascrive, mentre il nostro scopo è semplicemente quello di aggiungere codice a tali metodi, quindi richiamando il metodo originale all’inizio dell’override, è come se ci limitassimo ad aggiungere codice a tale metodo, non inserire il riferimento al metodo originale, porterà ad un crash del programma all’atto dell’esecuzione del metodo. 3.3 Passare i dati tra le activity Ora che abbiamo visto come richiamare una nuova Activity, potremmo aver bisogno di passare alcuni dati dell’activity precedente alla nuova activity, questo risultato può essere raggiunto in diversi modi. Il primo modo prevede l’utilizzo di variabili statiche, infatti anche dopo aver chiuso l’activity da cui vogliamo ottenere dei dati, è sempre possibile recuperarne il valore delle variabili statiche, le variabili statiche infatti avranno sempre l’ultimo valore che le è stato assegnato da una qualsiasi istanza di tale classe/activity, poiché la caratteristica delle variabili statiche è quella di essere legate direttamente alla propria 83 classe, e non alle sue istanze, come accade per le variabili non statiche, per cui esse sono accessibili in modo statico da qualsiasi classe purché visibili, vediamo ora un esempio: public class Classe1 extends ActionBarActivity { protected static int st; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_classe1); st = 2; } @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add("Avvia Classe2"); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { //C’è solo una voce, l’if non è necessario st = 5; startActivity(getApplicationContext(), Classe2.class); finish(); return true; } public class Classe2 extends ActionBarActivity { private int i; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_classe2); } @Override protected void onStart(){ super.onStart(); i = Classe1.st; } @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add("Avvia Classe1"); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { startActivity(getApplicationContext(), Classe1.class); return true; } In questo esempio abbiamo l’Activity Classe1 con la variabile statica st, alla creazione le viene assegnato valore 2, poi selezionando dal menu la voce Avvia 84 Classe2, st assumerà valore 5, verrà avviata l’Activity Classe2 e terminata l’Activity Classe1, all’avvio di Classe2 nello stato Started recuperiamo il valore di st, tramite Classe1.st e lo assegnamo alla variabile i, per cui i varrà 5, ossia l’ultimo valore assegnato a st, anche se l’activity Classe1 è stata terminata, selezionando Avvia Classe1, verrà riaperta Classe1 da zero, e siccome non abbiamo utilizzato finish() Classe2 passerà allo stato Stopped, ora terminado Classe1 con in tasto Back, verrà riavviata Classe2 che passerà allo stadio onRestart, per poi passare nuovamente allo stato Started, rieseguendo quindi il metodo onStart(), ora però i varrà 2 in quanto l’ultimo valore assegnato a st è 2, dato che abbiamo terminato la nuova istanza di Classe1 mentre st valeva ancora 2, anche se non avessimo terminato la prima istanza di Classe1 con finish() il risultato sarebbe stato identico, ma con la differenza che chiudendo Classe2 con il tasto Back, saremmo tornati alla prima istanza di Classe1. Cosi facendo, possiamo passare a qualsiasi Activity i valori delle variabili statiche delle activity precedenti, se la classe da cui dobbiamo prendere i dati non è stata mai inizializzata, le sue variabili statiche avranno comunque i valori di inizializzazione se inizializzate alla creazione, altrimenti avranno il valore di default. In alternativa possiamo passare dati ad un'altra Activity, tramite l’Intent, singolarmente o tramite un Bundle, per prima cosa definiamo cos’è un Bundle, esso è un particolare tipo di variabile, che a dire il vero altri non è che un istanza della classe Bundle, un oggetto di tipo Bundle, come un array, può contenere diversi dati tramite una sola variabile, ma a differenza di un array, può contenere diversi tipi di dati, inoltre i dati sono inseriti e letti tramite dei metodi della classe Bundle, la sintassi per creare un Bundle vuoto è la seguente: Bundle nomeBundle = new Bundle(); Mentre per l’inserimento dei dati utilizzeremo vari metodi, vediamo ora un esempio: Bundle bund = new Bundle(); int i1, i2; String s; bund.putInt("alpha",1); bund.putInt("beta", 2); bund.putString("gamma", "Stringa"); i1 = bund.getInt("alpha"); // i1 = 1 i2 = bund.getInt("beta"); // i2 = 2 s = bund.getString("gamma"); // s = "Stringa" Per inserire i dati in un Bundle bisogna usare il metodo relativo al tipo di dato che vogliamo inserire, ad esempio putInt per valori di tipo integer, i metodi hanno come parametri una stringa, in cui inserire l’identificativo del campo ed il valore che vogliamo inserire, per recuperare il valore, basta usare il metodo adatto ad esempio 85 getInt per recuperare un valore di tipo int, la stringa identificativa del metodo put_, ci servirà come parametro del metodo get_, inserire dati con un identificativo già presente, porterà alla sovrascrittura del vecchio dato, anche se di tipo differente, es: Bundle bund = new Bundle(); int i; String s; bund.putInt("Key",1); bund.putString("Key", "Stringa"); bund.putString("Key", "Nuova Stringa"); i1 = bund.getInt("Key"); // i1 = 0 - Non esiste più un int dall’identificativo Key. s = bund.getString("Key"); // s = "Nuova Stringa" - "Stringa" è stato sovrascritto. È anche possibile eliminare valori da un Bundle tramite svuotarlo completamente tramite clear(); in questo modo: remove(String key); oppure Bundle bund = new Bundle(); bund.putInt("Key1",1); bund.putInt("Key2",2); bund.putInt("Key3",3); bund.remove("Key2"); // Elimina Key2, rimangono Key1 e Key3 bund.clear(); // Elimina tutto, bund è vuoto È anche possibile utilizzare il metodo generico get(String key); che ci restituirà un valore di tipo Object, che dovrà poi essere convertito nel tipo di variabile appropriato, ad esempio: Bundle bund = new Bundle(); Static final String[] key = {"Key1","Key2","Key1"} Object[] obj = new Object[3]; // Creiamo un array Object int i; double d; String s; bund.putInt(key[0], 3); bund.putString(key[1], "Stringa"); bund.putDouble(key[2], 1.5); for (byte b = 0, b < 3, b++){ obj[b] = bund.get(key[b]); } i = (Integer) obj[0]; // Il cast va fatto in oggetto Integer e non variabile int i = (String) obj[1]; // Il cast va fatto in oggetto String d = (Double) obj[2]; // Il cast va fatto in oggetto Double e non variabile double Così facendo possiamo creare array Object che contengono diversi tipi di valori, facendo però attenzione al fatto che i valori contenuti nelle variabili Object, devono essere convertiti tramite casting nelle relativa variabili, ma castando però l’oggetto relativo al tipo di variabile e non il tipo di variabile, per cui per recuperare un valore intero da una variabile Object, dovremmo castarla con (Integer) e non (int) quindi: i = (int) obj[0]; // Errore, non si può fare il cast da Object a int i = (Integer) obj[0]; // Ok 86 Per cui: byte b = (Byte) obj[0]; short s = (Short) obj[1]; int i = (Integer) obj[2]; long l = (Long) obj[3]; float sn = (Float) obj[4]; double d (Double) obj[5]; String s = (String) obj[6]; Dopo aver creato un Bundle, è possibile passarlo ad un'altra Activity tramite l’intent nel seguente modo Bundle bundle = new Bundle(); Intent intent = new Intent(getApplicationContext, Activity2.class); bundle.putString("nome","Mario"); bundle.putInt("anno", 1975); intent.putExtras(bundle); startActivity(intent); Possiamo poi recuperare il Bundle nel seguente modo: Intent intent = getIntent(); Bundle bundle = intent.getExtras(); Possiamo anche inserire dei Bundle all’interno di un Bundle, tramite il metodo putBundle(String key, Bundle bundle); recuperabile poi con getBundle(String key); in questo modo possiamo passare diversi Bundle, tramite un unico Bundle, è anche possibile inserire in un Bundle tutti i valori di un altro Bundle in aggiunta ai valori già contenuti in esso tramite il metodo putAll(Bundle bundle); è inoltre anche possibile inserire array monodimensionali, oppure ArrayList, per far ciò possiamo usare il metodo appropriato al tipo di array o ArrayList, ad esempio nel caso di un array int, putIntArray(String key, int[] array); se l’array è multidimensionale, dobbiamo dividerlo in più array monodimensionali, ad esempio: int[][] arrayOriginale = {{0,1,2},{3,4,5}}; int[][] arrayCopia = new int[2][3]; // Array vuoto 2x3 Bundle bund = new Bundle(); // Dividiamo l’array bund.putIntArray("0",arrayOriginale[0]); bund.putIntArray("1",arrayOriginale[1]); // Recuperiamo e riuniamo l’array arrayCopia[0] = bund.getIntArray("0"); arrayCopia[1] = bund.getIntArray("1"); Così facendo, è quindi anche possibile trasferire array multidimensionali tramite un Bundle, dividendolo in più array monodimensionali, naturalmente più dimensioni avrà un array, è più difficoltoso sarà scomporlo in array monodimensionali. 87 Parlando di array, abbiamo nominato gli ArrayList, si tratta in fatti di un tipo di oggetto a cavallo tra un Array e un Bundle, come nel caso dei Bundle, sono un tipo di oggetto e sono quindi legati ad una classe, di conseguenza è necessario utilizzare dei metodi per inserire o recuperare i dati nell’ArrayList, e non possiamo quindi semplicemente usare il simbolo = come nel caso degli array, gli ArrayList inoltre sono generalmente monodimensionali, ma possono contenere anche altri ArrayList, per cui possono diventare anche multidimensionali, similarmente agli array però utilizzano un identificativo di tipo int, e non String come per i Bundle, inoltre analogamente agli array, essi possono contenere solamente un tipo di dati, anche se questo limite può essere aggirato, come nel caso dei comuni array inserendo dati di tipo Object, ArrayList è un tipo grezzo, per cui bisognerà parametrizzarlo, parleremo a breve delle classi e dei tipi grezzi, comunque sia la sintassi di un ArrayList è la seguente: ArrayList<Tipo> = new ArrayList<Tipo>(dimensioneIniziale); Dove Tipo è un tipo di oggetto non primitivo, quindi ad esempio Integer e non int, mentre dimensioneIniziale è la dimensione iniziale dell’ArraList, può anche essere lasciato vuoto, a differenza però dei normali array, è comunque possibile aggiungere oggetti oltre la dimensione iniziale dell’ArrayList, in tal caso l’ArrayList si estenderà automaticamente, per cui può risultare una valida alternativa agli array, se non sappiamo da subito quanti dati dovrà contenere, come per i Bundle bisogna utilizzare dei metodi per inserire e recuperare dati, inoltre potendo contenere un solo tipo di dato definito alla creazione, non abbiamo diversi metodi per ogni tipo di dato, vediamo ora un esempio di ArrayList: ArrayList<Integer> al int i; al.add(5); // Indice al.add(10); // Indice al.add(15); // Indice i = al.get(1); // i = = new ArrayList<Integer>(3); 0 1 2 10; Con il metodo add(Tipo Oggetto); è possibile aggiungere dati all’ArrayList, per inserire dati in una posizione specifica invece possiamo utilizzare add(int posizione, Tipo Oggetto); ma solo se stiamo puntando ad una posizione esistente, altrimenti verrà generato un errore, inserendo un dato in una determinata posizione il dato precedentemente in quella posizione e tutti i dati successivi verranno spostati avanti di una posizione, con set(int posizione, Tipo Oggetto); possiamo invece sostituire un dato in una determinata posizione, senza spostare gli altri dati, con clear(); invece possiamo azzerare l’ArrayList. 88 È anche possibile creare ArrayList contenenti altri ArrayList, e quindi multidimensionali, nel seguente modo: ArrayList<ArrayList<Integer>> arrayMulti = new ArrayList<ArrayList<Integer>>(); ArrayList<Integer> al1 = new ArrayList<Integer>(), al2 = new ArrayList<Integer>(); int i; al1.add(1); al1.add(2); al1.add(3); al2.add(4); al2.add(5); al2.add(6); //Index //Index //Index //Index //Index //Index 0 1 2 0 1 2 arrayMulti.add(al1); //Index 0 {1,2,3} arrayMulti.add(al2); //Index 1 {4,5,6} i = arrayMulti.get(1).get(0); // i = 4 In questo modo abbiamo creato un ArrayList bidimensionale inserendo due ArrayList in un altro ArrayList, gli ArrayList possono essere inseriti anche nei Bundle, ma solo determinati tipi di ArrayList, per cui ad esempio gli ArrayList che contengono altri ArrayList non possono essere inseriti, ma devono essere scomposti in ArrayList semplici, proprio come avviene per gli Array normali. Per inserire un ArrayList all’interno di un Bundle basta utilizzare il metodo adeguato al tipo di ArrayList, quindi se abbiamo un ArrayList<Integer> utilizzeremo il metodo putIntegerArrayList(String key, ArrayList<Integer> arraylist); È anche possibile passare dati tra le Activity semplicemente attraverso l’Intent, senza utilizzare i Bundle, ma inserendo i dai direttamente nell’Intent, in questo caso però non utilizzeremo il metodo putExtras, che vale solo per l’inserimento di un Bundle, ma useremo invece il metodo putExtra senza s finale, inoltre a differenza di putExtras dovremmo inserire come ulteriore parametro anche un identificativo della risorsa, che deve iniziare con il nome del pacchetto, nome che può essere recuperato tramite il metodo getPackageName(); Vediamo quindi un esempio: package com.test.alpha; … int i = 18; String s = "Stringa"; final String pkg = getPackageName(); // Recuperiamo Intent intent = new Intent(getApplicationContext(), intent.putExtra(pkg + "Numero", i); // Inseriamo il intent.putExtra(pkg + "Testo", s); // Inseriamo il startActivity(intent); // Avviamo Activity2 … Possiamo poi recuperare I dati così: il Package Name Activity2); valore di i valore di s 89 … int i; String s; final String pkg = getPackageName(); Intent intent = getIntent(); i = intent.getIntExtra(pkg + "Numero", 0); // i = 18 s = intent.getStringExtra(pkg + "Testo"); // s = "Stringa" … A differenza del metodo di inserimento, il metodo di recupero varia in base al tipo di dato da recuperare, inoltre in caso di valori numerici, dovremmo definire anche un default value, ovvero un valore da restituire nel caso in cui il dato non venga trovato, nel nostro caso, zero. Questi sono i principali modi per passare dati tra le Activity, in alternativa potremmo anche memorizzare i dati in un database sql oppure su di un file esterno, per poi recuperarli da un qualsiasi Activity, ma ciò è necessario solo se vogliamo conservare le informazione anche dopo la chiusura dell’applicazione, comunque sia tratteremo l’argomento in seguito. 3.4 Il file R.java Andando a vedere la composizione del nostro progetto, noteremo la cartella gen, all’interno di essa ci dovrebbero essere uno o più pacchetti, tra cui il pacchetto della nostra applicazione (lo stesso che troviamo nella cartella src) contenente due files, un dei quali R.java, questo file è molto importante, in quanto tiene traccia di tutti gli id dei nostri oggetti, per cui inserendo ad esempio una EditText nel file di Layout, con ad esempio l’id EditText1 nel file R.java all’interno della nested class id, verrà inserita una nuova costante static final EditText1 di tipo int, con un valore esadecimale, un valore esadecimale è un particolare tipo di numero a base 16 e non a base 10 come i normali numeri decimali per cui ogni cifra può rappresentare 16 valori diversi (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f) per cui ad esempio il numero 10 in esadecimale tradotto in decimale diverrebbe 16, mentre il numero 10 decimale diventerebbe il numero a in esadecimale, normalmente i numeri esadecimali vengono utilizzati in informatica, poiché una coppia di cifre esadecimale rappresenta esattamente un byte, infatti 162 = 256, trattandosi int di una variabile a 32 bit i valori esadecimali utilizzati sono ad 8 cifre, essendo una coppia di cifre equivalenti ad 8 bit, 8/2*8 = 32, per definire un valore esadecimale, basta inserire prima del numero il prefisso 0x quindi per scrivere 255 in esadecimale, ci basterà scrivere 0xff, le variabili int però sono signed, quindi sono considerati valori positivi tutti quelli compresi tra 0x0 e 0x7fffffff, mentre 0x80000000 è il valore più basso, per cui ad ogni variabile è assegnato un valore int che ci tornerà utile a breve. 90 3.5 Gestire gli oggetti Abbiamo già visto come modificare il file xml contenete la parte grafica dell’Activity o del Fragment, abbiamo inoltre anche visto alcune tipologie di oggetti che possiamo inserire all’interno di tale file, come ad esempio i Button o le EditText, ma non sappiamo ancora come interagire con tali oggetti, per farlo infatti ricorreremo proprio al sopracitato file R.java ed al metodo findViewById(int id); contenuto sia nella classe Activity, che nella classe View, per poter interagire con un qualsiasi oggetto, dobbiamo prima assegnarlo ad una variabile, per fare queso utilizzeremo findViewById(int id); un metodo che ci restituirà un valore di tipo View, che come abbiamo visto in precedenza per Object, dovremmo poi convertire tramite cast nel tipo di oggetto relativo, il parametro id altri non è che il valore della variabile relativa al nostro oggetto presente nel file R.java, potremmo quindi inserire tale valore come parametro, oppure recuperarlo direttamente dalla classe R tramite R.id.nomevariabile, per cui immaginiamo di inserire nel layout dell’activity un EditText dall’id EditText1, potremmo interagire con l’oggetto nel seguente modo: File R.java … static final int EditText1 = 0x7f001534; … File MainActivity.java … import android.widget.EditText; … EditText et1 = (EditText) findViewById(0x7f001534); // Oppure in decimale EditText et2 = (EditText) findViewById(2130711860); // Oppure con riferimento alla classe R EditText et3 = (EditText) findViewById(R.id.EditText1); … Sia et1, et2 ed et3 fanno riferimento allo stesso oggetto, questo poiché R.id.EditText1 è semplicemente un riferimento alla costante di tipo int contenuta nel file R.java, per cui scrivere R.id.EditText1 oppure il suo valore non fa alcuna differenza, sia esso scritto in decimale o esadecimale, comunque sia è molto più semplice e quindi consigliabile l’utilizzo del riferimento alla classe R anche se non strettamente necessario, anche per una questione di leggibilità del codice, inoltre bisogna ricordarsi di importare gli oggetti con cui andremo ad interagire, in questo caso android.widget.EditText; ma non necessariamente quelli con cui non interagiamo, ad esempio una TextView con cui non interagiamo. 91 Una volta creata la variabile relativa all’oggetto, possiamo utilizzarla per interagire con l’oggetto attraverso i suoi metodi, per cui ad esempio vediamo ora come recuperare il testo inserito dall’utente in un EditText ed inserirlo in una TextView. … private EditText et = (EditText) findViewById(R.id.EditText1); private TextView t = (TextView) findViewById(R.id.TextView1); … String tmp = et.getText().toString; t.setText(tmp); // Oppure t.setText(et.getText().toString()); Così facendo, abbiamo recuperato il testo inserito dall’utente nell’EditText tramite il metodo getText(); per le editText dobbiamo usare anche il metodo toString(); in quanto il metodo getText(); restituisce un Editable, le TextView invece restituiscono CharSequince, così facendo abbiamo passato ad una variabile locale tmp, per poi inserirlo nella TextView tramite il metodo setText(CharSequence testo); naturalente il tutto può essere fatto anche in un solo passaggio, senza utilizzare la variabile locale tmp, come nel secondo esempio. Per quanto riguarda il metodo findViewById(int id); esso è contenuto nella classe Activity, e grazie al principio di ereditarietà, viene ereditato anche dalla nostra Activity, che estende ActionBarActivity, che a sua volta estende FragmentActivity che estende Activity, e così via, difatti la gerarchia completa della nostra classe è la seguente: NostraActivity < ActionBarActivity < FragmentActivity < Activity < ContextThemeWrapper < ContextWrapper < Context < Object Per cui ne eredita tutti i metodi visibili delle classi contenute nella gerarchia, salvo i metodi originali sovrascritti, di cui verrà ereditato solo l’override, quindi anche il metodo findViewById(int id); contenuto nella classe Activity, se decidiamo di lavorare nel Fragment però potrebbe presentarsi un problema, infatti la classe del fragment non estende FragmentActivity, ma Fragment, che non estende nient’altro se non la classe Object estesa di default da tutte le classi, per cui non eredita il metodo findViewById(int id); dalla classe Activity, ma come detto in precedenza però tale metodo è contenuto anche nella classe View, ed il Fragment autogenerato da Eclipse è il seguente: public static class PlaceholderFragment extends Fragment { public PlaceholderFragment() { } 92 @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main, container, false); return rootView; } } Nel codice abbiamo la variabile locale rootView di tipo View, ed essendo findViewById(int id); contenuto anche nella classe View, possiamo utilizzarlo nel fragment nel seguente modo: … public static class PlaceholderFragment extends Fragment { private EditText et; private TextView t; public PlaceholderFragment() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main, container, false); et = (EditText) rootView.findIdByView(R.id.EditText1); t = (TextView) rootView.findIdByView(R.id.TextView1); return rootView; } } In questo caso, abbiamo usato la variabile locale rootView per utilizzare il metodo findIdByView(int id); essendo una variabile locale, possiamo utilizzarla solo all’interno del metodo onCreateView, salvo renderla globale, comunque sia, una volta passati tutti i riferimenti agli oggetti, all’interno delle rispettive variabili globali, non avremmo più bisogno del metodo findViewById(int id); per interagire con tali oggetti, ma solo le variabili e i relativi metodi. Ogni oggetto ha metodi diversi, ma molti metodi sono comuni a molti oggetti, inoltre ogni oggetto ha sia dei metodi setter che ci permettono di modificare degli attributi, sia dei metodi getter, che ci permettono invece di leggere tali attributi. Gli oggetti una volta assegnati a delle variabili, sono considerati variabili a tutti gli effetti, per cui possono essere anche passati come parametro del costruttore di una classe esterna, oppure come parametro di un metodo di tale classe ad esempio: Classe1: 93 … public class Classe1 extends ActionBarActivity{ … TextView t = (TextView) findIdByWiew(R.id.TextView1); … new Classe2(t); // Passiamo al costruttore di Classe2 la TextView t … } Classe2: package com.test.alpha; import android.widget.TextView; public class Classe2{ public Classe2(TextView arg0){ arg0.setText("Test"); } } Così facendo abbiamo creato una classe esterna con un costruttore, che ha come parametro una variabile di tipo TextView, e che sostituisce il testo di tale TextView con “Test”, per cui eseguendo Classe1, qualunque fosse il testo di TextView1, su schermo apparirà Test. 3.6 Gestire i click Fino ad ora abbiamo visto come creare programmi che eseguono determinate operazioni in maniera sequenziale, poi abbiamo visto come rompere questa sequenzialità tramite l’utilizzo del menù, ora vedremo invece come gestire i click su alcuni oggetti, ed eseguire del codice al verificarsi di tali click, questo renderà le nostre applicazioni interattive, dato che non ci limiteremo più ad eseguire delle operazioni in maniera automatica e sequenziale, ma potremo finalmente iniziare ad interagire con le nostre applicazioni. Per fare questo, ci basterà utilizzare il metodo della classe View: setOnClickListener(View.OnClickListener l); Trattandosi di un metodo contenuto nella classe View, può essere utilizzato da qualsiasi oggetto che estenda tale classe, o una sua sottoclasse, e dato che praticamente tutti i Widget estendono direttamente o indirettamente la classe View, tale metodo può essere utilizzato da praticamente tutti i Widget, anche se normalmente viene utilizzato con i Button o le ImageView, prima di parlare di tale metodo però, bisogna chiarire il fatto che sebbene normalmente gli 94 Override si facciano all’interno della subclasse, è anche possibile farli nel momento in cui essa viene istanziata e tale override varrà solo per tale istanza, facciamo ora un esempio: ClasseX: package com.test.beta; public class ClasseX{ public ClasseX(){ } public boolean metodoX(){ return false; } } ClasseY: package com.test.beta; public class ClasseY{ private boolean a, b; public ClasseY(){ ClasseX x1 = new ClasseX(){ @Override public boolean metodoX(){ return true; } }; ClasseX x2 = new ClasseX(); a = x1.metodoX(); // a = true b = x2.metodoX(); // b = false } } Così facendo solo l’istanza x1 di ClasseX riceve l’Override del metodo MetodoX, e quindi restiturà true, nell’istanza x2 invece non viene eseguito l’Override, quindi viene eseguito il metodo originale che restituirà invece false, questo esempio ci tornerà subito utile, in quanto il metodo setOnClickListener, ha come parametro un oggetto di tipo View.OnClickListener, che altri non è che un interfaccia all’interno della classe View, le interfacce infatti sono considerate comunque classi, e quindi possono essere contenute anche all’interno di una classe, tali interfacce possono poi quindi essere istanziate tramite la loro outerclass con un oggetto del tipo OuterClass.Interfaccia, istanziare però un interfaccia in questo modo, ci obbligherà 95 anche ad eseguire l’Override di tutti i metodi astratti contenuti nell’interfaccia, per cui immaginiamo di riscrivere l’esempio precedente utilizzando un interfaccia all’interno di ClasseX, il risultato sarebbe il seguente: ClasseX: package com.test.beta; public class ClasseX{ //Il costruttore non è necessario, istanzieremo l’interfaccia. public interface IntX{ public boolean MetodoX(); } } ClasseY: package com.test.beta; public class ClasseY{ private boolean a, b; public ClasseY(){ ClasseX.IntX x1 = new ClasseX.IntX(){ @Override public boolean metodoX(){ // TODO Auto-generated method stub return true; } }; ClasseX.IntX x2 = new ClasseX.IntX(){ @Override public boolean metodoX(){ // TODO Auto-generated method stub return false; } }; a = x1.metodoX(); // a = true b = x2.metodoX(); // b = false } } Il risultato alla fine non cambia, ma cambia il fatto che non dobbiamo più creare un oggetto ClasseX, ma ClasseX.IntX, inoltre trattandosi di un interfaccia, ne momento in cui digitiamo new ClasseX.IntX() Eclipse ci genererà in automatico l’Override del metodoX(); inserendoci il commento // TODO Auto-generated method stub e la stessa cosa avverrà anche nel momento in cui andremo ad istanziare l’interfaccia della classe 96 View: new View.OnClickListener(), detto questo quindi, andiamo a vedere come eseguire del codice al click di un pulsante: … import android.widget.Button; … public class MainActivity extends ActionBarActivity{ private Button bt = (Button) findViewById(R.id.Button1); private int i = 0; … bt.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub i++; } }); … } Questa semplice applicazione, contiene un Button, precedentemente definito nel file di Layout xml predefinito, ad ogni pressione del pulsante la variabile i viene incrementata di 1, inoltre nel momento in cui andremo a scrivere new View.OnClickListener(), Eclipse ci genererà in automatico l’Override del metodo onClick(View v) in cui andare a definire ciò che deve accadere alla pressione del Button, ma come detto prima, non dobbiamo utilizzare obbligatoriamente un Button, ma possiamo rendere cliccabile anche ad esempio un ImageView, questo ci permetterà di creare dei pulsanti personalizzati. Proviamo ora a far interagire più oggetti tra loro: package com.test.beta; import import import import android.support.v7.app.ActionBarActivity; android.os.Bundle; android.widget.Button; android.widget.ProgressBar; public class MainActivity private final Button meno private final Button più private final ProgressBar private int val = 50; extends ActionBarActivity { = (Button) findViewById(R.id.Button1); = (Button) findViewById(R.id.Button2); pb = (ProgressBar) findViewById(R.id.ProgressBar1); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); pb.setProgress(val); pb.setMax(100); meno.setOnClickListener(new View.OnClickListener() { @Override 97 public void onClick(View v) { aggiorna(-1); } }); più.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { aggiorna(1); } }); } private void aggiorna(int arg0){ val += arg0; if (val > 100) val = 100; else if (val < 0) val = 0; pb.setProgress(val); } } Questa volta ho voluto mostrare la Classe completa, ma senza Fragment e Menù, in quanto non necessari per un applicazione così semplice, in questa Activity abbiamo tre costanti, contenenti i riferimenti ai due Button più e meno e alla ProgressBar, che altri non è che una barra di caricamento, ora abbiamo iniziato assegnando alla ProgresBar un valore massimo, ed un valore iniziale, ma questo è superfluo in quanto tali valori possono essere impostati nel file di Layout, ed infine abbiamo una variabile int dal valore 50, ora benché la variabile spazierà da 0 a 100, è comunque consigliabile utilizzare una variabile int, in quanto i numeri interi generalmente sono cosiderati int, quindi per rendere un numero intero byte, è necessario un cast, qusto per evitare confusione negli overloading, poiché ad esempio potremmo avere un metodo con un parametro int, ed un altro con parametro byte, se dessimo come parametro 10, il compilatore non capirebbe se si tratta di un parametro byte o int, per cui considera tutti i numeri interi, tranne in fase di inizializzazione di variabile int, Abbiamo inoltre il metodo aggiorna(int arg0) che aggiunge alla variabile val il parametro arg0, poi controlla se supera 100 o è inferiore a 0 riporta il valore di val entro il range 0-100, infine imposta il progresso della ProgressBar al valore di val con setProgress(val), infine abbiamo l’interazione con i due Button, premendo su meno, verrà eseguito il metodo aggiorna con parametro -1 mentre premendo il pulsante più verrà eseguito il metodo aggiorna con parametro 1, come risultato si avrà un riempimento dell’1% della ProgressBar alla pressione del pulsante più ed uno svuotamento dell’1% alla pressione del pulsante meno. 98 3.7 Interagire con l’utente Spesso per eseguire determinate operazioni, abbiamo bisogno di alcuni dati inseriti dall’utente, ad esempio se volessimo realizzare una semplice calcolatrice con le quattro operazioni fondamentali, potremmo usare ad esempio un EditText per ricevere in input i numeri dall’utente, e nonostante le EditText, contengano testo, è anche possibile renderle numeriche tramite l’attributo Input Type da editor xml oppure tramite il metodo setInputType(int tipo); da programma, comunque sia ciò applicherà solo una maschera all’EditText ciò però non cambierà il tipo di ritorno del metodo getText(); per cui dovremmo convertire la CharSequence numerica in un numero intero o naturale, tramite i rispettivi metodi della classe del tipo di variabile in cui vogliamo convertire, per i numeri interi Integer.parseInt(String valore); mentre per i numeri naturali, Double.parseDouble(String valore); esistono tali metodi però anche per altri tipi di variabili numeriche ad esempio Byte.parseByte(String valore); tutti metodi che ci restituiranno un valore numerico contenente la trasposizione del valore numerico di una Stringa, quindi ad esempio: int i = Integer.parseInt("-125"); // i = -125 double d = Double.parseDouble("12.56"); // d = 12.56 Ma genererà un errore se la stringa contiene caratteri non validi o comunque una stringa che non rappresenti il tipo di ritorno, ad esempio: int e1 = Integer.parseInt("12j"); // Errore, j non ammesso int e2 = Integer.parseInt("12.56"); // Errore, valore non intero int e3 = Integer.parseInt("0x25ff"); // Errore, ammessi solo valori decimali int e4 = Integer.parseInt("10-2"); // Errore, - ammesso solo all’inizio byte e5 = Byte.parseByte("130"); // Errore, valore fuori scala è però possibile utilizzare valori non decimali aggiungendo il parametro int radix, che se omesso equivale a 10, per cui ad esempio potremmo specificare 16 per una stringa esadecimale oppure 2 per una stringa binaria, ma anche valori personalizzati, per cui ad esempio inserendo come radix 17 verrà calcolata anche la g come cifra valida, oppure inserendo 4 verranno considerati validi solo i numeri da 0 a 3 es: int i1 = Integer.parseInt("ff5d", 16); // Ok i1 = 65373 range 0-f int i2 = Integer.parseInt("gh", 18); // Ok i1 = 305 range 0-h int i3 = Integer,parseInt("4231", 4); // Errore range 0-3 Vediamo ora come realizzare una semplice calcolatrice Per prima cosa inseriamo due EditText dall’id editText1 ed editText2, inseriamo poi quattro Button dagli id piu, meno, per e div ed infine una TextView dall’id: textView1, dopo aver definito il Layout passiamo al codice vero e proprio: 99 package com.test.beta; import import import import import android.support.v7.app.ActionBarActivity; android.os.Bundle; android.widget.Button; android.widget.EditText; android.widget.TextView; public class Calcolatrice extends ActionBarActivity { private private private private private private private final final final final final final final Button meno = (Button) findViewById(R.id.meno); Button più = (Button) findViewById(R.id.piu); Button per = (Button) findViewById(R.id.per); Button div = (Button) findViewById(R.id.div); EditText et1 = (EditText) findViewById(R.id.editText1); EditText et2 = (EditText) findViewById(R.id.editText2); TextView ris = (TextView) findViewById(R.id.textView1); // Semplifichiamo i flag private static final int private static final int private static final int dell’imput type, anche se non necessario t_num = InputType.TYPE_CLASS_NUMBER; f_dec = InputType.TYPE_NUMBER_FLAG_DECIMAL; f_sig = InputType.TYPE_NUMBER_FLAG_SIGNED; private double a = 0, b = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_calcolatrice); // Rendiamo le EditText numeriche con flag decimale e signed // Proprietà che possiamo anche definere da Layout xml et1.setInputType(t_num | f_dec | f_sig); et2.setInputType(t_num | f_dec | f_sig); ris.setText("Risultato: 0.0"); meno.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { assegna(); ris.setText("Risultato: " + (a – b)); } }); più.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { assegna(); ris.setText("Risultato: " + (a + b)); } }); div.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { assegna(); ris.setText("Risultato: " + (a / b)); } }); 100 per.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { assegna(); ris.setText("Risultato: " + (a * b)); } }); } private void assegna(){ // Assegnamo i valori delle EditText alle rispettive variabili a = Double.parseDouble(et1.getText().toString()); b = Double.parseDouble(et2.getText().toString()); } } Per prima cosa abbiamo creato le variabili relative ai vari widget, e le abbiamo inizializzate tramite il metodo findViewById(int id); Poi abbiamo assegnato manualmente gli ImputType delle due EditText, rendendole numeriche con i flag signed e decimal, quindi l’edit text aggetterà soltanto numeri in quanto number, un eventuale punto tra le cifre in quanto decimal, ed anche un eventuale meno all’inizio in quanto signed, ciò non va necessariamente fatto dalla classe Calcolatrice, ma può essere fatto dal relativo file xml in questo caso activity_calcolatrice, abbiamo inoltre creato il metodo void assegna(); che altro non fa che assegnare alle variabili double a e b i valori convetiti tramite il metodo Double.parseDouble(String valore); delle due EditText, valori ottenibili con i metodi getText().toString(); in quanto parseDouble non accetta come parametro un Editable. Infine abbiamo reso i quattro Button cliccabili, ognuno di essi eseguirà prima il metodo assegna, e poi cambierà il testo della TextView ris in: Risultato: x dove x è il risultato dell’operazione relativa al Button tra le variabili a e b quindi ad esempio premendo il Button per si avrà Risultato: a*b, quindi dando in input 6 e 3 si avranno i seguenti risultati: più: Risultato: 9.0 meno: Risultato: 3.0 per: Risultato: 18.0 div: Risultato: 2.0 Essendo a e b variabili Double, il risultato verrà mostrato come Double, e quindi avrà sempre almeno una cifra decimale, anche se tale cifra è 0, questo sempre per non creare confusione nei parametri dei metodi, ad esempio: 101 public class Classe1{ double x = 0; byte b = 3; // Ok in assegnazione public Classe1(){ x = metodoX(5); // x = 50.0 --- 5 è int x = metodoX((byte) 5); // x = 5.0 --- byte necessita di cast, altrimenti è int x = metodoX(5.0); // x = 0.5 --- i numeri con il punto sono considerati double b = 5; // Errore, 5 è int b = (byte) 5; // Ok, è necessario il cast } public byte metodoX(byte a){ return a; } public int metodoX(int a){ return a*10; } public double metodoX(double a){ return a/10; } } In questo caso abbiamo due overload di un metodo, il compilatore considererà int tutti i valori interi e double tutti i valori con cifre decimali, quindi per far considerare al compilatore come double un numero intero dobbiamo aggiungerci .0 quindi per far considerare il numero intero 5 come double, dobbiamo scrivere 5.0, mentre per farlo considerare come ad esempio byte, dovremmo invece farne il cast quindi (byte) 5 altrimenti non verrà accettato come parametro byte, anche se non esistono overload di tale metodo, inoltre convertendo una variabile double in stringa essa verrà rappresentata con la cifra decimale, anche se si tratta di un numero intero come ad esempio 50 che viene rappresentato 50.0, e con questo chiudiamo la nostra parentesi sui parametri numerici. Ora il codice che abbiamo appena mostrato però non è però affidabile al 100% in quanto anche se l’EditText è impostata per non permettere l’inserimento di valori non numerici, potrebbero comunque venir inseriti valori non validi, ovvero lasciando la casella vuota oppure inserendo solo un meno, se questo dovesse accadere il metodo parseDouble incorrerà in un errore, che a dire il vero non è un vero e proprio errore, ma un eccezione, che comunque se non intercettata porterà comunque ad un crash del programma, nel prossimo paragrafo vedremo come ovviare a tale problema e gestire le eccezioni. 102 3.8 Gestire le eccezioni Spesso abbiamo accennato al fatto che determinate operazioni potessero generare errori e quindi mandare in crash il programma, c’è però da fare una piccola distinzione tra i vari tipi di errori, vi sono infatti gli errori gravi e le eccezioni. Le eccezioni sono un tipo di errore meno grave, e possono essere gestite, esse sono contenute nel pacchetto java.lang ed estendono la classe java.lang.Excepition che a sua volta estende la classe java.lang.Throwable gli errori invece estendono altre classi che a loro volta estendono java.lang.Error. Un eccezione viene lanciata quando viene eseguita un operazione non valida, ad esempio si cerca di inserire un valore in una posizione inesistente di un array oppure si cerca di convertire in valore numerico una stringa non valida, ad esempio: int e = Integer.parseInt("18x2"); In questo caso 18x2 non è una stringa valida quindi il metodo parseInt lancia un eccezione ovvero NumberFormatException, questa eccezione viene passata al metodo chiamante e se non viene intercettata passa al chiamante del chiamante e così via, se non viene mai intercettata essa porta ad un crash dell’applicazione, se invece viene intercettata viene eseguito un blocco di codice da noi definito e l’applicazione continua normalmente, per catturare un eccezione dobbiamo utilizzare il blocco try catch, vediamo ora un esempio: int x = 0, y = 0, z = 0; String s; try{ x = 10; y = Integer.parseInt("18x2"); // Eccezione, l’operazione non viene eseguita z = 20; // Il blocco termina con l’eccezione }catch (NumberFormatException Eccezione0){ s = Ecezzione0.toString(); // s = "java.lang.NumberFormatException",x = 10,y = 0,z = 0 } In questo caso il metodo parseInt lancerà una NumberFormatException l’operazione non viene eseguita così come il resto del blocco try ma vengono comunque eseguite le operazioni precedenti all’eccezione, il blocco catch invece si comporta come un metodo con parametro una variabile di tipo Trowable in questo caso la nostra eccezione legata alla variabile locale Eccezione0, il contenuto del blocco catch viene eseguito solo se viene lanciata l’eccezione inserita come parametro, è possibile inserire più catch dopo un blocco try per gestire diverse eccezioni, oppure gestire più eccezioni in uno stesso catch, ma solo se il progetto è compilato con almeno java 1.7 tramite l’utilizzo di | questo però renderà la variabile locale di tipo final, vediamo un esempio: 103 int[] i = new int[2]; String s; try{ i[0] = 1; i[1] = 2; i[2] = Integer.parseInt("18x2"); // Eccezione, l’operazione non viene eseguita }catch (NumberFormatException | ArrayIndexOutOfBoundsException Err0){ s = Err0.toString(); // s = "java.lang.NumberFormatException" } In questo caso stiamo cercando di gestire due eccezioni, ovvero NumberFormatException che viene lanciata quando si tenta di convertire una stringa non valida in numerica e ArrayIndexOutOfBoundsException che invece viene lanciata quando si tenta di inserire un valore in una posizione non valida di un array, in questo caso prima di assegnare il valore alla variabile, viene eseguito il metodo parseInt che lancia l’eccezione NumberFormatException e termina il blocco try, quindi non viene inserito nulla in i[2], la variabile s quindi conterrà "java.lang.NumberFormatException:" se invece avessimo scritto invece: i[2] = Integer.parseInt("182"); parseInt non avrebbe lanciato alcuna eccezione, ma essendo i definito con array con due posizioni, solo [0] e [1] sono posizioni valide, quindi l’inserire un valore nella posizione [2] lancerà un eccezione di tipo ArrayIndexOutOfBoundsException che viene comunque catturata dal blocco catch e quindi s conterrà invece il seguente valore: "java.lang.ArrayIndexOutOfBoundsException:" Un blocco try quindi non può generare due eccezioni contemporaneamente, ma può doversi ritrovare a gestire diversi tipi di eccezioni, è anche possibile catturare tutte le eccezioni con un solo catch catturando Exception oppure Throwable, in questo caso però verranno intercettati anche gli errori gravi, che di norma non andrebbero intercettati, vediamo ora un esempio: int[] i = new int[2]; String s; try{ i[0] = 1; i[1] = 2; i[2] = Integer.parseInt("18x2"); // Eccezione, l’operazione non viene eseguita }catch (Exception Err0){ s = Err0.toString(); // s = "java.lang.NumberFormatException" } In questo caso verranno catturate tutte le eccezioni, ma non gli errori, dopo un catch Exception, non ci possono essere altri catch che catturino eccezioni in quanto gestite da catch Exception, ma può essere inserito un catch Exception dopo altri catch in modo da gestire le eccezioni non gestite in precedenza, lo stesso discorso vale per 104 Throwable, detto questo quindi potremmo modificare l’applicazione calcolatrice nel seguente modo: package com.test.beta; import import import import import android.support.v7.app.ActionBarActivity; android.os.Bundle; android.widget.Button; android.widget.EditText; android.widget.TextView; public class Calcolatrice extends ActionBarActivity { private private private private private private private final final final final final final final Button meno = (Button) findViewById(R.id.meno); Button più = (Button) findViewById(R.id.piu); Button per = (Button) findViewById(R.id.per); Button div = (Button) findViewById(R.id.div); EditText et1 = (EditText) findViewById(R.id.editText1); EditText et2 = (EditText) findViewById(R.id.editText2); TextView ris = (TextView) findViewById(R.id.textView1); // Semplifichiamo i flag private static final int private static final int private static final int dell’imput type, anche se non necessario t_num = InputType.TYPE_CLASS_NUMBER; f_dec = InputType.TYPE_NUMBER_FLAG_DECIMAL; f_sig = InputType.TYPE_NUMBER_FLAG_SIGNED; private double a = 0, b = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_calcolatrice); // Rendiamo le EditText numeriche con flag decimale e signed // Proprietà che possiamo anche definere da Layout xml et1.setInputType(t_num | f_dec | f_sig); et2.setInputType(t_num | f_dec | f_sig); ris.setText("Risultato: 0.0"); meno.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (assegna()) // il metodo viene comunque eseguito ris.setText("Risultato: " + (a – b)); } }); più.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (assegna()) ris.setText("Risultato: " + (a + b)); } }); div.setOnClickListener(new View.OnClickListener() { @Override 105 public void onClick(View v) { if (assegna()) ris.setText("Risultato: " + (a / b)); } }); per.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (assegna()) ris.setText("Risultato: " + (a * b)); } }); } private boolean assegna(){ // Utilizziamo Try / Catch try{ a = Double.parseDouble(et1.getText().toString()); b = Double.parseDouble(et2.getText().toString()); return true; } catch (Exception ex0){ ris.setText("Errore: " + ex0); return false; } } } Abbiamo cambiato il metodo assegna() e lo abbiamo reso boolean, abbiamo poi inserito un return true alla fine del try ed un return false alla fine del catch, in questo modo il metodo restituirà true se tutto è andato a buon fine e false se cattura un eccezione, inoltre nel caso dovesse catturare un eccezione inserirà nella TextView il tipo di eccezione. Nei vari Button invece abbiamo sostituito assegna(); con if (assegna()) in questo modo il metodo assegna viene comunque eseguito e se restituisce true eseguirà l’if, altrimenti no, ricordandosi che un if senza parentesi graffe considera all’interno dell’if solo l’istruzione successiva ad esso. In questo esempio abbiamo utilizzato come parametro di catch Exception, questo di permette di catturare qualsiasi eccezione, ma non gli errori, se vogliamo essere più specifici possiamo anche definire l’eccezione specifica da catturare in questo caso avremmo potuto utilizzare anche catch (NumberFormatException ex0) avremmo potuto utilizzare anche catch (Trowable arg0) ma in questo caso intercetteremmo anche gli errori gravi, che non andrebbero intercettati, volendo è anche possibile catturare solamente gli errori, anche se sconsigliato, tramite catch (Error arg0). Non solo possiamo catturare le eccezioni, ma possiamo anche lanciarle noi stessi a nostra volta tramite il comando throw, esso infatti ci permette di lanciare una 106 qualsiasi sottoclasse di Throwable, il che significa che possiamo sia utilizzare eccezioni già presenti nelle librerie Java, sia eccezioni create da noi, inoltre esistono due tipi di eccezioni, le eccezioni Checked, ovvero quelle eccezioni che estendono direttamente la classe Exception, esse se previste devono per forza di cosa essere gestite dal chiamante tramite try catch, richiamare quindi un metodo che potrebbe lanciare un eccezione Checked al difuori del relativo blocco try catch porterà ad un errore del compilatore, d’altro canto le eccezioni Uncecked invece non devono essere per forza di cose gestite dal chiamante, ma se non vengono gestite, comunque porteranno ad un crash dell’applicazione nel momento in cui dovessero venir lanciate, ma è comunque possibile scrivere il codice senza utilizzare il blocco try catch e non incorrere in un errore del compilatore, le eccezioni Unchecked estendono la classe RuntimeException che è comunque una sottoclasse di Exception, un esempio di eccezioni Uncheked, sono le due eccezioni citate negli esempi, ovvero NumberFormatException e ArrayIndexOutOfBounds, infatti possiamo utilizzare il metodo parseInt anche senza utilizzare try catch, anche se questo ci potrebbe esporre a problemi, comunque se dovessimo lanciare un’eccezione Checked in un metodo, dovremmo anche definire il fatto che il metodo lanci quell’eccezione tramite throws, mentre non è necessario per le eccezioni uncheked, c’è però da fare un distinzione tra throw e throws, throw viene definito all’interno del metodo e lancia l’eccezione al chiamante, throws invece viene definito alla definizione del metodo ed indica semplicemente che il metodo può lanciare una determinata eccezione, questo ci servirà in fase di programmazione in quanto sappiamo che tale metodo può lanciare una determinata eccezione e che quindi dovremmo gestirla, throws comunque è obbligatorio solo per le eccezioni Checked, in quanto devono essere previste per forza di cose, vediamo ora un esempio. int[] i = new int[5]; … public void MetodoX(int arg0, int arg1) throws ArrayIndexOutOfBoundsException { if (arg0 < 0 || arg0 > 4) throw new ArrayIndexOutOfBoundsException("Posizione non valida"); i[arg0] = arg1; } In questo esempio throws ArrayIndexOutOfBoundsException non è obbligatorio in quanto si tratta di un eccezione Uncheked, ma è comunque utile in fase di programmazione in quando andando a chiamare il metodo sapremmo che esso può lanciare un ArrayIndexOutOfBoundsException e che quindi sarebbe opportuno gestirla, inoltre anche se scorretto, è anche possibile specificare che un metodo possa lanciare un eccezione che non potrà mai lanciare, inoltre è anche possibile specificare più 107 eccezzioni in un metodo, ora tornando al nostro esempio, abbiamo creato un metodo con due parametri int, che assegna il valore di arg1 alla posizione arg0 dell’array i[], ora abbiamo specificato che se arg0 dovesse essere minore di 0 o maggiore di 4 debba venir lanciata l’eccezione ArrayIndexOutOfBoundsException, eccezione che comunqe sarebbe satata lanciata ugualmente dalla riga di codice successiva se arg0 < 0 || arg0 > 4 in quanto l’array i[] è definito con 5 posizioni, la differenza però sta nel fatto che abbiamo inserito un parametro nel costruttore dell’eccezione ovvero un messaggio che sarà passato al chiamante, quindi ora immaginiamo di andare a richiamare tale metodo in questo modo: String ex, msg; try{ MetodoX(6, 25); } catch(ArrayIndexOutOfBoundsException e){ ex = e.toString(); // java.lang.ArrayIndexOutOfBoundsException: Posizione non valida msg = e.getMessage(); // Posizione non valida } In questo caso il chiamante intercetterà l’eccezione da noi lanciata, quindi verrà passato al chiamante anche il messaggio da noi inserito, che possiamo recuperare con e.getMessage(); inoltre facendo e.toString(); ci verrà restituito il percorso completo dell’eccezione ed il messaggio, se invece l’eccezione fosse stata lanciata automaticamente ad i[arg0] = arg1; invece e.toString(); ci avrebbe restituito: "java.lang.ArrayIndexOutOfBoundsException:" senza messaggio inoltre e.getMessage(); ci avrebbe restituito: "null" Lanciare quindi un eccezione manualmente ci permette anche di passare un messaggio al metodo chiamante, naturalmente possiamo anche lanciare un eccezione utilizzando il costruttore di default e quindi senza messaggio, alcune eccezioni possono avere anche altri costruttori, ad esempio ArrayIndexOutOfBoundsException può avere come parametro anche un valore int, che genererà un messaggio predefinito, dove tale parametro indicherebbe la posizione non valida dell’array, per cui inserendo ad esempio come parametro 5, verrà generato il seguente messaggio: "Array index out of range: 5", altre eccezioni possono avere altri costruttori, detto questo, come già accennato in precedenza, possiamo anche creare le nostre eccezioni personalizzate, per farlo ci basterà creare una classe che estenda Exception nel caso di un eccezione Checked oppure RuntimeException nel caso di un eccezione Unchecked, proviamo ora a realizzare un eccezione Checked, un metodo che la lanci ed un metodo chiamante che invece dovrà gestirla. 108 Classe ProgressBarOutOfRangeException: package com.test.beta; public class ProgressBarOutOfRangeException extends Exception{ private String msg = ""; private boolean overflow = false; public ProgressBarOutOfRangeException(){ overflow = false; } public ProgressBarOutOfRangeException(boolean arg0){ overflow = arg0; } public ProgressBarOutOfRangeException(String msg){ super(msg); this.msg = msg; overflow = false; } public ProgressBarOutOfRangeException(boolean arg0, String msg){ super(msg); this.msg = msg; overflow = arg0; } public boolean isOverflow(){ return overflow; } @Override public String toString(){ return "ProgressBarOutOfRangeException:" + msg + " Overflow:" + overflow; } } Classe Activity1: package com.test.beta; import import import import import android.support.v7.app.ActionBarActivity; android.os.Bundle; android.widget.Button; android.widget.ProgressBar; android.widget.TextView; public class Activity1 extends ActionBarActivity { private private private private private private final Button più = (Button) findViewById(R.id.button1); final Button meno = (Button) findViewById(R.id.button2); final ProgressBar pb = (ProgressBar) findViewById(R.id.progressBar1); final TextView t = (TextView) findViewById(R.id.textView1); int i = 50; String ex = ""; 109 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_calcolatrice); pb.setMax(100); pb.setProgress(i); t.setText(i+"%"); meno.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { i--; aggiorna(); } }); più.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { i++; aggiorna(); } }); private void controlla() throws ProgressBarOutOfRangeException{ if (i > pb.getMax()) throw new ProgressBarOutOfRangeException(true, i + " Non valido"); else if (i < 0) throw new ProgressBarOutOfRangeException(false, i + " Non valido"); } private void aggiorna(){ try { controlla(); } catch (ProgressBarOutOfRangeException e){ if (e.isOverflow()) i = 100; else i = 0; ex = e.toString(); } finally { pb.setProgress(i); t.setText(i+"%") } } } In questo esempio abbiamo creato un eccezione personalizzata di tipo Checked, in quanto estendiamo direttamente la classe Exception, questa eccezione ha quattro costruttori diversi che ci permettono sia di inserire un messaggio, sia di modificare una variabile boolean all’interno della classe, abbiamo poi fatto un override del metodo toString(); che ci permette di personalizzare la conversione in stringa 110 dell’eccezione, per cui ad esempio prendiamo in considerazione il throw che eseguiamo nell’esempio, ovvero: throw new ProgressBarOutOfRangeException(true, i + " Non valido"); In questo caso senza aver eseguito l’override del metodo toString(); facendo: ex = e.toString(); ex avrebbe contenuto: com.test.beta.ProgressBarOutOfRangeException:101 eseguendo invece l’override, ex invece conterrebbe: Non valido ProgressBarOutOfRangeException:101 Non valido Overflow:true Trattandosi di un eccezione Checked, il metodo controlla() deve obbligatoriamente avere anche throws ProgressBarOutOfRangeException, o si incapperà in un errore del compilatore, inoltre il metodo chiamante aggiorna() deve chiamare controlla() all’interno di un try, con un catch che preveda l’eccezione, in questo caso siamo andati anche ad utilizzare in metodo isOverflow() della nostra eccezione per stabilire se abbiamo superato il range di valori in positivo oppure in negativo, essendo questo un metodo specifico della nostra eccezione, non possiamo richiamarlo se la catturiamo in modo generico tramite catch (Exception e) per cui dobbiamo essere precisi e catturare l’esatta eccezione, abbiamo inoltre anche aggiunto il blocco finally, esso determina delle operazioni che devono essere eseguite indipendentemente dal fatto che sia stata catturata o meno un eccezione, ma non solo, il contenuto di finally verrà eseguito anche se l’eccezione non viene catturata nemmeno dal chiamante e l’applicazione viene terminata, questo ci può tornare utile ad esempio se lavoriamo con file esterni all’applicazione, finally ci permetterà di interagire con essi prima dell’arresto dell’applicazione in caso di errore o eccezione non catturata, comunque sia se l’eccezione viene correttamente catturata, viene eseguito anche il resto del metodo al difuori del blocco try catch, ma non il resto del blocco try, in questo specifico caso il blocco finally non era necessario in quanto potevamo anche inserire il suo contenuto subito dopo il blocco try catch, le eccezioni personalizzate possono quindi essere utilizzate quando le eccezioni predefinite non possono essere usate per gestire in modo adeguato un determinato problema, oppure se si ha la necessità di differenziare varianti dello stesso problema, ad esempio come nel nostro esempio abbiamo specificato un comportamento differente a seconda del fatto che i fosse superiore al valore massimo o minore del valore minimo, possiamo infatti inserire nelle nostre eccezioni dei getters come ad esempio il metodo isOverflow(); che ci permettono di gestire in maniera differente variazioni della stessa eccezione, nella maggior parte dei casi però sono sufficienti le eccezioni predefinite e non è quindi necessario crearne di personalizzate. 111 3.9 I tipi grezzi Abbiamo già accennato ai tipi grezzi parlando degli ArrayList, un tipo grezzo è un tipo di oggetto che deve essere definito nel momento in cui istanziamo la classe, tale classe è detta classe grezza, una classe grezza può contenere più tipi grezzi, ma devono essere tutti definiti nel momento in cui viene istanziata la classe, vediamo ora come realizzare una classe grezza: package com.test.beta; public class Grezza<X,Y> { private X a; private Y b; public Grezza(X arg0, Y arg1){ a = arg0; b = arg1; } public X getA() { return a; } public Y getB() { return b; } } In questo caso abbiamo una classe grezza con due tipi grezzi, ovvero i tipi X e Y, abbiamo due variabili di tipo grezzo ed un costruttore con due parametri di tipo grezzo, infine abbiamo i getters delle due variabili. Abbiamo già nominato il termine getter, ma non lo abbiamo mai definito, i getters ed i setters sono dei particolari metodi che hanno lo scopo di recuperare o impostare il valore di una variabile di una determinata classe, i getters per convenzione hanno come nome getVariabile, nessun parametro e come ritorno la variabile da recuperare, se si tratta di una variabile boolean, il metodo si chiamerà invece isVariabile dove variabile è il nome della variabile, i setters invece hanno lo scopo di impostare il valore della variabile, hanno come nome setVariabile, come parametro il valore da assegnare alla variabile e come tipo di ritorno void, inoltre assegna il valore del parametro alla variabile interessata, ecco alcuni esempi di getters e setter: public int getI() { // Getter return i;} public boolean isB() { // Getter boolean return b;} public void setI(int i){ // Setter this.i = i; } 112 Chiudiamo questa parentesi riguardante i getters ed i setters e torniamo a parlare dei tipi grezzi, l’utilizzo dei tipi grezzi può portare ad alcuni errori che il compilatore non può controllare, ad esempio: public X converti(Y arg0){ return (X) arg0; } Potrebbe portare ad un ClassCastException in quanto il compilatore non può sapere di che tipi saranno X e Y e quindi non può sapere in fase di compilazione se sarà un cast valido, vediamo ora invece come istanziare la precedente classe grezza: package com.test.beta; public class Classe1 { public Grezza<Integer, String> raw = new Grezza<Integer, String>(15, "Grezzo"); public int i = raw.getA(); // i = 15 public String s = raw.getB(); // s = "Grezzo" public Classe1() { } } In questo esempio abbiamo memorizzato nella variabile raw due diversi valori del tipo definito nel momento in cui abbiamo istanziato la classe Grezza, in questo caso Integer e String, ma potevamo utilizzare qualsiasi altro tipo non primitivo, così facendo anche i parametri del costruttore si sono adattati ai nuovi tipi, in quanto erano parametri grezzi, così come il tipo delle due variabili all’interno della classe ed il tipo di ritorno dei getters, le classi grezze quindi sono molto malleabili, e diverse istanze possono contenere diversi tipi, inoltre a causa della loro natura mutevole, variabili e metodi grezzi non possono mai essere definiti static, in quanto ogni istanza può definire un tipo diverso per tali metodi e variabili, bisogna però tenere a mente che non è possibile istanziare tipi grezzi all’interno di una classe grezza, quindi non è possibile creare Array grezzi, è possibile però creare Array Object che possono contenere qualsiasi tipo di dato, tali dati poi possono essere presi singolarmente e convertiti tramite cast nei relativi tipi, attenzione però, non è possibile fare il cast di un array, ma solo dei suoi singoli dati, ad esempio: Object[] obj = new Object[2]; obj[0] = 1; obj[1] = 2; int i = (Integer) obj[0]; // Ok, è possibile eseguire il cast di un elemento int[] arr = (Integer[]) obj; // Errore, non è possibile eseguire il cast di un array È però possibile copiare il contenuto di un array all’interno di un altro array, tramite il metodo System.arrayCopy nel seguente modo: 113 Object[] obj = new Object[2]; obj[0] = 1; obj[1] = 2; Integer[] arr = new Integer[obj.length]; // Creiamo un array della stessa lunghezza System.arrayCopy(obj,0,arr,0,arr.length); // Copiamo l’array obj in arr Il metodo arrayCopy, ha i seguenti parametri: Object src – L’array da copiare. int srcPos – La posizione da cui iniziare a copiare, 0 per iniziare dall’inizio. Object dst – L’array in cui copiare src. int dstPos – La posizione in cui iniziare ad inserire la copia dell’array, 0 per l’inizio. int lenght (src.lenght – Il numero di posizioni da copiare da src, per copiare tutto l’array - srcPos). Tale metodo però funziona solamente con gli array monodimensionali, è però possibile copiare parti monodimensionali di array multidimensionali, è quindi possibile in questo modo anche copiare array multidimensionali, ad esempio: Object[][] obj = {{1,2,3},{4,5,6,7}}; Integer[][] arr = new Integer[obj[0].length][obj[1].length]; System.arrayCopy(obj[0], 0, arr[0], 0, arr[0].length); System.arrayCopy(obj[1], 0, arr[1], 0, arr[1].length); In questo modo abbiamo una copia convertita dell’array multidimensionale obj, attenzione però, poiché se è vero che una variabile primitiva int accetta come valore il valore di una variabile non primitiva Integer, un array int non accetta un array Integer, in quanto gli array non possono essere direttamente convertiti, inoltre possiamo copiare un array Object, solo in un array di un tipo che estenda Object, quindi qualunque tipo non primitivo, purché sia compatibile con i dati contenuti nell’array Object, per questo motivo abbiamo dovuto creare un array Integer e non int, cercare di copiare un array Object in un array di tipo primitivo causerà il lancio di una ArrayStoreException, la stessa cosa accadrà se l’array da copiare, contiene dati non compatibili con l’array di destinazione, detto questo quindi vediamo ora come inserire un array generico in una classe grezza: package com.test.beta; public class ArrayGrezzo<X> { private Object[] obj; public Grezza(int arg0){ obj = new Object[arg0]; } 114 public void setValue(X value, int index) { obj[index] = value; // Object accetta qualsiasi tipo di valore } public X getValue(int index){ return (X) obj[index]; // Eseguiamo il cast del valore } } Così facendo abbiamo creato una classe grezza con all’interno un array che andrà a contenere dati di tipo indefinito, e poiché non è possibile istanziare tipi grezzi e quindi creare array grezzi in quanto: T[] arrayGrezzo = new T[Lunghezza]; genererà un errore di compilazione, per cui abbiamo dovuto utilizzare un array di tipo Object, array che può contenere qualunque tipo di dato, tramite il costruttore della classe abbiamo inizializzato l’array, tramite il metodo setValue invece possiamo inserire un valore del tipo definito in una determinata posizione dell’array, metre con il metodo getValue, possiamo recuperare un valore contenuto in una determinata posizione dell’array, tale valore verrà riconvertito nel tipo da noi definito. Prima abbiamo detto che non è possibile creare array grezzi, questo però non è del tutto vero, poiché il problema non è che non possiamo creare array grezzi, ma che non possiamo istanziarli con new T[Lunghezza], ma possiamo comunque crearli ed inizializzarli tramite i parametri del costruttore o dei metodi, ad esempio: package com.test.beta; public class ArrayGrezzo<X> { private X[] arr; public Grezza(X[] arg0){ arr = arg0; } public void setValue(X value, int index) { arr[index] = value; } public X getValue(int index){ return arr[index]; } public X[] getArray(){ return arr; } } In questo modo creiamo un array grezzo senza inizializzarlo, poi tramite il costruttore gli passiamo i valori di un array inizializzandolo, il resto del codice è simile all’esempio precedente. 115 3.10 Le classi enum Le classi enum sono un particolare tipo di classe, e servono a contenere dei valori costanti, esse non sono definite dalla parola chiave class, ma da enum, le classi enum possono contenere costanti semplici o costanti con valori, inoltre si tratta comunque di classi, quindi possono anche contenere metodi, vediamo ora come creare una semplice classe enum: package com.test.beta: public enum Medaglie{ ORO, ARGENTO, BRONZO, NULLA } Vediamo ora come utilizzare tale classe Classe Podio: package com.test.beta: public class Podio{ private final Medaglie medaglia; private final String nome; public Podio (String nome, Medaglie medaglia){ this.medaglia = medaglia; this.nome = nome; } public String Risultato(){ switch(medaglia){ case ORO: return "Complimenti case ARGENTO: return "Complimenti case BRONZO: return "Complimenti default: return "Mi dispiace } } " + nome + " sei arrivato primo!"; " + nome + " sei arrivato secondo!"; " + nome + " sei arrivato terzo!"; " + nome + " ma non sei sul podio, ritenta"; } Classe Gara: package com.test.beta: public class Gara{ private static final Podio mario = new Podio("Mario", Medaglie.ORO); private static final Podio pietro = new Podio("Pietro", Medaglie.ARGENTO); private static final Podio filippo = new Podio("Filippo", Medaglie.BRONZO); private static final Podio andrea = new Podio("Andrea", Medaglie.NULLA); public static String[] risultati = new String[4]; 116 public Gara(){ risultati[0] risultati[1] risultati[2] risultati[3] = = = = mario.Risultato(); pietro.Risultato(); filippo.Risultato(); andrea.Risultato(); } } Abbiamo mostrato un semplice esempio di enum, esso contiene quattro valori, considerati come costanti public static final, tali costanti non hanno ne tipo ne valore, ma possono essere utilizzate per creare oggetti del tipo della classe, in questo caso oggetti di tipo Medaglie, tali oggetti poi possono essere utilizzati come variabile di uno switch statement utilizzando come case i nomi delle costanti, attenzione però in quanto non possiamo utilizzare tale sintassi negli if statements quindi: Medaglie medaglia = Medaglie.ORO; if (medaglia == ORO) // Errore --if (medaglia == Medaglie.ORO) // Ok --- Abbiamo poi creato una classe Podio, con un costruttore che ha come parametri, una String, ed un oggetto di tipo Medaglie, ed il metodo String Risultato(), che restituisce una stringa di testo diversa a seconda del valore della variabile medaglia, valore che viene passato come parametro dalla classe chiamante, infine abbiamo creato una classe chiamante di nome Gara, che istanzia la classe Podio quattro volte, ed il relativo metodo Risultato() che restituirà quattro stringhe differenti, quindi dopo aver eseguito la classe Gara, l’array risultati avrà i seguenti valori: [0]: [1]: [2]: [3]: "Complimenti "Complimenti "Complimenti "Mi dispiace Mario sei arrivato primo!" Pietro sei arrivato secondo!" Filippo sei arrivato terzo!" Andrea ma non sei sul podio, ritenta" Gli enum però possono essere anche più complessi, le variabili infatti possono contenere anche dei valori, proviamo ora a vedere un enum più complesso: package com.test.beta: public enum Medaglie{ ORO("Mario", 52.25), ARGENTO("Pietro", 53.12), BRONZO("Filippo", 53.98), NULLA("Andrea", 55.04); private final String nome; private final double tempo; 117 public Medaglie(String nome, double tempo){ this.nome = nome; this.tempo = tempo; } public String getNome(){ return nome; } public double getTempo(){ return tempo; } } In questo caso abbiamo un enum più complesso, abbiamo oltre alle costanti, anche dei valori, abbiamo inoltre due costanti final non inizializzate un costruttore e due getters, in questo caso il costruttore è necessrio, in quanto abbiamo dei parametri nelle costanti, le costanti enum infatti rappresentano delle istanze della stessa classe, e quindi possono avere anche dei parametri, per cui ad esempio è come se avessimo: public public public public static static static static final final final final Medaglie Medaglie Medaglie Medaglie ORO = new Medaglie("Mario", 52.25); ARGENTO = new Medaglie("Pietro", 53.12); BRONZO = new Medaglie("Filippo", 53.98); NULLA = new Medaglie("Andrea", 55.04); Motivo per cui è necessario un costruttore relativo ai parametri delle costanti, e non solo, possiamo anche avere costanti con diverso numero e tipo di parametri, purché siano previsti i relativi costruttori, essendo le costanti enum di tipo static, esse ci permettono di richiamare i metodi non statici della classe in modo statico, questo perché è già la costante a creare l’istanza, quindi possiamo richiamare i getters non statici in modo “statico” in questo modo: String nome = Medaglie.ORO.getNome(); // nome = "Mario" double tempo = Medaglie.ARGENTO.getTempo(); // tempo = 53.12 Due istanze diverse danno risultati diversi, ma comunque richiamiamo il metodo in modo statico, in quanto la classe viene istanziata automaticamente dalla costante dell’enum, quindi il metodo viene comunque chiamato in modo non statico, anche se all’atto di scrivere il codice noi non istanziamo nulla manualmente, la classe viene comunque istanziata tramite il costruttore relativo ai parametri della costante enum, oltre ai getters, possiamo anche inserire altri metodi in una classe enum, come se si trattasse di una normale classe, attenzione però, le classi enum non possono essere istanziate se non tramite le loro costanti, per cui anche se: ORO("Mario", 52.25) Funziona in modo molto simile a: public static final Medaglie ORO = new Medaglie("Mario", 52.25); 118 Tale codice non può essere utilizzato in quanto le classi enum non possono essere istanziate in modo normale, per cui dobbiamo utilizzare le costanti enum, le due costanti infatti funzionano in modo simile, ma non sono la stessa cosa, l’esempio serve solo a far comprendere il funzionamento delle costanti enum e non deve essere frainteso. Vediamo ora una classe enum più complessa: package com.test.beta: public enum Gare{ MARIO("Mario", 52.25, 54.23), PIETRO("Pietro", 53.12, 50.15, 52.36), FILIPPO("Filippo", 53.98, 55,78), ANDREA("Andrea", 55.04); private final String nome; private final double[] tempi; public gare(String nome, double t0){ this.nome = nome; tempi = new double[1]; tempi[0] = t0; } public gare(String nome, double t0, double t1){ this.nome = nome; tempi = new double[2]; tempi[0] = t0; tempi[1] = t1; } public gare(String nome, double t0, double t1, double t2){ this.nome = nome; tempi = new double[3]; tempi[0] = t0; tempi[1] = t1; tempi[2] = t2; } public String risultati(){ String tmp = nome + " ha ottenuto i seguenti tempi: "; for (int i = 0; i < tempi.length(); i++){ tmp += tempi[i] + ", "; } return tmp.substring(0, tmp.length() - 2) + "!"; } public double[] getTempi(){ return tempi; } public double media(){ double tmp = 0; for (int i = 0; i < tempi.length(); i++){ tmp += tempi[i]; } return tmp / tempi.length(); } } 119 Vediamo ora la classe chiamante: package com.test.beta: public class Risultati{ public static final String[] risultato = new String[4]; public static final double[] media = new double[4]; public static final double[][] tempi = new double[4][]; public risultati(){ risultato[0] risultato[1] risultato[2] risultato[3] = = = = Gare.MARIO.risultati(); Gare.PIETRO.risultati(); Gare.FILIPPO.risultati(); Gare.ANDREA.risultati(); media[0] media[1] media[2] media[3] = = = = Gare.MARIO.media(); Gare.PIETRO.media(); Gare.FILIPPO.media(); Gare.ANDREA.media(); tempi[0] tempi[1] tempi[2] tempi[3] = = = = Gare.MARIO.getTempi(); Gare.PIETRO.getTempi(); Gare.FILIPPO.getTempi(); Gare.ANDREA.getTempi(); } } Valori delle variabili: risultato: [0] - "Mario ha ottenuto i seguenti tempi: 52.25, 54.23!" [1] - "Pietro ha ottenuto i seguenti tempi: 53.12, 50.15, 52.36!" [2] - "Filippo ha ottenuto i seguenti tempi: 53.98, 55,78!" [2] - "Andrea ha ottenuto i seguenti tempi: 55.04!" media: [0] - 53.24 [1] - 51,87666666666667 [3] - 54.88 [4] - 55.04 tempi: [0] - {52.25, 54.23} [1] - {53.12, 50.15, 52.36} [2] - {53.98, 55,78} [3] - {55.04} In questo enum abbiamo tre costruttori diversi, in quanto vi sono costanti con parametri diversi, e deve essere specificato un costruttore per ogni variazione, inoltre abbiamo un getter e due altri metodi, nei metodi risultati e media abbiamo inserito un ciclo for in quanto la lunghezza di tempi è variabile, da notare il fatto che abbiamo definito tempi come final, ma lo abbiamo inizializzato tre volte, ciò si può fare, in quanto l’abbiamo inizializzata in tre costruttori diversi, inoltre una costante globale 120 final se non inizializzata alla creazione, deve essere obbligatoriamente inizializzata in ogni costruttore, una costante static final invece deve essere sempre inizializzata alla creazione, inoltre nella classe chiamante abbiamo creato degli array static final, e gli abbiamo assegnato più volte dei valori, non si tratta di un errore in quanto è l’istanza dell’array ad essere definita in una costante, ma non i suoi valori, infatti gli array vanno inizializzati con new, quindi possiamo sempre assegnare valori ai campi di un array final, ma non riassegnarlo, abbiamo anche utilizzato il metodo substring, esso ci restituisce una parte della stringa, in questo caso l’intera stringa meno gli ultimi due caratteri, ciò ci permette di tagliare un “, “ superfluo. Le classi enum ci possono tornare utili quando vogliamo assegnare dei valori costanti ad alcuni oggetti, le classi enum quindi ci permettono di interagire con tali oggetti in maniera molto semplice, inoltre possono contenere anche metodi che possono essere richiamati tramite l’oggetto in modo statico. 3.11 I Thread Spesso parlando delle caratteristiche dei vari dispositivi Android e non, si parla di processori single core, dual core o quad core, questa caratteristica però come molti erroneamente pensano, non indica il numero dei processori di un dispositivo, ma il numero di nuclei del processore di tale dispositivo, quindi un dispositivo con processore quad core, non ha quattro processori, ma un solo processore con quattro nuclei, ora molti si chiederanno qual è l’utilità di questi nuclei, per rispondere a questa domanda, dobbiamo prima introdurre il concetto di multi tasking, i moderni sistemi infatti possono eseguire più applicazioni contemporaneamente, a differenza dei vecchi sistemi a riga di comando quali MS-DOS che invece potevano eseguire una sola operazione alla volta, ciò è possibile tramite l’utilizzo di una tecnica detta time sharing, il processore quindi non esegue realmente tutte le operazioni contemporaneamente, ma esegue piccole porzioni di tali applicazioni consecutivamente, ciò ci da l’impressione che il processore stia eseguendo contemporaneamente più compiti, ma in realtà li esegue un po’ alla volta consecutivamente, per fare un esempio immaginiamo di dover riempire quattro ciotole con cento palline utilizzando un solo braccio, potremmo riempire le ciotole consecutivamente, oppure inserire una sola pallina e passare alla ciotola successiva, per poi ricominciare dalla prima ciotola una volta inserita la pallina nell’ultima, così riempiremo le ciotole in modo omogeneo, un processore funziona più o meno in questo modo, la situazione oggi è leggermente differente in quanto i processori ora hanno più nuclei, quindi possono eseguire realmente più operazioni contemporaneamente tanti più nuclei esso ha, inoltre le applicazioni spesso hanno più thread, ovvero sono divise in parti, che possono essere eseguite da diversi nuclei, 121 quindi un applicazione con quattro thread, può sfruttare appieno le pontenzialità di un processore con quattro nuclei, dato che ogni nucleo detto core potrà eseguire un diverso thread, i thread però hanno anche un'altra utilità, in quanto un singolo thread non può eseguire più operazioni contemporaneamente, avere due thread separati ci permetterà di eseguire due operazioni contemporaneamente, immaginiamo infatti di dover eseguire delle operazioni nel mentre viene svolto un ciclo che può essere interrotto dall’utente, se eseguissimo il ciclo nello stesso thread dell’applicazione, l’applicazione inizierebbe ad eseguire il ciclo e l’utente non potrebbe più interagire con l’applicazione fintanto che il ciclo è in esecuzione, quindi non può ne interromperlo ne l’applicazione può eseguire altre operazioni, ad esempio: ... boolean ciclo = true; Button più, meno, stop; ProgressBar pb = (ProgressBar) findViewById(R.id.progressBar1); int progress = 50; Più = (Button) findViewById(R.id.piu); meno = (Button) findViewById(R.id.meno); stop = (Button) findViewById(R.id.stop); più.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { progress++; } }); meno.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { progress--; } }); stop.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ciclo = false; } }); while(ciclo){ if (progress > 100) progress = 100; else if (progress < 0) progress = 0; Pb.setProgress(progress); } … 122 Eseguendo il seguente conduce l’applicazione si bloccherà in quanto essendo tutto nello stesso thread, fintanto che l’applicazione esegue il ciclo, non verrà eseguito nient’altro nel thread, l’unico modo che abbiamo di fermare il ciclo è modificare il valore della variable ciclo in false, il problema è che essendo il ciclo nel thread principale esso blocca il thread, noi non possiamo più interagire in alcun modo e l’applicazione rimane bloccata in un ciclo infinito, per ovviare a questo problema, possiamo creare un thread, per farlo ci basterà creare una classe che estenda Thread ed esegua l’override del metodo void run(); Vediamo ora come inserire il ciclo dell’esempio precedente: Classe MainActivity: package com.test.beta; import import import import android.support.v7.app.ActionBarActivity; android.os.Bundle; android.widget.Button; android.widget.ProgressBar; public class MainActivity extends ActionBarActivity { private final Button meno = (Button) findViewById(R.id.meno); private final Button più = (Button) findViewById(R.id.piu); private final Button stop = (Button) findViewById(R.id.stop); private Thread1 thread1 = new Thread1(); public static ProgressBar pb; public static int progress = 50; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); pb = (ProgressBar) findViewById(R.id.progressBar1); thread1.start(); // Avviamo il thread più.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { progress++; } }); meno.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { progress--; } }); stop.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { thread1.termina(); } }); } 123 Classe Thread1: package com.test.beta; public class Thread1 extends Thread{ private boolean ciclo = true; public Thread1 (){ } public void termina(){ ciclo = false; } @Override public void run() { while(ciclo){ if (MainActivity.progress > 100) MainActivity.progress = 100; else if (MainActivity.progress < 0) MainActivity.progress = 0; MainActivity.pb.setProgress(MainActivity.progress); } } } Così facendo abbiamo inserito il ciclo della classe precedente in un thread separato, in quanto il contenuto del metodo run() delle classic he estendono Thread, verrà eseguito in un thread separato nel momento in cui avviamo il thread tramite il metodo start(), tale metodo può essere richiamato dalla classe chiamante oppure direttamente dalla classe del thread nel suo costruttore, nel qualcaso il thread verrà avviato nel momento in cui verrà istanziata la classe, il ciclo di vita di un thread inzia nel momento in cui viene chiamato il metodo start() e termina nel momento in cui termina il metodo run(), in questo caso il nostro thread controllerà costantemente se il valore di progress sia all’interno del range 0 – 100 e lo correggerà di conseguenza, inoltre aggiornerà continuamente il Progress della ProgressBar fintanto che ciclo rimane true, chiamando il metodo termina() renderemo ciclo false interrompendo quindi il ciclo e terminando il thread, per riavviare il thread dovremmo nuovamente istanziare il thread ed avviarlo tramite start() se non previsto già nel costruttore, abbiamo reso le variabili progress e pb statiche in modo da poter essere lette e modificate dal thread in modo statico, in realtà sarebbe possibile anche farlo con variabili non static passando al costruttore l’istanza della classe tramite this, ma la classe MainActivity non abbiamo bisogno di istanziare più volte la classe MainActivity, le variabili statiche sono più che adeguate. 124 Creando un nuovo thread, possiamo eseguire più operazioni contemporaneamente, per cui il programma non si bloccherà più all’esecuzione del ciclo, in quanto esso viene eseguito separatamente dal resto dal thread principale, bisogna però ricordarsi che viene eseguito nel nuovo thread il contenuto del metodo run() e non tutta la classe, quindi possiamo chiamare dal thread principale i metodi della classe che contiene il thread, in questo modo possiamo interrompere il ciclo del nuovo thread dal thread principale, ed è proprio ciò che avviene con il metodo termina() che va ad interrompere il ciclo del metodo run(), il metodo termina non viene eseguito nel nuovo thread, ma nel thread principale, quindi non deve attendere che il ciclo terminini. Grazie all’utilizzo del thread possiamo aggiornare lo stato della progress bar in tempo reale ogniqualvolta la variabile progress viene modificata, se non avessimo usato il thread, avremmo dovuto aggiornare manualmente lo stato della progress bar ogniqualvolta avessimo modificato il valore della variabile progress, utilizzare il thread quindi semplifica il codice, soprattutto nei casi in cui dobbiamo aggiornare lo status di alcuni oggetti al variare di variabili che vengono modificante spesso nel nostro codice, dall’altro lato però abbiamo un ciclo continuo, che consuma più risorse di un aggiornamento una tantum, in molti casi però l’utilizzo di un thread di questo tipo semplifica moltissimo il codice, per cui spesso il gioco vale la candela, e comunque sia i processori dei moderni dispositivi possono gestire agilmente applicazioni ben più complesse. Essendo possibile eseguire più thread contemporaneamente, tali thread potrebbero accedere contemporaneamente ad alcuni metodi, questo potrebbe portare a confusione, per ovviare a ciò possiamo utilizzare la parola chiave synchronized, synchronized può contenere una porzione di codice oppure più semplicemente un intero metodo, un metodo synchronized è accessibile solamente da un thread alla volta, se più thread richiamano lo stesso metodo essi verranno messi in coda, ed eseguiranno il metodo solo dopo che i thread precedenti hanno concluso con esso, vediamo ora un esempio: Classe MainActivity: package com.test.beta; import android.support.v7.app.ActionBarActivity; import android.os.Bundle; import android.widget.Button; public class MainActivity extends ActionBarActivity { public static String risultato; private final Button b1 = (Button) findViewById(R.id.button1); private final Button b2 = (Button) findViewById(R.id.button2); 125 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); b1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { risultato = "" ; new ThreadSync("A"); new ThreadSync("B"); new ThreadSync("C"); } }); b2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { risultato = ""; new ThreadUnsync("A"); new ThreadUnsync("B"); new ThreadUnsync("C"); } }); } Classe ThreadSync: package com.test.beta; public class ThreadSync extends Thread{ private String nome; public ThreadSync (String nome){ this.nome = nome; start(); } @Override public void run() { ClasseX.sync(nome); } } Classe ThreadUnsync: package com.test.beta; 126 public class ThreadUnsync extends Thread{ private String nome; public ThreadUnsync (String nome){ this.nome = nome; start(); } @Override public void run() { ClasseX.unsync(nome); } } Classe ClasseX: package com.test.beta; public class ClasseX{ public ClasseX (){ } public static synchronized void sync(String nome) { for (int i = 1; i < 4; i++) { MainActivity.risultato += nome + ":" + i + " "; try { Thread.sleep(500); } catch (InterruptedException e) { MainActivity.risultato = e.toString(); } } public static void unsync(String nome) { for (int i = 1; i < 4; i++) { MainActivity.risultato += nome + ":" + i + " "; try { Thread.sleep(500); } catch (InterruptedException e) { MainActivity.risultato = e.toString(); } } } Abbiamo creato due classi Thread, una che richiama un metodo synchronized e l’altra che richiama un normale metodo, inoltre abbiamo creato un applicazione con due Button, il primo crea tre diversi thread della classe ThreadSync mentre il secondo crea tre diversi thread della classe ThreadUnsync, la differenza sta nel fatto che il metodo sync porterà a questo risultato: risultato = "A:1 A:2 A:3 B:1 B:2 B:3 C:1 C:2 C:3"; Mentre il metodo unsync porterà a questo risultato: 127 risultato = "A:1 B:1 C:1 A:2 B:2 C:2 A:3 B:3 C:3"; Questo perché i tre thread cercano di accedere contemporaneamente al metodo, abbiamo inoltre utilizzato il metodo Thread.sleep(500); esso ci permette di sospendere il thread per x millisecondi, in questo caso 500, questo ci permette di non far accavallare i thread nei metodi non synchronized, tale problema non si presenta nei metodi synchronized in quanto i thread prima di accedere al metodo vengono sospesi finché il thread precedente non abbia terminato con il metodo. È anche possibile sincronizzare una parte di codice tramite un blocco synchronized ad esempio: synchronized(this){ // Codice } Il parametro indica cosa bloccare, in questo caso l’intero contesto, nel caso di metodi statici in cui non possiamo invocare il contesto possiamo bloccare direttamente una classe in questo modo: synchronized(ClasseDaBloccare.class){ // Codice } La classe bloccata rimarrà bloccata fintanto che un thread sta eseguendo il codice nel blocco synchronized, se il codice all’interno del blocco però non interagisce con tale contesto il blocco synchronized non verrà bloccato, synchronized quindi ci sarà utile nelle applicazioni che utilizzano i thread, per evitare che essi si accavallino, portando a risultati diversi da quelli desiderati. 3.12 Il menù Source Spesso ci troviamo a dover scrivere continuamente alcuni blocchi di codice, quali ad esempio i cicli o gli if, è però possibile automatizzare alcune di queste operazioni tramiti il menù Source di Eclipse. Per prima cosa abbiamo la parte relativa ai commenti, tramite essa possiamo aggiungere un commento es: //Commento tramite Toggle Comment (Ctrl + / ) oppure rendere la parte di codice selezionata un commento multiriga es: /* Commento */ tramite Add Block Comment (Ctrl + Shift + / ) oppure rendere un commento codice tramite Remove Block Comment (Ctrl + Shift + \ ) oppure generare un commento relativo all’elemento es: /** @author Autore */ con Generate Element Comment (Ctrl + Shift + J), i commenti servono a rendere il codice più leggibile, inoltre i commenti degli elementi ossia /** Commento */ posono essere letti in fase di progettazione anche dalle classi chiamanti, utili quindi soprattutto in caso di classi 128 senza sorgenti, quali ad esempio le varie librerie dei file Jar, la seconda parte del menù Source invece riguarda la formattazione del codice, è quindi possibile tabulare una parte di codice avanti o indietro, tramite Shift Right e Shift Left, è anche possibile tabulare automaticamente parti di codice mal tabulate tramite Correct Identation (Ctrl + I), quindi ad esempio il seguente codice: while (a < 5){ a++; b++; } Varrà corretto in: while (a < 5){ a++; b++; } È inoltre possibile formattare automaticamente tutto il codice, oppure una parte di esso tramite: Format (Ctrl + Shift + F) oppure Format Element, Format a differenza di Correct Identation, correggerà tutta la formattazione, e non solo la tabulazione, quindi anche le spaziature e le righe vuote. La terza parte del menù ci permette invece di riordinare il codice e di ripulirlo, ci permette di aggiungere degli import tramite Add Import (Ctrl + Shift + M), basterà infatti posizionarsi (non evidenziarlo) con il cursore su di un elemento che richieda l’import ed esso verrà aggiunto automaticamente, è anche possibile importare direttamente i metodi statici per poterli richiamare direttamente, ad esempio: package com.test.beta; import com.test.alpha.ClasseX; public class ClasseY{ public ClasseY(){ ClasseX.metodoX(); } } Può essere scritto anche come: package com.test.beta; import static com.test.alpha.ClasseX.metodoX; public class ClasseY{ public ClasseY(){ metodoX(); } } 129 Selezionando quindi ClasseX verrà importata la ClasseX, selezionando invece metodoX verrà importato il metodoX in maniera statica, in questo modo possiamo richiamare il metodo come se fosse ereditato. Organize Imports (Ctrl + Shift + O) invece riordinerà i vari import ed eventualmente aggiungerà gli import mancanti e rimuoverà gli import inutilizzati. Sort Members riordinerà invece le varie variabili metodi e quant’altro inoltre rimuoverà le variabili inutilizzate. Clean Up, ottimizzerà invece il codice, possiamo decidere noi in che modo ottimizzare il codice, ad esempio possiamo rendere final tutte le variabili che vengono assegnate una sola volta. La quarta parte del menù riguarda invece i metodi ed i costruttori: Override/Implement Methods… Ci permetterà di eseguire l’implementazione dei metodi ereditati in modo automatico. l’override o Generate Getters and Setters… Ci permetterà invece di generare i getters e i setters in modo automatico delle variabili desiderate. Generate Delegate Method… Invece creerà un metodo uguale al metodo di un determinato oggetto che richiama tale metodo, ad esempio immaginiamo di avere: package com.test.beta; public class ClasseX{ public ClasseX(){ } public void MetodoVoid(){ //Codice } public int MetodoInt(int arg0){ return arg0; } } Creando un istanza di ClasseX in un altra classe, potremmo creare dei metodi delegate in questo modo: package com.test.beta; public class ClasseY{ ClasseX cx = new ClasseX(); 130 public ClasseY(){ } public void MetodoVoid(){ cx.MetodoVoid() } public int MetodoInt(int arg0){ return cx.MetodoInt(arg0); } } Attenzione però, in quanto i metodi delegati non sono override, dato che tali metodi non vengono ereditati dalla classe, in quanto si tratta di metodi di un istanza della classe, in questo caso cx, utilizzare l’annotazione @Override genererà un errore in quanto ClasseY non estende ClasseX, ma semplicemente la istanzia, l’utilizzo del metodo delegato ci permette di eseguire tali metodi da un istanza della nostra classe. Generate toString()… questa funzione ci permetterà di generare l’override del metodo toString(), tale metodo ci permette di convertire una variabile in una Stringa, il metodo toString() è un metodo della classe Object, quindi viene ereditato da tutte le classi, visto che tutte le classi estendono direttamento o indirettamente la classe Object, questo metodo verrà invocato automaticamente se inseriamo una variabile tra delle stringhe, ad esempio: String stringa = "Valore: " + variabileNonString; Equivale a: String stringa = "Valore: " + variabileNonString.toString(); Esso però viene sottointeso solo se inseriamo la variabile tra una o più stringhe, ma non se da sola, es: String stringa = variabileNonString; // No String stringa = variabileNonString.toString(); // Si Tramite l’override del metodo toString() possiamo definire la stringa che verrà restituita ad esempio: @Override public String toString() { return "NomeClasse [variabile=" + variabile + "]"; } Abbiamo poi Geneate hashCode() and equals()… esso ci permette di eseguire l’override dei metodi hashCode() ed equals() della classe Object, hashCode() servirà a generare un codice rappresentativo dell’istanza della classe, possiamo decidere noi al momento della creazione dell’override quali variabili prendere in considerazione, il 131 metodo equals(Object obj); invece controllerà che l’Object dato come parametro sia equivalente all’istanza della classe, restituirà true se coincide, false se non coincide. Generate Constructor using Fields… ci permetterà di creare un costruttore avente come parametri le variabili selezionate, inoltre imposterà il valore di tali variabili tramite i propri parametri, ad esempio: package com.test.beta; public class ClasseX{ private int i; private String s; public ClasseX(int i, String s){ this.i = i; this.s = s; } } Utilizzando invece Generate Constructor from Superclass… potremmo generare dei costruttori che inizializzino I costruttori della superclasse, quindi ipotizziamo di avere la seguente superclasse: package com.test.beta; public class SuperClasse{ private int i; private String s; public SuperClasse(int i, String s){ this.i = i; this.s = s; } public SuperClasse(String s){ i = 0; this.s = s; } } Utilizzando Generate Constructor from Superclass… Poremmo selezionare quali costruttori inizializzare, supponendo di sceglierli tutti, otterremmo il seguente codice autogenerato: package com.test.beta; public class SottoClasse extends SuperClasse{ public SottoClasse(int i, String s){ super(i, s); // TODO Auto-generated constructor stub } 132 public SottoClasse(String s){ super(s); // TODO Auto-generated constructor stub } } Ciò ci servirà in quando SuperClasse non ha un costruttore di default, quindi come abbiamo già visto in precedenza dobbiamo necessariamente inizializzare un costruttore della classe madre in ogni costruttore della classe figlia, se tale costruttore non viene specificato verrà utilizzato il costruttore di default super(); se disponibile, altrimenti si incorrerà in un errore di compilazione, questa funzione inoltre ci permette di vedere quali costruttori ha la classe madre. Abbiamo poi il sottomenù Surround With (Alt + Shift + Z), che ci permetterà di inserire il codice evidenziato in un blocco autogenerato, i blocchi utilizabili sono i seguenti: Try / Catch Try/Multi-Catch Ciclo do while Ciclo while Ciclo for If Runnable Synchronized Questo ci permetterà di scrivere il nostro codice più agilmente in quanto non dovremmo riscrivere manualmente ogni blocco, ma solo riadattarlo alle nostre esigenze. Abbiamo infine Externalize Strings… che ci permetterà di spostare le stringhe letterali in una classe esterna, e sostituire tali stringhe con referenze a tale classe, ciò ci servirà per creare programmi facilmente localizzabili in più lingue, in quanto potremmo esternalizzare tutte le stringhe da tradurre, qusto ci può tornare utile se vogliamo lasciare la scelta della lingua all’utente, altrimenti potremo utilizzare gli strumenti che ci mette a disposizione android per fare in modo che venga scelata automaticamente la localizzazione corretta se disponibile, tramite strings.xml in 133 res\values, a breve vedremo come localizzare un app, sia tramite externalize strings che tramite strings.xml. Come ultima voce abbiamo Find Broken Externalized Strings, che selezionando direttamente la classe o il rispettivo package, ci permette di trovare ed eliminare tutte le stringhe esternalizzate inutilizzate, alcune voci del menù source infatti possono essere applicate anche a tutto il package, come ad esempio format che se selezionato avendo selezionato il package dal gestore di Eclipse (non dal sorgente) formatterà tutto il package. 3.13 I Timer Nelle nostre applicazioni, potremmo avere la necessità di eseguire delle operazioni allo scadere di un timer, nel caso di un thread separato abbiamo visto come sia possibile sospenderne l’esecuzione pur un dato periodo di tempo grazie al metodo statico sleep(long time); che ci permetterà di sospendere il thread per x millisecondi, è però possibile eseguire un timer direttamente all’interno del thread principale, o anche di un thread secondario, senza sospendrlo, grazie alla classe Handler, per fare ciò possiamo utilizzare il metodo postAtTime(Runnable r, long uptimeMillis); che ci permetterà di eseguire un determinato Runnable dopo x millisecondi dall’avvio del dispositivo, il metodo postDelayed(Runnable r, long delayMillis); invece eseguirà il Runnable dopo x millisecondi dalla chiamata del metodo, entrambi i metodi a differenza di sleep, non sono statici, quindi dovremmo istanziare la classe Handler per poterli utilizzare, inoltre come parametro dovremmo specificare un Runnable, abbiamo accennato a Runnable nel paragrafo precedente, parlando di Surround With… Runnable è un interfaccia contenente il metodo astratto run(); Ora parlando delle classi astratte e dei metodi abbiamo affermato che essi non possano essere istanziati, ed in effetti essi non possono essere direttamente istanziati, ma possono essere istanziati se ne implementiamo tutti i metodi, un po’ come avviene quando eseguiamo un override di un metodo di una classe che stiamo istanziando, che è ciò che facciamo quando istanziamo l’interfaccio View.onClickListener implementando il metodo astratto onClick(View v); la stessa cosa accade con l’interfaccia Runnable, per istanziarla infatti dovremmo implementare tutti i suoi metodi astratti, nel caso di Runnable, solo il metodo void run(); in questo modo: Runnable r = new Runnable() { @Override public void run(){ //Codice } } 134 I metodi postDelayed e postAtTime necessitano come parametro un Runnable, in quanto al momento opportune eseguiranno l’implementazione del metodo run di tale interfaccia, vediamo ora un esempio: package com.test.beta; import android.os.Handler; public class Timer1{ public String s1, s2; public Timer1(){ new Handler().postDelayed(new Runnable(){ @Override public void run() { s1 = "Sono passati 30 secondi"; } }, 30000); s2 = "Sono passati 0 secondi"; } } In questo esempio dopo 30 secondi dalla chiamata del metodo postDelayed, verrà eseguito s1 = "Sono passati 30 secondi"; Il tutto senza sospendere l’esecuzione, quindi nel frattempo verrà eseguito anche s2 = "Sono passati 0 secondi"; Per migliorare la leggibilità del codice, possiamo anche utilizzare variabili o costanti per istanziare gli ogetti, ad esempio: package com.test.beta; import android.os.Handler; public class Timer1{ public String s1, s2; private static final Handler h = new Handler(); private static final Runnable r = new Runnable(){ @Override public void run() { s1 = "Sono passati 30 secondi"; } }; public Timer1(){ h.postDelayed(r, 30000); s2 = "Sono passati 0 secondi"; } } Il funzionamento è identico, ma il codice risulta più leggibile. Possiamo inoltre utilizzare alcuini metodi della classe SystemClock per gestire il tempo, ad esempio con il metodo uptimeMillis(); potremo ottenere i millisecondi 135 trascorsi da quando abbiamo acceso il dispositivo, escludendo i momenti di sospensione (schermo nero), mentre con elapsedRealtime() ed elapsedRealtimeNanos() potremo ottenere rispettivamente i millisecondi e i nanosecondi trascorsi dall’accensione del dipositivo, compresi i momenti di sospensione, con currentThreadTimeMillis() invece potremo ottenere i millisecondi trascorsi dall’avvio del thread, infine con setCurrentTimeMillis(long millis) potremo modificare l’orologio di sistema, ma solamente se abbiamo specificato i permessi necessari dal file AndroidManifest.xml inoltre possiamo sempre utilizzare il metodo sleep(long millis) del metodo Thread, ricordandoci però che può lanciare l’eccezione checked InterruptedException se il Thread viene interrotto durante lo sleep, ed essendo un eccezione checked, va obbligatoriamente catturata. Java però ci mette anche a disposizione le classi java.util.Timer e java.util.TimerTask, grazie ad esse potremmo definire dei timer che verranno eseguiti in thread separati, inoltre è anche possibile eseguire delle oprazioni ripetute a cadenze regolari, per farlo ci basterà istanziare un Timer, poi potremo avviare il timer tramite i metodi schedule e scheduleAtFixedRate che ci permetteranno di eseguire un TimerTask, TimerTask è una classe astratta che implementa Runnable, per cui funziona in maniera analoga al Runnable del metodo postDelayed, schedule ci peretterà di eseguire l’operazione dopo un certo numero di millisecondi, o ad una data precisa, mentre scheduleAtFixedRate ci permetterà di eseguire tali operazioni ad un ritmo regolare, in realtà anche schedule ci permette di ripetere l’operazione, ma con una sostanziale differenza, nel caso una ripetizione dovesse venir ritardata, nel caso di schedule, avremo un ritardo generale, in quanto le operazioni verranno ripetute con la stessa cadenza, mentre nel caso di scheduleAtFixedRate, in caso di ritardo, esso verrà recuperato riducendo il ritardo tra l’operazione in ritardo e l’operazione o eventualmente le operazioni successive, ad esempio immaginiamo di avere: t.schedule(task, 500, 1000); //Ritardo: 500 > 1000 > 1000 > 1200 > 1000 > 1000 Totale: 5700 t.scheduleAtFixedRate(task, 500, 1000) //Ritardo: 500 > 1000 > 1000 > 1200 > 800 >1000 Totale: 5500 Per cui scheduleAtFixedRate risulterà più preciso ad esempio se vogliamo realizzare un orologio, i parametri dei metodi sono i seguenti: (TimerTask (TimerTask (TimerTask (TimerTask task, task, task, task, Date long Date long when) delay) when, long period) delay, long period) TimerTask è una classe astratta che implementa Runnable e va istanziata così: 136 new TimerTask() { @Override public void run() { // Codice } }; La differenza con Runnable però è data dal fatto che Runnable sia un interfaccia mentre TimerTask è una classe astratta, quindi contiene anche dei metodi non astratti, ovvero il metodo cancel() che ci permetterà di annullare l’esecuzione ripetuta del metodo run() ed il metodo scheduledExecutionTime() che invece ci restituisce il momento dell’ultima esecuzione, il secondo parametro è un long oppure un Date, per quanto riguarda Date, si tratta sempre di una classe contenuta all’interno del package java.util, che può essere utilizzata per definire una data, anche se la maggiorparte dei suoi metodi sono deprecati, ovvero considerati obsoleti, essi possono comunque essere utilizzati, in alternativa il metodo getTime() della classe GregorianCalendar, ci restituirà un oggetto di tipo Date, la differenza tra il parametro long delay ed il parametro Date when, sta nel fatto che delay indica i millisecondi tra cui verrà eseguito il metodo run() del TimerTask, mentre il when indica una data specifica in cui verrà eseguito il metodo, infine abbiamo un eventuale long period, che indica i millisecondi tra le varie ripetizioni, se assente l’operazione non verrà ripetuta, se ripetuta, l’operazione verrà eseguita indefinitamente, fintanto che il Timer od il TimerTask non vengano interrotti dal rispettivo metodo cancel(), attenzione però, in quanto un TimerTask già utilizzato non può essere riutilizzato neanche se cancellato, in quanto i metodi schedule e scheduleAtFixedRate ci lanceranno un IllegalStateException, per poter riutilizzare il TimerTask dovremmo istanziarlo nuovamente, la stessa cosa se accade per il Timer se utilizziamo il metodo cancel(), possiamo però utilizzarlo più volte se non lo cancelliamo, a differenza del TimerTask, la classe Timer è sicuramente il tipo di timer più affidabile e completo, ma se necessitiamo di un timer monouso che lavori sul thread principale, la classe Handler ci può tornare decisamente utile, in quanto Timer esegue le operazioni in un thread separato, questo potrebbe essere un problema quando interagiamo con oggetti che estendono la classe View, in quanto solo il thread che ha creato la View può modificarla, per cui ad esempio chiamare il metodo setText di una TextView, da un thread diverso, e quindi anche da un thread generato da Timer, ci porterà al lancio di un eccezione, per evitare questo problema, possiamo utilizzare il metodo runOnUiThread(Runnable action) della classe Activity, esso ci permettera di eseguire un determinato Runnable all’interno del main thread, ovvero il thread principale, ciò anche se tale metodo è chiamato da un altro thread, permettendoci di modificare le View, proviamo ora a vedere come realizzare un semplice cronometro: 137 package com.example.timer; import java.util.Timer; import java.util.TimerTask; import import import import import android.os.Bundle; android.support.v7.app.ActionBarActivity; android.view.View; android.widget.Button; android.widget.TextView; public class MainActivity extends ActionBarActivity { private int h = 0; private byte m = 0, s = 0; private Button start, stop, reset; private boolean t_state = false; private final Timer t = new Timer(); private TimerTask task; private TextView txt; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); txt = (TextView) findViewById(R.id.textView1); start = (Button) findViewById(R.id.start); stop = (Button) findViewById(R.id.stop); reset = (Button) findViewById(R.id.reset); start.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (!t_state) { task = new TimerTask() { @Override public void run() { s++; if (s >= 60) { s -= 60; m++; if (m >= 60) { m -= 60; h++; } } refresh(); } }; t.scheduleAtFixedRate(task, 1000, 1000); t_state = true; } } }); 138 stop.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { task.cancel(); t_state = false; } }); reset.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { h = 0; m = 0; s = 0; refresh(); } }); } public void refresh() { final CharSequence m_tmp, s_tmp; if (m < 10) m_tmp = "0" + m; else m_tmp = m + ""; if (s < 10) s_tmp = "0" + s; else s_tmp = s + ""; final Runnable r = new Runnable() { public void run() { txt.setText(h + ":" + m_tmp + ":" + s_tmp); } }; runOnUiThread(r); } } In questo esempio abbiamo una TextView txt e tre Button start, stop e reset, alla pressione di start, viene eseguito un timer ripetuto che incrementa s di 1 ogni 1000 millisecondi, se s supera 60 viene incrementato m di 1 e ridotto s di 60, se m supera 60, viene incrementato h di 1 e ridotto m di 60, inoltre viene chiamato il metodo refresh che aggiorna la TextView tramite il metodo runOnUiThread ed il Runnable r, in modo che setText venga eseguito all’interno del main thread, inoltre grazie alle variabili m_tmp e s_tmp, viene aggiunto un eventuale zero davanti alle cifre singole 139 (0-9) in modo da visualizzare sempre numeri a due cifre per quanto riguarda i minuti e i secondi, la pressione del tasto stop cancellerà il TimerTask ma non azzererà le variabili h, m, s, quindi un eventuale pressione di start, farà ripartire il timer, infine il tasto reset, azzererà le variabili h, m, s e chiamerà il metodo refresh(), ma non fermerà il timer che ricomincerà semplicemente da zero, il metodo scheduleAtFixedRate ci garantirà precisione rispetto al metodo schedule, vediamo ora come utilizzare il parametro Date when, per fare questo avremmo bisogno della classe Date, oppure della classe GregorianCalendar, la classe Date è più semplice, ma i suoi metodi sono deprecati, mentre la classe GregorianCalendar è più complessa ma i suoi metodi non sono deprecati, anche se spesso non è un problema utilizzare metodi deprecati, è buona norma evitarli quando possibile, comunque sia, vediamo entrambi i metodi: package com.example.timer; import import import import import java.util.Timer; java.util.TimerTask; java.util.Calendar; java.util.Date; java.util.GregorianCalendar; import import import import android.os.Bundle; android.support.v7.app.ActionBarActivity; android.view.View; android.widget.TextView; public class MainActivity extends ActionBarActivity { private private private private private private final Timer t = new Timer(); final Date date = new Date(); final Calendar cal = new GregorianCalendar(); TimerTask t1, t2; final TextView[] txt = new TextView[2]; static final FULL_DAY = 1000*60*60*24 // 24h in millisecondi @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); txt[0] = (TextView) findViewById(R.id.textView1); txt[1] = (TextView) findViewById(R.id.textView2); t1 = new TimerTask() { @Override public void run() { task("Date", 0); } }; t2 = new TimerTask() { @Override public void run() { task("GregorianCalendar", 1); } }; 140 // Metodi deprecati date.setHours(20); date.setMinutes(00); date.setSeconds(00); // Metodi non deprecati cal.set(Calendar.HOUR_OF_DAY, 20); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); t.scheduleAtFixedRate(t1, date, FULL_DAY); t.scheduleAtFixedRate(t2, cal.getTime(), FULL_DAY); } private void task(String arg0, int n){ Runnable r = new Runnable(){ @Override public void run() { txt[n].setText("Timer: " + arg0); } }; runOnUiThread(r); } } Inizializzare un oggetto Date o Calendar, con il costruttore di default, lo inizializzerà con data ed ora corrente, quindi il nostro timer verrà eseguito alle ore 20:00:00 dello stesso giorno, in quanto la data non è stata specificata, ma soltanto l’ora, entrambi i timer funzioneranno in maniera analoga e verranno eseguita alle ore 20:00:00, inoltre verranno ripetuti ogni 24 ore, quindi alle ore 20 di ogni giorno, possiamo anche modificare la data, non solo l’ora, possiamo quindi impostare un giorno del mese, oppure giorno e mese, come anche l’anno. 3.14 I Toast Spesso capita di dover comunicare qualcosa all’utente, per fare questo possiamo affidarci alla classe Toast. Un Toast è un messaggio visualizzato nella parte inferiore dello schermo per qualche secondo, per fare questo possiamo utilizzare il metodo: public static Toast makeText (Context context, CharSequence text, int duration) Che ci restituirà un Toast, che può essere poi visualizzato tramite il metodo show(); context è il contensto dell’activity o dell’applicazione, di solito possiamo utilizzare this, text è il testo da visualizzare, può essere sostituito da un riferimento alla classe R, ad esempio R.string.hello_world, mentre duration è la durata, è pssoibile scegliere tra Toast.LENGTH_SHORT (0) oppure Toast.LENGHT_LONG (1), siccome il metodo restituisce 141 un oggetto di tipo Toast, possiamo visualizzarlo immediatamente aggiungendo il metodo show(), ad esempio: Toast.makeText(this, "Messaggio", Toast.LENGTH_SHORT).show(); Verrà visualizzato quindi Messaggio in un popup in basso, con un piccolo margine, per un periodo breve, restituendo un oggetto di tipo Toast, è anche possibile assegnarlo ad una variabile, ed in seguito modificarlo e/o visualizzarlo, inoltre è anche possibile modificarne la posizione, vediamo ora un esempio: Toast t = Toast.makeText(this, "Messaggio", Toast.LENGTH_SHORT); t.setText("Nuovo Messaggio"); t.setGravity(Gravity.TOP, 0, 10); t.setDuration(Toast.LENGTH_LONG); t.show(); In questo caso invece verrà visualizzato Nuovo Messaggio, e non Messaggio, in quanto abbiamo utilizzato: setText("Nuovo Messaggio") inoltre esso verrà visualizzato in alto e non più in basso, sempre con un piccolo margine (10), poiché abbiamo utilizzato: setGravity(Gravity.TOP, 0, 10) inoltre la durata sarà maggiore, dato che abbiamo utilizzato: setDuration(Toast.LENGTH_LONG), inoltre avendolo definito come variabile, possiamo visualizzara questo Toast ogni volta che vogliamo, utilizzando t.show(), Attenzione però i Toast vanno obbligatoriamente creati con il metodo makeText, e non istanziando la classe Toast, percui: Toast t = new Toast(this); t.setText("Messaggio"); // RuntimeException, t non è stato creato con makeText t.setDuration(Toast.LENGTH_LONG); t.show(); Lancerà una makeText. RuntimeException in quanto il Toast non è stato creato con il metodo Per quanto riguarda setDuration ed il parametro duration di makeText, bisogna tener conto, che anche se si tratta di un int, esso viene trattato come un flag, e riconosce solamente i valori delle costanti LENGTH_SHORT e LENGTH_LONG, che sono rispettivamente 0 e 1, per cui è possibile utilizzare tali valori, anche se Eclipse ci darà un warning in quanto non stiamo utilizzando direttamente Toast.LENGTH_SHORT o Toast.LENGTH_LONG, mentre l’utilizzo di valori diversi da 0 e 1 verrà considerato come 0 e non lancerà quindi un eccezione, anche se abbiamo solamente due tipi di durate, è possibile modificarle utilizzando alcuni artifizi. Prima di tutto bisogna considerare che LENGTH_SHORT, farà durare il Toast 2000 millisecondi (2 secondi) mentre LENGTH_LONG 3500 millisecondi, (3.5 Secondi), bisogna poi considerare che chiamare nuovamente il metodo show() sullo stesso 142 Toast ne resetterà la durata, per cui se chiamiamo nuovamente il metodo show() di un Toast da 2000 millisecondi dopo 1000 millisecondi esso durerà altri 2000 millisecondi dal momento della chiamata, quindi durerà un totale di 3000 millisecondi (1000 passati + i nuovi 2000), e non 4000, quindi chiamare due volte di seguito show() non ne raddoppierà la durata, è inoltre anche possibile accorciarne la durata utilizzando il metodo cancel() prima che esso scompaia, quindi immaginiamo di avere un Toast da 2000 millisecondi, chiamando cancel() dopo 1500 millisecondi esso durerà solo 1500 millisecondi, per fare questo ci basterà chiamare i relativi metodi all’interno di un timer. Vediamo ora degli esempi: … // Dichiariamo le variabili private byte ciclo = 0; private final Toast t = Toast.makeText(this, "Messaggio", Toast.LENGTH_SHORT); private final Timer timer = new Timer(); … timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { t.show(); ciclo++; if (ciclo >= 9) cancel(); // Terminiamo il TimerTask } }, 0, 1000); … In questo esempio abbiamo un Toast da 2000 millisecondi, ed un Timer ripetuto ogni 1000 millisecondi per nove volte, infatti alla nona esecuzione viene chiamato il metodo cancel() del TimerTask non del Toast, per cui il Toast durerà 10000 millisecondi: 1000 + 1000 + 1000 + 1000 + 1000 + 1000 + 1000 + 1000 + 2000, in questo modo possiamo influenzare la durata del Toast, allo stesso modo, possiamo anche accorciare la durata del Toast, ad esempio: … final Toast toast = Toast.makeText(this, "Messaggio", Toast.LENGTH_SHORT); final Timer timer = new Timer(); toast.show(); timer.schedule(new TimerTask() { @Override public void run() { toast.cancel(); } }, 1000); … 143 In questo esempio invece dopo 1000 millisecondi viene chiamato il metodo cancel() di Toast, non di TimerTask questa volta, il Toast quindi scomparirà dopo soli 1000 millisecondi e non dopo i canonici 2000 millisecondi. Abbiamo già visto invece come modificare la posizione del Toast, ci basterà infatti utilizzare il metodo setGravity(int gravity, int xOffset, int yOffset) dove gravity indica la posizione, che può essere recuperata dalle costanti della classe Gravity, ad esempio Gravity.TOP oppure Gravity.BOTTOM, mentre xOffset ed yOffset indicano lo spostamento del relativo asse cartesiano, la posizione predefinita è Gravity.BOTTOM, con un leggero yOffset, una cosa da tenere in mente però, è che non possono essere visualizzati più Toast contemporaneamente, se più Toast devono venir eseguiti contemporaneamente, essi verranno eseguiti in sequenza, anche se in posizioni dello schermo differenti. I Toast sono molto utili perché ci permettono di comunicare con l’utente, senza andare a toccare l’interfaccia grafica. 3.15 Testare le app, l’AVD ed il Debugger Nel mentre si sta creando un applicazione, spesso è necessario testarla, il test può essere fatto sia su un dispositivo Android, che da Eclipse, testare su di un dispositivo fisico, ci assicurerà un miglior feedback, in quanto potremo utilizzare un vero dispositivo, con parti e sensori reali e non emulati, ma dall’altro lato la cosa potrebbe essere più scomoda, per testare l’applicazione su di un dispositivo Android, ci basterà compilarla sotto forma di installabile Android (.apk) firmandolo con una nostra firma, Android di default non accetta applicazioni al di fuori dello store, ma abilitando la voce “Origini Sconosciute” dalle impostazioni di sicurezza del dispositivo, ci sarà possibile istallarle senza problemi, è anche possibile collegare il dispositivo come ADB (Android Debug Bridge), ma sono necessari dei drivers che non funzionano con tutti i dispositivi, quindi possiamo testare direttamente le applicazioni da Eclipse installando una System Image di Android da Window > Android SDK Manager, selezionando la System Image della versione o delle versioni di Android desiderate: 144 Una volta installata la System Image dovremmo creare un AVD (Android Virtual Device) da Window > Android Virtual Device Manager: 145 In cui potremmo creare un nuovo AVD selezionando New… oppure gestire quelli già creati in precedenza, nell’immagine ad esempio sono stati creati tre AVD differenti, con tre System Images differenti, cliccando su New… ci ritroveremo questa finestra: AVD Name: È il nome dell’AVD Device: Si tratta del dispositivo da emulare, ciò determinerà la risoluzione del Display, ed imposterà un valore a RAM e VM Heap. Target: È la System Image da utilizzare, ovvero la versione di Android da emulare. CPU/ABI: Il tipo di processore da emulare, di solito non può essere modificato in quanto di norma abbiamo armeabi, dipende comunque dalla Sytem Image. Keyboard: Indica se utilizzare la tastiera del computer come dispositivo di imput, oppure utilizzare la tastiera virtuale di Android. Skin: L’aspetto della finestra dell’AVD Front Camera: Dispositivo da utililizzare per emulare la fotocamera secondaria. Back Camera: Dispositivo da utililizzare per emulare la fotocamera principale. RAM: Il quantitativo di RAM da assegnare all’AVD, verrà utilizzata parte della ram del Computer. 146 VM Heap: Parte di RAM da utilizzare come chace per le applicazioni, viene sottratta dalla RAM assegnata al dispositivo, quindi deve essere proporzionata ad esso, di solito si utilizzano 16 o 32 in rapporto alla grandezza della RAM. Internal Storage: La quantità di memoria interna dell’AVD. SD Card: La grandezza della scheda di memoria esterna dell’AVD, può essere utilizzato anche un file esterno. Snapshot: Indica se salvare lo stato della RAM dell’AVD, questo ci permetterà un avvio più rapido dell’AVD. Use Host GPU: Indica se utilizzare l’accellerazione grafica Open GL della scheda video del Computer, incrementando le prestazioni dell’AVD. Una volta creato un AVD che soddisfi i requisiti della nostra app, potremo utilizzarlo per testarla, possiamo testarla im maniara semplice, oppure utilizando il debugger, la prospettiva di debug, ci permette di analizzare nel dettaglio il comportamento della nostra app, analizzare gli errori e le eccezioni, oppure i valori delle variabili in un dato momento, come ci da anche la possibilità di modificare manualmente i valori di tali variabili. Per avviare l’AVD senza utilizzare il debugger ci basterà cliccare su Run, o dal menù Run, oppure direttamente dalla barra degli strumenti o premendo ctrl + F11, cliccando su Run Configurations… potremo definire meglio cosa avviare, ad esempio potremo avviare un activity differente dalla principale, mentre Run History conterrà un elenco delle ultime configurazioni lanciate. Per utilizzare anche il debugger dovremo invece cliccare su Debug, o dal menù Run, oppure direttamente dalla barra degli strumenti o premendo F11, come per Run, troveremo anche Debug Configurations… e Debug History, anche nel caso di Debug, varrà avviato l’AVD, ma esso a differenza di Run, verrà collegato al debugger, quindi eclipse ci chiederà se passare alla prospettiva di debug, possiamo passare dalla prospettiva Java a quella di Debug e viceversa in qualsiasi momento, cliccando sui relativi pulasnti la prospettiva di Debug si presenterà così: 147 Abbiamo varie aree della prospettiva di Debug che ho numerato da 1 a 6: 1. Debug – Qui abbiamo l’applicazione di cui stiamo eseguendo il Debug e tutti i suoi thread, potremo quindi vedere in ogni momento quanti thread sono in esecuzione. 2. Variables/Breakpoints/Expressions – Qui potremo vedere i valori delle variabile dell’applicazione ed anche modificarli, inoltre possiamo anche vedere i Breakpoints, ovvero i punti di interruzione, oppure utilizzare delle espressioni per gestire le variabili es (a + b – c) oppure (a = b – c), questa è forse la parte più importante della prospettiva di debug. 3. Codice Sorgente – Qui abbiamo il sorgente dell’applicazione, un po’ come avviene per la prospettiva Java. 4. Outline – Anche essa presente nella prospettiva Java, indica le varie risorse della classe attuale, come ad esempio metodi, inner classes e campi. 5. LogCat – Contiene un log delle attività dell’AVD, è presente anche nella prospettiva Java, ma come scheda. 6. Consolle – Presente anche nella prospettiva Java, indica lo stato di alcune operazioni, quali il lancio di un app su AVD, oppura la modifica di un AVD, abbiamo poi altre schede quali Error Log, che ci mostra un log degli errori, Devices, che mostra gli AVD o gli ADB connessi, e Tasks, che ci indica la posizione di tutti i commenti // TODO che indicano parti di codice da completare, di solito sono generati quando autogeneriamo un metodo, o un override. 148 Ma per eseguire il debug di un applicazione, non ci basterà semplicemente lanciarla, dovremo infatti anche definire dei breakpoints, ovvero dei punti di codice, che prima di essere eseguiti sospenderanno l’applicazione, e ci permetteranno di controllare e modificare le variabili in quel dato momento, non potremo infatti accedere alle variabili se l’esecuzione non viene sospesa da un breakpoint, per impostare un breakpoint, ci bastera cliccare sulla stricetta verticale all’inizio dell’istruzione interessata, cliccandoci aparira un pallino blu, o un altro simbolo a seconda del tipo di breakpoint, cliccando nuovamente sul pallino lo rimuoveremo, eliminando il breakpoint, es: Una volta raggiunto un breakpoint, nella scheda variables, avremo this, in cui troveremo tutte le variabili globali, incluse quelle ereditate, mentre sotto avremo tutte la variabili locali, esse potranno anche essere modificate manualmente, oppure tramite la scheda expressions, ad esempio con: a = 15 oppure possiamo semplicemente leggerne il valore, digitando semplicemente il nome della variabile, ad esempio: Possiamo quindi utilizzare le stesse espressioni che utilizziamo in Java, ad esempio h += s–m che equivale: h = h + s-m per cui se l’applicazione si comporta in modo anomalo, grazie ad un breakpoint, potremo controllarne le variabili ed eventualmente identificare il problema. Potremo inoltre inserendo un breakpoint in un blocco catch, riusciremo tramite la relativa variabile locale individuare il tipo di eccezione, ed anche un eventuale messaggio legato all’eccezione, questo ci aiuterà a capire perché è stata lanciata l’eccezione, ed in caso di un catch generico, anche cosa è stato catturato, dovremmo però sempre impostare un breakpoint, se viene lanciata un eccezione che non viene catturata, l’applicazione verrà interrotta ed il debugger non ci segnalerà la riga di codice che ha generato tale eccezione, quindi dovremo trovarla noi manualmente, utilizzando i breakpoint, considerando che se un breakpoint viene raggiunto vuol dire che l’eccezione non è ancora stata lanciata, mentre se esso non viene raggiunto, l’eccezione è gia stata lanciata, ad esempio: TextView t = null; int a = 15; int b = 10; t.setText("Risultato = " + (a + b)); // NullPointerException (t = null) a = 0; b = 0; 149 Nel nostro esempio abbiamo creato la variabile t, ma non l’abbiamo inizializzata, quindi t è null, andando quindi a richiamare un metodo di t, essendo esso null, verrà lanciata l’eccezzione NullPointerException, che però non viene catturata, quindi eseguendo il debug del codice, vengono raggiunti i primi due breakpoints, ma non viene raggiunto il terzo, quindi l’errore sta tra il secondo e terzo breakpoint, considerando anche la riga del secondo breakpoint, ma non quella del terzo, quindi: TextView t = null; int a = 15; int b = 10; t.setText("Risultato = " + (a + b)); // NullPointerException (t = null) a = 0; b = 0; Dobbiamo quindi cercare l’errore nella zona evideziata, questo comunque non significa che non vi possano essere altri errori dopo la zona evidenziata, o che vi sia un solo errore all’interno di essa, infatti vi potrebbero essere anche più errori, per identificare l’eccezione potremmo utilizzare un blocco try catch generico all’interno della zona evidenziata, quindi: TextView t = null; try{ int a = 15; int b = 10; t.setText("Risultato = " + (a + b)); // NullPointerException (t = null) a = 0; }catch(Exception e){ e.printStackTrace(); } b = 0; Il metodo printStackTrace() ci permetterà di registrare nel System.err l’eccezione, e quindi visualizzarne i dettagli tramite il LogCat, con la tag System.err, si tratta infatti di un PrintStream, volendo possiamo anche inserire dei messaggi che verranno visualizzati nel LogCat, ad esempio con: System.err.println("Errore Sconosciuto"); Verrà visualizzato nel LogCat, con tag System.err il messaggio Errore Sconosciuto, utilizzando quindi printStackTrace() ci verrà montrato nel LogCat il tipo di eccezione e la posizione ad esempio: 150 Nell’immagine d’esempio, sono stati utilizzati i metodi: System.err.println("Test"); e.printStackTrace(); Ed è stata lanciata una NullPointerException nella riga 44 della classe MainActivity.java, all’interno del metodo onCreate, quindi ipotizzando la seguente applicazione: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.test.logtest; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; import android.widget.TextView; public class MainActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView t = null; int b; try { int a = 15; b = 10; t.setText("Risultato = " + (a + b)); a = 0; } catch (Exception e) { e.printStackTrace(); } b = 0; } } Lanciando questa applicazione, anche senza utilizzare alcun breakpoint, nel LogCat verrà visualizzato tra le altre cose: Application Tag Text com.test.logtest com.test.logtest System.err System.err Java.lang.NullPointerException at com.test.logtest.MainActivity.onCreate (MainActiviy.java:19) Ovvero un NullPointerException alla riga 19 all’interno del metodo onCreate della classe MainActivity.java nel pacchetto com.test.logtest, per visualizzare i PrintStream di sistema della nostra applicazione, dovremmo utilizzare il debugger, altrimenti non verranno mostrati nel LogCat, possiamo anche mostrare messaggi non di errore nel logcat utilizzado System.out invece di System.err, sempre tramite il metodo println, il messaggio verrà visualizzato di colore verde (Info), mentre nel caso di System.err 151 verrà visualizzato color arancione (Warning), è anche possibile utilizzare la classe Log per visualizzare messaggi nel LogCat, abbiamo sei livelli di gravità, selezionando un livello di gravità dal LogCat, verranno visualizzati tutti i messaggi di tale livello o di livello superiore, i livelli sono i seguenti: 1. 2. 3. 4. 5. 6. Verbose (Nero) Debug (Blu) Info (Verde) Warning (Arancione) Error (Rosso) Assert (Rosso) È quindi possibile utilizzare i seguenti metodi statici per visualizzare messaggi nel Logcat: Log.v(String tag, String text); // Verbose Log.d(String tag, String text); // Debug Log.i(String tag, String text); // Info Log.w(String tag, String text); // Warning Log.e(String tag, String text); // Error Log.wtf(String tag, String text); // Assert, solo per gli errori più gravi Ogni metodo può avere come parametro aggiuntivo anche un Throwable, che verrà mostrato nel logcat in maniera simile a printStackTrace, ma con il tag da noi definito, e quindi non nel System.err, ogni metodo mostrerà il messaggio con la relativa tag nel ed il relativo livello nel LogCat, il metodo wtf però, si comporterà come un vero e proprio errore dal punto di vista del LogCat, mostrando quindi anche altre informazioni relative all’errore, il metodo wtf è un metodo relativo agli errori più gravi, e non deve essere usato per visualizzare messaggi di log nel LogCat, a differenza degli altri metodi inoltre, visualizzerà come messaggio: androit.util.Log$TerribleFailure: text dove text è il parametro text del metodo, gli altri metodi visualizzeranno invece solo text e l’eventuale Throwable, i log sono utili in fase di debug, in quanto non necessitano di breakpoint per essere letti, quindi ad esempio: … int a = 5, b = 19; Log.v("Variabili", "a = " + a + " b = " + b); … Verrà mostrato nel LogCat, una voce di tipo Verbose, con la Tag: Variabili ed il Text: a = 5 b = 19 in questo modo possiamo controllare lo stato delle variabili direttamente dal LogCat, senza utilizzare breakpoint, quindi senza sospendere l’applicazione, inoltre tali messaggi permangono nel LogCat, dandoci quindi anche un quadro 152 dell’evoluzione delle variabili, il debugger quindi è uno strumento essenziale per ogni buon programmatore, in quanto ci permette di studiare il comportamento della nostra app nei minime dettagli, esaminare le eventuali eccezioni e quindi trovare con maggior facilità gli eventuali difetti di programmazione, il debugger puòessere utilizzato anche collegando un dispoitivo Android come ADB, abilitando il Debug USB dal menù Opzioni Sviluppatore del nostro dispositivo, per utilizzare l’ADB sono necessari dei driver ADB compatibili con il nostro dispositivo, per utilizzare un dispositivo come ADB, prima di tutto dovremmo procurarci i drivers, possiamo scaricarli direttamente dall’SDK manager, ma non sono compatibili con tutti i dispositivi, poi ci basterà abilitare Debug USB dalle opzioni sviluppatore, nei dispositivi più recenti (4.2 +) per abilitare le opzioni sviluppatore, bisognerà andare su Info sul Telefono e premere sette volte sulla voce Numero Build, una volta addivato il Debug USB, dovrebbero installarsi i drivers ADB, se la periferica non dovesse venir riconosciuta, bisognerà procurarsi dei driver compatibli, una volta installati i drivers, selezionando Run o Debug da Eclipse, dovrebbe apparire tra i dispositivi collegati nella finestra superiore il nostro dispositivo, selezionandolo, verra utilizzato in sostituzione dell’AVD, utilizzando l’ADB verranno mostrati moltissime voci nel LogCat, questo potrebbe rendere più complesso l’utilizzo del Log, per rendere il LogCat più leggibile, possiamo impostare dei filtri, quindi ad esempio mostrare solo voci con una determinata Tag, lanciando un app, tramite l’ADB, Eclipse crea automaticamente un filtro relativo a tale app, quindi ci mostrerà solamente le voci relative alla nostra app, che saranno comunque più di quelle mostrate utilizzando l’AVD, quindi l’utilizzo di filtri è consigliato, per aggungere un filtro, ci basterà premere su + dopo Saved Filters nella parte sinistra del LogCat, cliccando poi su un filtro esso verrà applicato, per eliminarlo ci bastera selezionarlo e premere su – nel momento in cui specifichiamo un nuovo filtro, possiamo utilizzare più parametri, quali ad esempio il Tag, l’applicazione e/o il livello, per cui potremo utilizzare un TAG comune ai nostri messaggi di debug, e filtrare in base a quel tag, l’utilizzo dell’ADB quando possibile è consigliabile rispetto all’AVD in quanto ci permette di utilizzare un dispositivo reale, con parti hardware reali e non emulate, quindi potremmo testare anche le funzioni relative ai sensori, quali ad esempio il giroscopio, oppure il sensore di luminosità, nel prossimo capitolo vedremo come lavorare con i file e come utilizzare l’hardware del dispositivo. 153 CAPITOLO 4 Interfacciarsi con il dispositivo 4.1 La serializzazione Abbiamo già visto come passare dati tra le activity e come lavorare con le variabili, ciò che abbiamo fatto fino ad ora però riguardava solamente variabili memorizzate in RAM, il cui valore viene perso una volta chiusa l’applicazione, nella maggiorparte dei casi però avremmo bisogno di memorizzare delle informazioni che verranno mantenute anche dopo la chiusura dell’applicazione, per fare questo, possiamo ricorrere alla serializzazione, ciò ci permetterà di memorizzare una o più variabili in uno o più file da noi definiti, tali file saranno memorizzati nella cartella interna dell’applicazione, l’utilizzo di tale tecnica quindi non necessita di speciali autorizzazioni, in quanto non scriviamo sulla memoria esterna, grazie alla serializzazione possiamo memorizzare una o più variabili all’interno di un file, è anche possibile memorizzare un Array oppure un ArrayList, un’istanza di una classe oppure un Array/ArrayList di istanze di classi, come unica variabile, ciò renderà più semplice la deserializzazione nel caso di molte variabili, è anche possibile serializzare più variabili in un unico file, in Android la serializzazione avviene in maniera leggermente differente rispetto al Java puro, il funzionamento è però analogo, la prima cosa da fare è definire il file su cui serializzare, che se non presente verrà creato, per fare questo utilizzeremo il seguente codice: … ObjetOutputStream oos; FileOutputSteam fos; try{ fos = this.openFileOutput("nome.estensione", this.MODE_PRIVATE); oos = new ObjectOutputStream(fos); // Codice oos.close(); }catch (Exception e) { e.printStackTrace(); } … Ciò che abbiamo fatto, è stato creare un ObjectOutputStream, che come paramentro del costruttore necessita un FileOutputStream, che può essere ottenuto tramite il metodo openFileOutput(String name, int mode) di Context, dove il Comtext deve essere il context della nostra activity, se non siamo nel context dell’activity perché ad esempio siamo in un fragment o in una classe esterna, possiamo sempre utilizzare una variabile Context, name è un parametro String che indica il nome del file da utilizzare, mentre mode è un parametro int che indica la modalità di utilizzo del file, esistiono quattro diverse modalità: 154 MODE_PRIVATE (0) – Crea un nuovo file, oppure sovrascrive quello esistente, tale file potrà essere utilizzato solamente dall’applicazione. Valore: 0 MODE_WORLD_READABLE (1) – Deprecato, crea un nuovo file, oppure sovrascrive quello esistente, tale file potrà essere letto da qualunque applicazione, ma scritto solo dalla nostra applicazione, Android sconsiglia l’utilizzo di tale modalità in quanto deprecata. Valore: 1 MODE_WORLD_WRITABLE (2) – Deprecato, crea un nuovo file, oppure sovrascrive quello esistente, tale file potrà essere letto e scritto da qualunque applicazione, Android sconsiglia l’utilizzo di tale modalità in quanto deprecata. Valore: 2 MODE_APPEND (32768) – Crea un nuovo file, oppure accoda i dati al file esistente. Valore: 32768 Trattandosi di costanti int, possiamo anche utilizzare i loro valori assoluti come parametro, ad esempio possiamo utilizzare 0 invece di MODE_PRIVATE, sia il metodo openFileOutput, che il costruttore dell’ObjectOutputStream, prevedono delle eccezioni checked (IOException, FileNotFoundException) che devono essere obbligatoriamente gestite, per cui dovremmo necessariamente utilizzare un blocco try catch. Una volta creato il file, dovremmo utilizzarlo per serializzare i nostri dati, per farlo possiamo utilizzare il generico metodo writeObject(Object object) oppure metodi specifici quali ad esempio writeInt(int value), possiamo anche serializzare più variabili nel file, vediamo ora come serializzare due variabili: … private int[] array = {1, 10, 100}; private int i = 1000; private final Context CTX = this; … ObjetOutputStream oos; FileOutputSteam fos; try{ fos = CTX.openFileOutput("File1.ser", CTX.MODE_PRIVATE); oos = new ObjectOutputStream(fos); oos.writeObject(array); // Per gli array utiliziamo writeObject oos.writeInt(i); oos.close(); // Chiudiamo il file } 155 catch (Exception e) { e.printStackTrace(); Toast.makeText(CTX, e.toString(), Toast.LENGTH_SHORT).show(); } … In questo modo abbiamo scritto nel file: File1.ser i valori delle variabili array e i, tali valori saranno conservati finché il file non sarà sovrascritto o eliminato, il che significa che il file resterà anche dopo la chiusura dell’applicazione, dopo aver serializzato i dati dovremmo chiudere l’ObjectOutputStream tramite il metodo close(). Una volta serializzato un file, per poter recuerare i dati al suo interno, dovremmo deserializzarlo, ovvero leggere il suo contenuto, ciò va fatto in maniera analoga alla serializzazione, ma con la differenza che utilizzeremo invece un ObjectInputStream ed un FileInputStream nel seguente modo: … ObjetInputStream ois; FileInputSteam fis; try{ fis = this.openFileInput("nome.estensione"); ois = new ObjectInputStream(fis); // Codice ois.close(); }catch (Exception e) { e.printStackTrace(); } … L’unica differenza con la scrittura del file, oltre alla sostituzione di Output con Input, è che il metodo openFileInput a differenza di openFileOutput ha solamente il parametro String name, per il resto la procedura è speculare alla serializzazione, una volta aperto il file in lettura, dovremmo recuperarne il contenuto tramite i relativi metodi, attenzione però, il file viene letto in maniera sequenziale, quindi dobbiamo utilizzare i metodi di lettura relativi ai metodi di scrittura utilizzati nel giusto ordine, quindi se abbiamo serializzato prima con writeObject e poi con writeInt, dovremmo deserializzare prima con readObject e poi con readInt, è anche possibile utilizzare il metodo read(byte[] buffer) per inserire in un array byte buffer.length() byte del file, ad esempio quindi con un array di lunghezza 4 possiamo deserializzare un int oppure due short o quattro byte, una volta deserializzato tutto il file, continuarne la lettura lancierà un EOFException, per cui bisognerà chiudere il file con close(), vediamo ora un esempio di deserializzazione: package com.test.ser; import java.io.FileInputStream; import java.io.FileOutputStream; 156 import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import android.support.v7.app.ActionBarActivity; import android.os.Bundle; import android.widget.Toast; public class MainActivity extends ActionBarActivity { private static final String FILE = "File1.dat"; private static int[] array; private static int i; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main); int[] tmp0 = {1, 10, 100}; int tmp1 = 1000; ObjetOutputStream oos; FileOutputSteam fos; try{ fos = this.openFileOutput(FILE, this.MODE_PRIVATE); oos = new ObjectOutputStream(fos); oos.writeObject(tmp0); oos.writeInt(tmp1); oos.close(); } catch (Exception e) { e.printStackTrace(); Toast.makeText(this, e.toString(), Toast.LENGTH_SHORT).show(); } ObjetInputStream ois; FileInputSteam fis; try{ fis = this.openFileInput(FILE); ois = new ObjectInputStream(fis); array = (int[]) ois.readObject(); // Eseguiamo il cast i = ois.readInt(); ois.close(); }catch (Exception e) { e.printStackTrace(); Toast.makeText(this, e.toString(), Toast.LENGTH_SHORT).show(); } } } Così facendo i valori delle variabili locali tmp0 e tmp1 vengono serializzati nel file File1.dat per poi essere deserializzate e assegnate alle variabili array e i, abbiamo deserializzato prima con readObject e poi con readInt, in quanto sono stati serializzati in quest’ordine, per semplificare le cose può essere utilizzato un solo Array o 157 ArrayList come unica variabile, è inoltre possibile serializzare istanze di una classe personalizzata, purché essa implementi l’interfaccia Serializable, in questo modo potremo gestire con più facilità i nosti dati, vediamo un esempio: Classe MainActivity: package com.example.test; import import import import import java.io.FileInputStream; java.io.FileOutputStream; java.io.ObjectInputStream; java.io.ObjectOutputStream; java.util.ArrayList; import import import import import import import import import import import import import import android.content.Context; android.os.Bundle; android.support.v4.app.Fragment; android.support.v7.app.ActionBarActivity; android.text.InputType; android.view.LayoutInflater; android.view.View; android.view.View.OnClickListener; android.view.ViewGroup; android.widget.Button; android.widget.CheckBox; android.widget.EditText; android.widget.TextView; android.widget.Toast; public class MainActivity extends ActionBarActivity { private private private private static static static static EditText nome, cognome, punteggio, telefono; Button salva, b1, b2, elimina; TextView t1; CheckBox cb; private private private private static static static static ArrayList<Utenti> al; int index = 0; final String FILE = "File1.dat"; Context ctx; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ctx = this; if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .add(R.id.container, new PlaceholderFragment()).commit(); } } public static class PlaceholderFragment extends Fragment { public PlaceholderFragment() { 158 } @SuppressWarnings("unchecked") @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main_activity3, container, false); nome = (EditText) rootView.findViewById(R.id.editText1); cognome = (EditText) rootView.findViewById(R.id.editText2); punteggio = (EditText) rootView.findViewById(R.id.editText3); telefono = (EditText) rootView.findViewById(R.id.editText4); salva = (Button) rootView.findViewById(R.id.button1); b1 = (Button) rootView.findViewById(R.id.button2); b2 = (Button) rootView.findViewById(R.id.button3); elimina = (Button) rootView.findViewById(R.id.button4); t1 = (TextView) rootView.findViewById(R.id.textView1); cb = (CheckBox) rootView.findViewById(R.id.checkBox1); final FileInputStream fis; punteggio.setInputType(InputType.TYPE_CLASS_NUMBER); telefono.setInputType(InputType.TYPE_CLASS_PHONE); try { fis = ctx.openFileInput(FILE); final ObjectInputStream ois = new ObjectInputStream(fis); al = (ArrayList<Utenti>) ois.readObject(); ois.close(); } catch (Exception e) { al = new ArrayList<Utenti>(); Toast.makeText(ctx, e.toString(), Toast.LENGTH_LONG).show(); e.printStackTrace(); } carica(); salva.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String s1, s2, t; s1 = nome.getText().toString(); s2 = cognome.getText().toString(); t = telefono.getText().toString(); int i; try { i = Integer.parseInt(punteggio.getText().toString()); } catch (NumberFormatException e) { e.printStackTrace(); i = 0; } boolean b = cb.isChecked(); al.add(new Utenti(s1, s2, t, i, b)); scrivi(); } }); 159 b1.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { index--; if (index < 0) index = al.size() - 1; carica(); } }); b2.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { index++; if (index >= al.size()) index = 0; carica(); } }); elimina.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { if (!al.isEmpty()) { al.remove(index); index = 0; carica(); } } }); return rootView; } private static void carica() { if (!al.isEmpty()) { Utenti u = al.get(index); t1.setText("n." + index + "\nNome: " + u.getNome() + "\nCognome: " + u.getCognome() + "\nPunteggio: " + u.getPunteggio() + "\nTelefono: " + u.getTelefono() + "\nPremium: " + u.isPremium()); } else { t1.setText("File Vuoto"); } } } } private static void scrivi() { FileOutputStream out; try { out = ctx.openFileOutput(FILE, Context.MODE_PRIVATE); final ObjectOutputStream oos = new ObjectOutputStream(out); oos.writeObject(al); oos.close(); } catch (Exception e) { e.printStackTrace(); Toast.makeText(ctx, e.toString(), Toast.LENGTH_LONG).show(); } } 160 Classe Utenti: package com.example.test; import java.io.Serializable; public class Utenti implements Serializable { private private private private static final long serialVersionUID = 1L; String nome, cognome, telefono; int punteggio; boolean premium; public Utenti(String nome, String cognome, String telefono, int punteggio, boolean premium){ this.nome = nome; this.cognome = cognome; this.telefono = telefono; this.punteggio = punteggio; this.premium = premium; } public String getNome() { return nome; } public void setNome(String nome) { this.nome = nome; } public String getCognome() { return cognome; } public void setCognome(String cognome) { this.cognome = cognome; } public String getTelefono() { return telefono; } public void setTelefono(String telefono) { this.telefono = telefono; } public int getPunteggio() { return punteggio; } public void setPunteggio(int punteggio) { this.punteggio = punteggio; } public String isPremium() { if (premium) return "Si"; else return "No"; } 161 public void setPremium(boolean premium) { this.premium = premium; } } In questo esempio abbiamo creato il file File1.dat, in cui abbiamo inserito un ArrayList<Utenti> in cui possiamo inserire le istanze della classe Utenti, per cui ci basterà recuperare semplicemente l’ArrayList, e lavorare quindi con esso, invece di recuperare sequenzialmente ogni dato, questa applicazione quindi si comporta in maniera molto simile ad un database sql, salvo che per l’uso del linguaggio sql per l’interrogazione, infatti è come se avessimo una tabella Utenti, con i campi nome, cognome, telefono, punteggio, premium il tutto però contenuto in un ArrayList e non in un database sql, possiamo anche aggiornare un campo utilizzando il metodo set(index, value); oppure tramite i setters della classe Utenti, abbiamo inoltre lavorato con il fragment, e con i Button, abbiamo dovuto quindi passarli il Context tramite una variabile, per il nome del file abbiamo utilizzato una costante, questo ci protegge da possibili errori di distrazione garantendo che il file in lettura sia lo stesso in scrittura, possiamo invece utilizzare una variabile, se abbiamo intenzione di creare e leggere più file diversi, ad esempio: private int n_file = 0; private String file = "File" + n_file + ".ext"; // File0.ext In alcuni casi potremmo non voler serializzare alcune variabili della classe, che dovranno quindi essere inserite manualmente ad ogni sessione, come ad esempio una password, per fare questo ci basterà utilizzare il modificatore transient esso non renderà la variabile non serializzabile, per cui ad esempio tornando alla classe Utenti, potremmo rendere il punteggio transient, in quanto deve essere resettato ad ogni sessione, quindi: package com.example.test; import java.io.Serializable; public class Utenti implements Serializable { private private private private static final long serialVersionUID = 1L; String nome, cognome, telefono; transient int punteggio; boolean premium; public Utenti(String nome, String cognome, String telefono, int punteggio, boolean premium){ this.nome = nome; this.cognome = cognome; this.telefono = telefono; this.punteggio = punteggio; this.premium = premium; } … 162 In questo modo aggiungendo all’ArrayList: al.add("Mario", "Rossi", "555-1234", 1128, true); Recuperando l’istanza dall’array list della sessione, otterremmo: … Utenti u0 = al.get(0); String nome, cognome, numero, premium; int punteggio; nome = u0.getNome(); // Mario cognome = u0.getCognome(); // Rossi numero = u0.getTelefono(); // 555-1234 punteggio = u0.getPunteggio(); // 1128 premium = u0.isPremium(); // Si … Serializzando invece l’ArrayList, e quindi caricandolo dal file, otterremmo invece: … Utenti u0 = al.get(0); String nome, cognome, numero, premium; int punteggio; nome = u0.getNome(); // Mario cognome = u0.getCognome(); // Rossi numero = u0.getTelefono(); // 555-1234 punteggio = u0.getPunteggio(); // 0 premium = u0.isPremium(); // Si … Il campo punteggio non è stato serializzato in quanto transient, e quindi resettato a 0. Oltre ai campi transient, anche i campi static non vengono serializzati in quanto non legati all’istanza, transient ci permette però a differenza di static di non serializzare un campo legato all’istanza della classe. 4.2 I database SQLite Un metodo alternativo alla serializzazione è l’utilizzo di un database SQLite, grazie ad esso potremmo gestire i nostri dati in tabelle utilizzando sia i metodi offerti dalla classe SQLiteDatabase, sia utilizzando il linguaggio SQL, sempre tramite la classe SQLiteDatabase, per prima cosa dobbiamo creare un database, e per farlo creeremo un Helper, ovvero una classe che estenda la classe astratta SQLiteOpenHelper, che ci permetterà di gestire il nostro database, vediamo ora come creare un Helper: package com.example.test; 163 import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; public class DbHelper extends SQLiteOpenHelper { public static final String DB_NAME = "Test", TABLE1_NAME = "Table1"; public static final String[] TABLE1_COL = { "_id", "Testo", "Numero" }; private static final int DB_VERSION = 1; public DbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("create table Table1 (_id integer primary key autoincrement, Testo text not null, Numero integer not null);"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS Table1"); onCreate(db); } } Prima di tutto creiamo delle costanti che ci aiuteranno a districarci nel database (anche se opzionali) in questo caso il nome del database, il nome della tabella (possono essere anche più di una) un array con i nomi delle colonne della tabella e la versione del database, creiamo poi il costruttore, che iniszializzi il costruttore della classe Madre, dovremmo inoltre includere un Context nel nostro costruttore, in quanto necessario al costruttore della SuperClasse. Il costruttore di SQLiteOpenHelper prevede un Context, il nome del Database (String), un opzionale CursorFactory, che può essere lasciato null e l’attuale versione del database, se la versione del nostro database è diversa da quella dello stesso database, ad esempio creato da una vecchia versione dell’applicazione, verrà eseguito il metodo onUpgrade, essendo SQLiteOpenHelper una classe astratta, dovremmo offrire obbligatoriamente un implementazione dei suoi metodi astratti, ovvero onCreate e onUpgrade. Il metodo onCreate verrà esegito alla creazione del database, mentre onUpgrade, come detto in precedenza, verrà eseguito quando la versione attuale del database differirà da quella precedentemente in memoria. Nel metodo on create, possiamo creare le tabelle del nostro database, ciò può essere fatto anche esternamente, ma è comunque consigliabile creare le tabelle alla 164 creazione del database tramite l’helper, in questo caso abbiamo creato la tabella Table1, con le colonne _id, Testo e Numero, per creare una tabella utilizzeremo il metodo execSQL(String sql) dove sql è una stringa contenente il codice in linguaggio SQL, nel nostro caso il codice relativo alla creazione di una tabella. Nel metodo onUpgrade invece eliminiamo la nostra tabella e la ricreiamo chiamando manualmente il metodo onCreate, per cui se aggiornando il database vengono modificate le colonne delle tabelle, cambiandone la versione, verrà ricreata la tabella, eliminando eventuali problemi di compatibilità, i dati contenuti in essa verranno però perduti, salvo specificare una querey di recupero dei dati nel metodo. Vediamo ora come utilizzare il database nella nostra applicazione: package com.example.test; import import import import import import import import import import import import import android.content.ContentValues; android.database.Cursor; android.database.sqlite.SQLiteDatabase; android.os.Bundle; android.support.v4.app.Fragment; android.support.v7.app.ActionBarActivity; android.view.LayoutInflater; android.view.View; android.view.View.OnClickListener; android.view.ViewGroup; android.widget.Button; android.widget.EditText; android.widget.TextView; public class SQLTest extends ActionBarActivity { private private private private private private private private static static static static static static static static EditText et1, et2; Button salva, carica, azzera; TextView tv; DbHelper dbh; SQLiteDatabase db; int[] id = { 0 }, i = { 0 }; String[] s = { "" }; final ContentValues CV = new ContentValues(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sqltest); dbh = new DbHelper(this); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .add(R.id.container, new PlaceholderFragment()).commit(); } } 165 public static class PlaceholderFragment extends Fragment { public PlaceholderFragment() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_sqltest, container, false); et1 = (EditText) rootView.findViewById(R.id.editText1); et2 = (EditText) rootView.findViewById(R.id.editText2); salva = (Button) rootView.findViewById(R.id.button1); carica = (Button) rootView.findViewById(R.id.button2); azzera = (Button) rootView.findViewById(R.id.button3); tv = (TextView) rootView.findViewById(R.id.textView2); salva.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { String t0 = et1.getText().toString(); int t1; try { t1 = Integer.parseInt(et2.getText().toString()); } catch (NumberFormatException e) { tmp = 0; e.printStackTrace(); } CV.put(DbHelper.TABLE1_COL[1], t0); CV.put(DbHelper.TABLE1_COL[2], t1); scrivi(); } }); carica.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { leggi(); String tmp = ""; int index = 0; while (index < id.length) { tmp += "Id = " + id[index] + " Txt = " + s[index] + " Int = " + i[index]; index++; if (index < id.length) tmp += "\n"; } tv.setText(tmp); } }); 166 azzera.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { db = dbh.getWritableDatabase(); db.delete(DbHelper.TABLE1_NAME, null, null); db.close(); } }); return rootView; } private void leggi() { db = dbh.getReadableDatabase(); Cursor cur = db.query(DbHelper.TABLE1_NAME, DbHelper.TABLE1_COL, null, null, null, null, null); id = new int[cur.getCount()]; i = new int[cur.getCount()]; s = new String[cur.getCount()]; while (cur.moveToNext()) { id[cur.getPosition()] = (cur.getInt(0)); s[cur.getPosition()] = (cur.getString(1)); i[cur.getPosition()] = (cur.getInt(2)); } dbh.close(); } private void scrivi() { db = dbh.getWritableDatabase(); db.insert(DbHelper.TABLE1_NAME, null, CV); CV.clear(); db.close(); } } } Per prima cosa abbiamo definito le variabili, tre cui il nostro DbHelper precedentemente creato, abbiamo inoltre inserito una variabile SQLiteDatabase, che ci permetterà di gestire il database, abbiamo inoltre definito una variabile di tipo ContentValues, che ci servirà per inserire i dati all’interno delle tabelle del database, in seguito abbiamo definito i metodi leggi() e scrivi(), che gestiscono rispettivamente la lettura e la scrittura del database. Cominciamo con il metodo leggi() Per prima cosa dobbiamo aprire il database in lettura, per farlo utilizzeremo il nostro DbHelper, ed il metodo getReadableDatabase() ereditato da SQLiteOpenHelper, che ci restituirà un oggetto di tipo SQLiteDatabase, che utilizzeremo per leggere i dati dal 167 database relativo al nostro helper, il secondo passo consiste nel navigare le tabelle del database, per fare ciò utilizzeremo il metodo: query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy); Dove: table – Indica il nome della tabella da cui leggere, è l’unico campo che non può essere null. columns – Un array contenente le colonne da leggere, se null verranno lette tutte le colonne, la documentazione ufficiale comunque scoraggia l’utilizzo di null, nel nostro esempio abbiamo inserito tutte le colonne. selection - Ci permette di filtrare i risultati in base al contenuto delle colonne, si utilizza la sintassi SQL della WHERE Clause, ad esempio: selection = "Numero=5 OR Numero=0"; In questo caso verranno mostrate solo le righe che hanno 5 o 0 come valore della colonna Numero. selectionArgs – Parametro legato a selection, ci permette di definire i valori di selection tramite un array String, inserendo dei ? invece dei valori, che saranno poi sostituiti dal contenuto dell’array, ad esempio: selection = "Numero=? OR Numero=?"; selectionArgs = {"5","0"}; Ciò funziona in maniera analoga all’esempio di selection, con la differenza però di poter gestire gli argomenti tramite un array, quindi in maniera varibile, se selection è null, oppure non contiene ? va impostato su null. groupBy – Serve a raggruppare campi con gli stessi valori in un unico campo, ad esempio: groupBy = "Testo"; Ragrupperà tutte le righe con lo stesso valore nella colonna Testo. having – Parametro legato a groupBy, permette di specificare delle funzioni SQL per gestire i gruppi, ad esempio: having = "count(Testo) < 2"; 168 Mostrerà solamente le righe non ripetute, ossia presenti meno di due volte, se groupBy è null, deve esserlo anche having. orderBy – Serve ad ordinare le righe in base al valore di un campo in modo crescente ASC o decrescente DESC, ad esempio: orderBy = "Numero ASC"; In questo caso ordinerà la le righe in base al valore della colonna Numero in modo crescente, se null le righe non verranno riordinate. Tale metodo ci restituirà un ogetto di tipo Cursor, che potremo utilizzare per navigare nella tabella, per spostare il Cursor, possiamo utilizzare i seguenti metodi boolean: moveToFirst(); // Sposta il cursore alla prima riga moveToPrevious(); // Sposta il cursore alla riga precedente moveToPosition(int position); // Sposta il cursore in una riga specifica moveToNext(); // Sposta il cursore alla riga successiva moveToLast(); // Sposta il cursore all’ultima riga Essi restituiranno true, se lo spostamento è avvenuto con successo, altrimenti restituiranno false, perciò: while (cur.moveToNext()) { … Scorrerà tutta la tabella, in quanto una volta raggiunta l’ultima posizione, il successivo moveToNext() restituirà false. Abbiamo quindi definito gli array di grandezza pari al numero di righe restituite dalla query, grazie al metodo getCount() che ci restituisce il numero di righe del Cursor. Altro passo fondamentale è quello di estrapolare i dati dalla tabelle, per farlo, possiamo utilizzare i metodi appropriati della classe Cursor, come ad esempio getInt(int columnIndex), getString(int columnIndex) oppure getDouble(int columnIndex) Dove columnIndex è il numero della colonna a partire da 0, abbiamo poi inserito i dati nella ralativa posizione dell’array tramite il metodo getPosition() che ci restituisce la posizione attuale del Cursor. In alternativa al metodo query, possiamo utilizzare il metodo rawQuery(String sql, String[] selectionArgs) dove sql è il codice sql della query e selectionArgs ha la stessa funzione che ha in query, ossia sostituire i ? di sql con il contenuto dell’array, rawQuery ci permette quindi di utilizzare il linguaggio sql, ci può quindi tornare utile per operazioni più complesse, oppure per chi è già abituato ad utilizzare il linguaggio sql, infine chiudiamo il database con il metodo close() di SQLiteDatabase. 169 Passiamo ora al metodo scrivi() Proprio come avviene per la lettura, dovremmo anche qui aprire il database, ma questa volta in scrittura, quindi tramite il nostro DbHelper richiamiamo il metodo getWritableDatabase() che ci restituirà sempre un SQLiteDatabase, che però ora potremo utilizzare per iserire dati nel database, per farlo utilizzeremo il precedentemente citato ContentValues, si tratta infatti di una classe simile ad un Bundle o ArrayList, ma che accetta solamente valori String, che possono essere però convertiti dalla classe stessa in formati numerici o boolean, ci servirà per inserire i dati nella tabella, per fare questo dovremmo utilizzare il metodo put(String key, String value) dove key è il nome della colonna in cui inserire i dati, mentre value è il valore da iserire nella colonna, ad esempio: CV.put("Testo", "Prova"); CV.put("Numero", "5"); Una volta inseriti i dati della riga nel ContentValues, possiamo procedere alla loro scrittura tramite il nostro metodo scrivi, dopo aver aperto il database in scrittura quindi possiamo utilizzare il metodo insert(String table, String nullColumnHack, ContentValues values) dove table è il nome della tabella, nullColumnHack, è un parametro opzionale e definisce quali colonno non definite notNull devono essere definite null se non previste in values, può essere tranquillamente impostato su null, mentre values sarà il nostro ContentValues, dopo aver inserito i dati nella tabella, puliamo il ContentValues tramite il metodo clear() e chiudiamo il database tramite il metodo close(). Abbiamo poi inserito anche un Button azzera, esso ci permetterà di svuotare la nostra tabella tramite il metodo delete(String table, String whereClause, String[] whereArgs) di SQLiteDatabase, dove table è il nome della tabella da cui dobbiamo eliminare le righe, whereClause ci permette di eliminare le righe in base al contenuto, mentre se lasciato null eliminerà tutte le righe della tabella (ma non la tabella), whereArgs ha la stessa funzione che negli altri metodi, una volta eliminate le righe chiudiamo il database con close(), per quanto riguarda la colonna _id essa è definita primary key autoincrement, quindi il suo valore aumenterà ad ogni nuova riga aggiunta alla tabella, eliminare righe non ridurrà il valore di tale contatore, per cui se l’ultima riga aveva id 4, anche eliminando tutte le righe, la prossima riga avrà comunque id 5, per eliminare le tabelle invece possiamo utilizzare il metodo execSQL come abbiamo fatto nel metodo onUpgrade, ad esempio: db.execSQL("DROP TABLE IF EXISTS Table1"); 170 In questo modo è anche possibile eliminare un intero database sostituiendo DROP TABLE con DROP DATABASE, l’utilizza di un database SQLite può essere più complicato rispetto all’utilizzo della serializzazione, ma offre la compatibilità con il linguaggio SQL, ed è comunque un ottimo metodo per gestire i dati. 4.3 La fotocamera Moltissimi dispositivi Android, sono dotati di fotocamera, è possibile far utilizzare tale fotocamera alla nostra applicazione, per farlo però dovremmo definire i permessi relativi nell’AndroidManifest.xml, i permessi necessari sono: android.permission.CAMERA // Per utilizzare la fotocamera android.permission.FLASHLIGHT // Per utilizzare il Flash android.permission.WRITE_EXTERNAL_STORAGE // Per salvare le fotografie sulla scheda SD Dovremmo inoltre utilizzare un particolare widget, ovvero il SurfaceView, che possiamo trovare in Advanced, esso ci servirà per visualizzare ciò che sta riprendendo la fotocamera, esso occuperà lo sfondo dell’applicazione, possiamo poi inserire degli elementi, quali pulsanti su di esso, per utilizzare la fotocamera, dovremmo affidarci ad alcune classi e interfaccie ossia: Camera // La classe che gestisce la fotocamera Parameters // Classe interna a Camera, serve per modificare le opzioni della fotocamera SurfaceHolder // Interfaccia che serve ad assegnare un Callback alla SurfaceView Callback // Interfaccia che ci permette di gestire la SurfaceView Vediamo ora come collegare la fotocamera alla SurfaceView: … private static Camera cam; private static SurfaceView sv; private static SurfaceHolder sh; … sv = (SurfaceView) findViewById(R.id.camera_surface); sh = sv.getHolder(); Callback cb = new Callback(){ @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { try { cam.setPreviewDisplay(holder); cam.startPreview(); } catch (IOException e) { e.printStackTrace(); } } @Override public void surfaceCreated(SurfaceHolder holder) { cam = Camera.open(); } 171 @Override public void surfaceDestroyed(SurfaceHolder holder) { cam.release(); } }; sh.addCallback(cb); … In alternativa è anche possibile implementare Callback nella nostra classe e dare this come argomento di addCallback, ad esempio: … public class MainActivity extends ActionBarActivity implements Callback { private static Camera cam; private static SurfaceView sv; private static SurfaceHolder sh; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); setContentView(R.layout.activity_main); sv = (SurfaceView) findViewById(R.id.camera_surface); sh = sv.getHolder(); sh.addCallback(this); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { try { cam.setPreviewDisplay(holder); cam.startPreview(); } catch (IOException e) { e.printStackTrace(); } } @Override public void surfaceCreated(SurfaceHolder holder) { cam = Camera.open(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { cam.release(); } } Il funzionamento è analogo all’esempio precedente, abbiamo prima di tutto creato un oggetto SurfaceView con il metodo findViewById, poi da esso abbiamo ottenuto il relativo SurfaceHolder, a cui abbiamo assegnato il nostro Callback tramite this, in quanto la nostra classe implementa Callback, oppure come nell’esempio precedente tramite una variabile dedicata, l’interfaccia Callback ha tre metodi astratti: 172 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height); public void surfaceCreated(SurfaceHolder holder); public void surfaceDestroyed(SurfaceHolder holder); surfaceChanged, viene chiamato quando la SurfaceView viene modificata e viene comunque chiamato sempre dopo surfaceCreated. surfaceCreated, viene chiamato quando la SurfaceView viene creata. surfaceDestroyed, viene chiamato quando la SurfaceView viene distrutta. Tutti e tre i metodo hanno come parametro il SurfaceHolder relativo. Per prima cosa dobbiamo aprire la fotocamera, ed è ciò che facciamo nel metodo surfaceCreated con: cam = Camera.open(), successivamente dobbiamo visualizzare ciò che sta visualizzando la nostra fotocamera sulla nostra SurfaceView, per farlo dobbiamo avviare l’anteprima, per prima cosa dobbiamo definire il SurfaceView su cui visualizzare l’anteprima, per farlo urilizzeremo il metodo setPreviewDisplay(SurfaceHolder holder) della classe Camera dove holder è il surface holder relativo alla nostra SurfaceView, dopo aver definito la SurfaceView da utilizzare, avviamo l’anteprima, è importante impostare l’activity in Landscape, in modo da visualizzare correttamente l’anteprima della fotocamera, per farlo utilizzeremo il metodo: Tale metodo va inserito necessariamente tra i metodi super.onCreate(savedInstanceState) e setContentView(R.layout.activity_main) del metodo onCreate, ciò renderà l’applicazione Landscape, ossia verà utilizzata con il dispositivo posto in orizzontale e non in verticale come le applicazioni Portrait, nel layout relativo alla nostra activity, possiamo passare anche alla vista Landscape, cliccando su: ciò ci permetterà di gestire il layout Landscape, dobbiamo infine inplementare il metodo onDestroy, che utilizzeremo per disattivare la fotocamera con il metodo release() della classe Camera. setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) Ora come ora abbiamo solamente l’anteprima della fotocamera, ma non possiamo ancora scattare delle foto, per farlo utilizzeremo il metodo: takePicture (ShutterCallback PictureCallback jpeg) shutter, PictureCallback raw, PictureCallback postview, Dove shutter è uno ShutterCallback, ovvero un particolare Callback che viene richiamato al momento dello scatto, raw è invece un PictureCallback, e viene richiamato subito dopo shutter, ci fornisce l’immagine grezza, esso non è supportato 173 da tutti i dispositivi, postview viene chiamato dopo raw, e ci permette di gestire l’anteprima della foto appena scattata anche esso non è supportato da tutti i dispositivi, infine abbiamo jpeg, che invece ci permette di gestire la foto in formato compresso, ovvero la foto che andremo a salvare, non tutti i Callback sono obbligatori e possono essere quindi null, analizziamo ora le due tipologie di Callback: ShutterCallback shutter = new ShutterCallback(){ @Override public void onShutter() { } }; ShutterCallback è un interfaccia con il metodo void onShutter, tale metodo verrà richiamato da takePicture se shutter != null. PictureCallback picture = new PictureCallback(){ @Override public void onPictureTaken(byte[] data, Camera camera) { } }; PictureCallback è un interfaccia con il metodo void onPictureTaken, a differenza di onShutter, esso ha due parametri data e camera, dove data è un array byte contenente il file relativo al al parametro di takePicture, nel caso di raw ad esempio è il file raw, mentre nel caso di jpeg è il file jpeg, tale variabile può poi essere serializzata, nel caso di jpeg ad esempio in un immagine .jpg, camera invece è l’oggetto camera tramite cui abbiamo chiamato il metodo takePicture, ognuno dei tre parametri PictureCallback di takePicture può chiamare un differente PictureCallback o essere null, vediamo ora come serializzare la nostra foto sulla nostra memoria esterna: … PictureCallback jpeg = new PictureCallback(){ @Override public void onPictureTaken(byte[] data, Camera camera) { File dir = new File(Environment.getExternalStoragePublicDirectory (Environment.DIRECTORY_PICTURES), "Fotografie"); dir.mkdirs(); SimpleDateFormat sdt = new SimpleDateFormat("yyMMddHHmmss"); String filename = sdt.format(new Date()) + ".jpg"; File file = new File(dir, filename); try { OutputStream os = new FileOutputStream(file); os.write(data); os.close(); Toast.makeText(getActivity(), filename + " Salvato in " + dir, Toast.LENGTH_SHORT).show(); } catch (Exception e) { Toast.makeText(getActivity(), e.toString(), Toast.LENGTH_SHORT).show(); } }; 174 Prima di tutto abbiamo definito il percorso in cui salvare il nostro file, ossia la cartella Fotografie all’interno della cartella predefinita di Android per le immagini, tale cartella è considerata un File anche essa, quindi creiamo un aggetto di tipo File, successivamente creiamo la cartella se non esitente tramite il metodo mkdir(), oltre al percorso, avremo anche bisogno di un nome per il nostro file, per evitare che la nostra applicazione sovrascriva in continuazione il nostro file, dobbiamo fare in modo che ogni file abbia un nome differente, quindi adoperiamo un espediente utilizzato in moltissime fotocamere, ed anche dalla stessa applicazione fotocamera di Adroid, ossia assegnare un nome basato su data ed ora dello scatto, per farlo utilizzeremo la classe Date, ma prima di poterla utilizzare, dovremmo formattare tale data in modo che possa essere utilizzata come nome file valido, per fare questo dovremmo utilizzare la classe SimpleDateFormat, inizializzandola con una stringa che rappresenterà la maschera della nostra data, nell’esempio abbiamo utilizzato: SimpleDateFormat sdt = new SimpleDateFormat("yyMMddHHmmss"); Dove yy indica le ultime due cifre dell’anno attuale ad esempio 14 per il 2014, MM indica invece il mese indicato con due cifre, ad esempio 03 per Marzo, dd indica invece il giorno del mese con due cifre, ad esempio 05 per il giorno 5, HH indica l’ora in formato 0-23, indicata con due cifre, quindi 04 per le 4AM, 16 per le 4PM, mm indica invece i minuti indacati sempre da due cifre quindi 09 per 9 minuti, ss infine indica i secondi, sempre rappresentati da due cifre quindi ad esempio 02 per 2 secondi, i valori sono case sensitive, utilizzando una sola lettera invece che due porterà a non forzare l’utilizzo di due cifre, quindi inserendo H invece di HH avremmo 4 invece che 04 per le 4AM e 16 per le 4PM, dopo aver creato la maschera, creiamo il nosro nome file tramite il metodo format(Date date) di SimpleDateFormat, nel seguente modo: String filename = sdt.format(new Date()) + ".jpg"; Qesto ci permetterà di formattare data ed ora attuali con la maschera da noi precedentemente creata, aggiungiamo poi l’estensione .jpg al nome del nostro file, una volta definiti nome e percorso del file, possiamo definire il File stesso in questo modo: File file = new File(dir, filename); Ora che abbiamo sia il File da scrivere, che i contenuti da inserire in esso, possiamo passare alla serializzazione di tale File nel seguente modo: try { OutputStream os = new FileOutputStream(file); os.write(data); os.close(); Toast.makeText(getActivity(), filename + " Salvato in " + dir, Toast.LENGTH_SHORT).show(); } catch (Exception e) { Toast.makeText(getActivity(), e.toString(), Toast.LENGTH_SHORT).show(); } 175 A differenza della serializzazione untilizzata in precedenza essa non scrive il file su di una cartella private dell’applicazione, ma su di una cartella pubblica nella memoria esterna, sono quindi necessari anche i relativi permessi nel AndroidManifest.xml, per il resto si tratta di una normale serializzazione, utilizziamo quindi il metodo write, che ci permette di serializzare un buffer di dati contenuto in un array byte, ossia la nostra fotografia, contenuta nella array byte data, chiudiamo il file e avvisiamo l’utente tramite un Toast del corretto salvataggio della fotografia, oppure del fatto che è stata sollevata un Eccezione. Possiamo anche modificare i parametri della nostra fotocamera, tra cui l’utilizzo del flash oppure il valore di esposizione o di bilanciamento del bianco, per farlo ci servirà un oggetto di tipo Parameters, che potremmo ottenere dal nostro oggetto Camera tramite il metodo getParameters() in questo modo: Parameters params = cam.getParameters(); Con essa poi possiamo modificare i vari parametri della fotocamera grazie ai relativi metodi della classe Parameters, per cui ad esempio se vogliamo modificare il tipo di flash, oppure la qualità della foto, utilizzeremo: Parameters params = cam.getParameters(); // Otteniamo i parametri della fotocamera params.setFlashMode(Parameters.FLASH_MODE_ON); // Flash attivo params.setJpegQuality(95); // Qualità 95% cam.setParameters(params); // Rendiamo effettive le modifiche È possibile modificare anche altri parametri ovviamente, tramite i metodi della classe Parameters, ricordandoci che per l’utilizzo del flash è necessario il relativo permesso. 4.4 La videocamera Stiamo sempre parlando della fotocamera, ma questa volta vedremo come registrare un video, per farlo potremmo utilizzare lo scheletro della nostra applicazione fotocamera, in aggiunta alla classe MediaRecorder, che ci permetterà di registrare un video, con relativo audio, per registrare correttamente ci serviranno i seguenti permessi: android.permission.CAMERA; // Per registrare il video android.permission.RECORD_AUDIO; // Per registrare l’audio android.permission.WRITE_EXTERNAL_STORAGE; // Per salvare il video su memoria esterna Come per la fotocamera avremmo bisogno di una SurfaceView sulla quale visualizzare ciò che sta riprendendo la nostra camera, dovremmo inoltre inizializzare la nostra camera, come abbiamo fatto per l’applicazione della fotocamera, per poter iniziare la registrazione, dobbiamo prima di tutto definire un MediaRecorder, esso può essere istanziato normalmente nel seguente modo: 176 private static MediaRecorder mr = new MediaRecorder(); Per poter utilizzare correttamente la videocamere, dovremmo prima di iniziare a registrare disimpegnare la nostra videocamera, quindi per prima cosa interrompiamo l’anteprima, se già avviata, tramite il metodo stopPreview() di Camera, procediamo poi allo sblocco della fotocamera tramite il metodo unlock() sempre di Camera, se l’anteprima non era stata avviata, procediamo solamente allo sblocco, dopo aver sbloccato la camera, la assegnamo al nostro MediaRecorder, tramite il metodo setCamera(Camera arg0) dove arg0 è appunto il nostro oggetto Camera, dobbiamo poi definire le sorgenti audio e video, per farlo utilizzeremo i metodi setAudioSource(int arg0) e setVideoSource(int arg0) dove arg0 rappresenta la sorgente, ad esempio: mr.setAudioSource(AudioSource.CAMCORDER); mr.setVideoSource(VideoSource.CAMERA); Passiamo poi all’assegnazione del profilo, che determinerà la qualità del file registrato, per farlo utilizzeremo il metodo setProfile(CamcorderProfile profile) della classe MediaRecorder, dove profile è il profilo che andremo ad utilizzare e che può essere ottenuto tramite il metodo get(int quality) di CamcorderProfile, dove quality può essere ottenuto da una costante di CamcorderProfile, ad esempio: mr.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH)); Definiamo ora il file da memorizzare, per farlo utilizzeremo il metodo setOutputFile(String path) dove path è il file da selvare, può essere definito manualmente, oppure essere recuperato da un oggetto File tramite il metodo getAbsolutePath() di File, per generare il nome del video possiamo utilizzare lo stesso metodo utilizzato per la fotocamera, esempio: SimpleDateFormat sdt = new SimpleDateFormat("yyMMddHHmmss"); File dir = new File(Environment.getExternalStoragePublicDirectory (Environment.DIRECTORY_MOVIES),"Video"); dir.mkdirs(); String filename = sdt.format(new Date()) + ".mp4"; File file = new File(dir, filename); mr.setOutputFile(file.getAbsolutePath()); A differenza della fotocamera, qui non dovremmo serializzare manualmente il File, ma solo definirne il percorso, dopo aver definito profilo e percorso, possiamo procedere alla registrazione, per farlo dobbiamo reimpostare l’anteprima sulla nostra SurfaceView, questa volta però tramite il nostro MediaRecorder, e non attravarso la nostra Camera, quindi utilizziamo il metodo setPreviewDisplay(Surface sv) della classe MediaRecorder, che a differenza dell’omonimo metodo della classe Camera, ha come parametro un Surface, e non un SurfaceHolder, possiamo però ottenere il Surface 177 necessario dal nostro SurfaceHolder tramite il metodo ad esempio: getSurface() di SurfaceHolder, mr.setPreviewDisplay(sh.getSurface()); Dopo aver riavviato l’anteprima, possiamo iniziare la registrazione tramite i metodi prepare() e start() di MediaRecorder, va prima chiamato prepare() per cui dovremmo prevedere una IOException in quanto si tratta di una Eccezione checked, in seguito va chiamato invece start() ad esempio: try { mr.prepare(); mr.start(); } catch (IOException e) { e.printStackTrace(); } A questo punto la nosta applicazione sta registrando, per interropere la registrazione utilizziamo il metodo stop() di MediaRecorder, a questo punto possiamo avviare nuovamente la registrazione oppure terminarla definitivamente chiudendo il MediaRecorder tramite il metodo release() una volta invocato release() per registrare un nuovo video bisognera instanzaiare nuovamente il MediaRecorder e ridefinirne quindi tutti i parametri come se fosse la prima registrazione in quanto dopo aver invocato release() l’istanza di MediaRecorder che ha invocato tale metodo smette di essere valida, per cui per poter registrare un nuovo metodo dovremmo rieseguire tutti i metodi necessari, ad esempio: … mr = new MediaRecorder(); cam.stopPreview(); // Solo se è già stata avviata l’anteprima cam.unlock(); // Solo se la Camera è stata ribloccata mr.setCamera(cam); mr.setAudioSource(AudioSource.CAMCORDER); mr.setVideoSource(VideoSource.CAMERA); mr.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH)); … Dopo aver chiuso il MediaRecorder, possiamo procedure a ribloccare la Camera tramite il metodo lock() di Camera e a riavviare l’anteprima nel seguente modo: mr.stop(); mr.release(); cam.lock(); try { cam.setPreviewDisplay(sh); cam.startPreview(); } catch (IOException e) { e.printStackTrace(); } Il video registrato sarà salvato nel percorso definito tramite setOutputFile. 178 Vediamo ora un esempio di applicazione Fotocamera/Videocamera: package com.example.videocam; import import import import import import java.io.File; java.io.FileOutputStream; java.io.IOException; java.io.OutputStream; java.text.SimpleDateFormat; java.util.Date; import import import import import import import import import import import import import import import import import import import import import import import import import android.content.pm.ActivityInfo; android.graphics.PixelFormat; android.hardware.Camera; android.hardware.Camera.Parameters; android.hardware.Camera.PictureCallback; android.media.CamcorderProfile; android.media.MediaRecorder; android.media.MediaRecorder.AudioSource; android.media.MediaRecorder.VideoSource; android.os.Bundle; android.os.Environment; android.support.v4.app.Fragment; android.support.v7.app.ActionBarActivity; android.view.LayoutInflater; android.view.SurfaceHolder; android.view.SurfaceHolder.Callback; android.view.SurfaceView; android.view.View; android.view.View.OnClickListener; android.view.ViewGroup; android.view.Window; android.view.WindowManager; android.widget.Button; android.widget.ImageView; android.widget.Toast; public class MainActivity extends ActionBarActivity { private private private private private private private private private private private static static static static static static static static static static static final int FULLSCREEN = WindowManager.LayoutParams.FLAG_FULLSCREEN; Button flash; ImageView shoot, video, logo; SurfaceView sv; SurfaceHolder sh; Camera cam; Parameters params; boolean recording = false; MediaRecorder mr; int f_mode = 0; PictureCallback p_call; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().setFormat(PixelFormat.TRANSLUCENT); // Abilitiamo la trasparenza requestWindowFeature(Window.FEATURE_NO_TITLE); // Nascondiamo l’action bar getWindow().setFlags(FULLSCREEN, FULLSCREEN); // Rendiamo l’app Fullscreen setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); // Landscape 179 setContentView(R.layout.activity_main); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .add(R.id.container, new PlaceholderFragment()).commit(); } } public static class PlaceholderFragment extends Fragment implements Callback, PictureCallback { public PlaceholderFragment() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main, container, false); shoot = (ImageView) rootView.findViewById(R.id.imageView1); video = (ImageView) rootView.findViewById(R.id.imageView2); logo = (ImageView) rootView.findViewById(R.id.imageView3); flash = (Button) rootView.findViewById(R.id.Flash); sv = (SurfaceView) rootView.findViewById(R.id.camera_surface); sh = sv.getHolder(); sh.addCallback(this); p_call = this; flash.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { f_mode++; String s1, s2; if (f_mode == 1) { s1 = Parameters.FLASH_MODE_OFF; s2 = "Off"; } else if (f_mode == 2) { s1 = Parameters.FLASH_MODE_ON; s2 = "On"; } else if (f_mode == 3) { s1 = Parameters.FLASH_MODE_RED_EYE; s2 = "O.Rossi"; } else { f_mode = 0; s1 = Parameters.FLASH_MODE_AUTO; s2 = "Auto"; } params.setFlashMode(s1); // Impostiamo il flash cam.setParameters(params); flash.setText(s2); } }); shoot.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { cam.takePicture(null, null, null, p_call); cam.startPreview(); // Riavviamo l’anteprima interrotta da takePicture } 180 }); sv.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { cam.autoFocus(null); // Avviamo l’autoFocus } }); video.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { flash.setEnabled(recording); shoot.setEnabled(recording); if (!recording) { recording = true; mr = new MediaRecorder(); logo.setVisibility(View.VISIBLE); cam.stopPreview(); cam.unlock(); mr.setCamera(cam); mr.setAudioSource(AudioSource.CAMCORDER); mr.setVideoSource(VideoSource.CAMERA); mr.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH)); SimpleDateFormat sdt = new SimpleDateFormat("yyMMddHHmmss"); File dir = new File(Environment.getExternalStoragePublicDirectory (Environment.DIRECTORY_MOVIES), "Video"); dir.mkdirs(); String filename = sdt.format(new Date()) + ".mp4"; File file = new File(dir, filename); mr.setOutputFile(file.getAbsolutePath()); mr.setPreviewDisplay(sh.getSurface()); try { mr.prepare(); mr.start(); // Inizia la registrazione } catch (IOException e) { e.printStackTrace(); } } else { recording = false; logo.setVisibility(View.INVISIBLE); mr.stop(); // Termina la registrazione mr.release(); cam.lock(); // Riblocchiamo la Camera try { cam.setPreviewDisplay(sh); cam.startPreview(); } catch (IOException e) { } } } }); return rootView; 181 } @Override public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){ try { cam.setPreviewDisplay(holder); cam.startPreview(); } catch (IOException e) { e.printStackTrace(); } } @Override public void surfaceCreated(SurfaceHolder holder) { cam = Camera.open(); params = cam.getParameters(); // Otteniamo i parametri della Camera params.setJpegQuality(95); // Impostamo la qualità params.setFlashMode(Parameters.FLASH_MODE_AUTO); // Impostiamo il flash cam.setParameters(params); // Applichiamo i parametri } @Override public void surfaceDestroyed(SurfaceHolder holder) { cam.release(); // Chiudiamo la Camera } @Override public void onPictureTaken(byte[] data, Camera camera) { SimpleDateFormat sdt = new SimpleDateFormat("yyMMddHHmmss"); File dir = new File(Environment.getExternalStoragePublicDirectory (Environment.DIRECTORY_PICTURES), "Fotografie"); dir.mkdirs(); String filename = sdt.format(new Date()) + ".jpg"; File file = new File(dir, filename); try { OutputStream os = new FileOutputStream(file); os.write(data); // Serializziamo la foto os.close(); Toast.makeText(getActivity(), filename + " Salvato in " + dir, Toast.LENGTH_SHORT).show(); } catch (Exception e) { Toast.makeText(getActivity(), e.toString(), Toast.LENGTH_SHORT).show(); } } } } 4.5 Registrare l’audio Grazie alla classe MediaRecorder, è anche possibilie registrare file sonori, farlo è molto più semplice che registrare un video, in quanto non dobbiamo interfacciarci con la videocamera o con la SurfaceView, per registrare l’audio utilizzeremo gli stessi metodi utilizzati per il video, meno quelli relativi al video, vediamo ora come iniziare una registrazione audio: 182 … private static MediaRecorder mr; … mr = new MediaRecorder(); mr.setAudioSource(AudioSource.CAMCORDER); mr.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mr.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); SimpleDateFormat sdt = new SimpleDateFormat("yyMMddHHmmss"); File dir = new File(Environment.getExternalStoragePublicDirectory (Environment.DIRECTORY_MUSIC),"AudioREC"); dir.mkdirs(); String filename = sdt.format(new Date()) + ".m4a"; File file = new File(dir, filename); mr.setOutputFile(file.getAbsolutePath()); try { mr.prepare(); mr.start(); } catch (IOException e) { e.printStackTrace(); } … Per terminarla ci basterà utilizzare gli stessi metodi che abbiamo utilizzato per la videocamera, meno quelli relativi alla Camera, quindi: mr.stop(); mr.release(); Con setAudioSource abbiamo definito la sorgente audio, in questo caso CAMCORDER, ovvero il microfono della videocamera, ma possiamo utilizzare anche il microfono del telefono, possiamo trovare tutte le sorgenti disponibili nella classe AudioSource. Con setOutputFormat abbiamo definito il container del file, il container deve essere compatibile con l’encoder audio, in questo caso abbiamo utilizzato il container MPEG_4 che ci consentirà di utilizzare le estensioni mp4 (Audio/Video) o m4a (Audio), il container AMR_NB ci permetterà di utilizzare l’estensione amr(audio), AMR_WB ci permettarà di utilizzare le estensioni amr(audio) e awb(audio), AAC ci permetterà di utilizzare l’estensione aac(audio), THREE_GPP infine ci permetterà di utilizzare le estensioni 3gp(audio/video) e 3gpp(audio/video) i formati disponibili sono nella classe OutputFormat. Con setAudioEncoder invece possiamo definire il codec audio del nostro file, tale codec deve essere compatibile con l’OutputFormat, quindi ad esempio AAC, HE_AAC e AAC_ELD sono compatibili con MPEG_4 e AAC, mentre AMR_NB e AMR_WB sono compatibili con i loro omonimi formati e THREE_GPP, AMR_NB è il formato più compatibile ed è compatibile anche con i dispositivi più vecchi (API < 10), ma è anche il formato che offre la minor qualità audio, HE_AAC offre la miglior 183 qualità, ma è il meno compatibile insiema ad AAC_ELD (Necessaria almeno API 16) AAC e AMR_WB sono invece compatibili dall’API 10. Il resto del codice è analogo a quello utilizzato per la videocamera, escludendo il codice relativo al video o alla Camera. 4.6 Riproduciamo file multimediali Abbiamo visto come registrare i nostri file audio/video, ma potremmo anche aver bisogno di utilizzare file multimediali nelle nostre applicazioni, ad esempio impostando una musica di sottofondo, oppure per visualizzare un video, per farlo potremmo utilizzare la classe MediaPlayer, che ci permetterà di gestire i nostri file multimediali, grazie ad essa possiamo utilizzare sia file interni all’applicazione, sia file esterni, purchè si abbiano i permessi relativi. Per prima cosa dobbiamo creare il nostro MediaPlayer, per farlo possiamo utilizzare il metodo static create di MediaPlayer, che ci restituirà un oggetto di tipo MediaPlayer, esistono vari overload di tale metodo, ma che portano tutte allo stesso risultato, abbiamo infatti: MediaPlayer.create(Context context, int resId); MediaPlayer.create(Context context, Uri uri); MediaPlayer.create(Context context, Uri uri, SurfaceHolder holder); Tutti e tre i metodi hanno come parametro un Context, il primo metodo ha come secondo parametro un int resId, tale metodo serve per utilizzare file audio interni al programma, il resId è infati l’id relative al file R.java e può essere utilizzato ad esempio per caricare una musica di sottofondo dalla cartella raw, ad esempio: MediaPlayer mp = MediaPlayer.create(this, R.raw.music1); Il secodo metodo ha invece come parametro un Uri uri, uri sta per Uniform Resource Identifier, ed indica il percorso del nostro file, ciò però non va confuso con il path di alcuni metodi, in quanto il metodo getPath() di File ci restituisce una String e non un Uri, non possiamo utilizzare neanche il metodo toURI di File, in quanto ci restituisce un URI e non un Uri si tratta infatti di due classi differenti, per ottenere l’Uri necesario da un File, possiamo utilizzare il metodo static fromFile(File file) di Uri, dove file è il nostro file da cui vogliamo ottenere l’Uri, quindi ad esempio: MediaPlayer mp = MediaPlayer.create(this, Uri.fromFile(music_file)); Tale metodo a differenza del primo ci servirà per utilizzare un file esterno al programma, il terzo metodo infine ci permette invece di utilizzare un file Video, 184 facendoci definire anche un SurfaceHolder su cui visualizzare il nostro video, come abbiamo già visto nella parte relative alla fotocamera e videocamera, ad esempio: SurfaceView sv = (SurfaceView) findViewById(R.id.surfaceView1); MediaPlayer mp = MediaPlayer.create(this, Uri.fromFile(video_file), sv.getHolder()); Tale metodo può essere utilizzato anche per il file audio, ma in questo caso non verrà visualizzato nulla nel SurfaceView, ciò ci può servire in caso uri, sia un file variabile, dopo aver creato il nostro MediaPlayer, possiamo avviarlo tramite il metodo start(), possiamo poi fermarlo temporaneamente con il metodo pause(), in questo modo la riproduzione si fermerà finche non verrà chiamato nuovamente il metodo start(), in tal caso la riproduzione ricomincierà dal punto in cui è stata fermata, per interrompere definitivamente la riproduzione utilizziama il metodo stop(), dopo aver chiamato tale metodo non potremmo più far ripartire la riproduzione con start(), dopo aver chiamato il metodo stop() possiamo ridefinire il MediaPlayer, oppure chiamare release(), nel qual caso l’istanza non sarà più valida e dovrà essere creata una nuova istanza ad esempio col metodo create, release inoltre libererà le risorse utilizzate dal MediaPlayer, e quindi andrebbe sempre chiamato nel momento in cui non si intende più utilizzare un determinato MediaPlayer, una volta terminata la riproduzione del file il MediaPlayer passerà allo stato paused, chiamando quindi nuovamente start() la riproduzione ricomincierà da capo, per impedire che la riproduzione si fermi alla fine del file, possiamo utilizzare il metodo setLooping(boolean looping) dove true farà ricominciare la riproduzione una volta terminato il file, mentre false farà fermare la riproduzione alla fine del file, possiamo ottenere lo stato del Looping tramite il metodo boolean isLooping(), di base il looping è false, per fermare la riproduzione e farla ricominciare da capo, possiamo utilizzare i metodi pause() e seekTo(int millis) dove millis è la posizione in millisecondi del file, essa è compresa tra 0 e getDuration(), per cui per fermare la riproduzione e farla ricominciare da capo possiamo fare così: mp.pause(); mp.seekTo(0); In questo modo il file tornerà alla posizione 0, ovvero l’inizio, è anche possibile impostare un nuovo MediaPlayer dopo la fine dell’attuale MediaPlayer grazie al metodo setNextMediaPlayer(MediaPlayer next) dove next è il MediaPlayer da accodare, è anche possibile impostare manualmente il file da utilizzare grazie al metodo setDataSource, che come il metodo create ha vari overload, grazie a tale metodo è possibile assegnare un file ad una nuova istanza di MediaPlayer, non creata dal metodo create, in questo caso andrà chiamato anche il metodo prepare(), è inoltre possibile impostare manualmente anche la SurfaceView da utilizzare grazie ai metodi 185 oppure setSurface(Surface surface), ciò ci può tornare utile anche per modificare la SurfaceView da utilizzare, il metodo setScreenOnWhilePlaying(boolean screenOn) può essere utilizzato per impedire che lo schermo si spenga durante la riproduzione del video, tale metodo non richiede permessi aggiuntivi per essere utilizzato, in alternativa abbiamo anche il metodo setWakeMode(Context context, int mode) più avanzato, ma necessita del permesso WAKE_LOCK per poter essere utilizzato, dove context è il Context da utilizzare, metre mode è il wake lock da impostare, i wake lock disponibili possono essere ottenuti dalla classe PowerManager, e sono i seguenti: setDisplay(SurfaceHolder sh) PowerManager.PARTIAL_WAKE_LOCK; // Mantiene la CPU attiva anche in Sleep, schermo e tastiera possono spegnersi. // Il tasto Power non annulla il wake lock. PowerManager.SCREEN_DIM_WAKE_LOCK; // Deprecato dall’API17 // Riduce la luminosità dello schermo invece di spegnerlo, la tastiera può spegnersi. // Il tasto Power annulla il wake lock. PowerManager.SCREEN_BRIGHT_WAKE_LOCK; // Deprecato dall’API13 // Impedisce allo schermo di spegnersi, la tastiera può spegnersi. // Il tasto Power annulla il wake lock. PowerManager.FULL_WAKE_LOCK; // Deprecato dall’API17 // Impedisce a schermo e tastiera di spegnersi. // Il tasto Power annulla il wake lock. Salvo la necessità di dover utilizzare un wake lock particolare quale il PARTIAL_WAKE_LOCK è consigliato utulizzare il metodo setScreenOnWhilePlaying (boolean screenOn) in quanto non richiede permessi aggiuntivi. Nel caso volessimo creare un lettore multimediale, potremmo aver la necessità di inserire una SeekBar che indichi la posizione corrente del file, e che ci permetta di spostarci nel file, per farlo possiamo utilizzare un thread che mantenga aggiornata la SeekBar, definendo sia il MediaPlayer che la SeekBar, possiamo fare in questo modo package com.example.player; public class Progress extends Thread { private boolean run = true; private boolean pause = false; public Progress (){ start(); } public void terminate(){ run = false; } public void setPause(boolean pause){ this.pause = pause; } 186 @Override public void run() { while(run){ try { if (!pause) MainActivity.sb.setProgress(MainActivity.mp.getCurrentPosition()); } catch (Exception e) { e.printStackTrace(); } } } } In questo modo la SeekBar si muoverà con l’avanzare della riproduzione, dove sb e mp sono rispettivamente la nostra SeekBar e il nostro MediaPlayer definiti static nella nosta MainActivity, vediamo ora come spostare la posizione della riproduzione, tramite la SeekBar: package com.example.player; import import import import import android.os.Bundle; android.support.v7.app.ActionBarActivity; android.media.MediaPlayer; android.widget.SeekBar; android.view.View.OnTouchListener; public class MainActivity extends ActionBarActivity { protected static MediaPlayer mp; protected static final SeekBar sb = findViewById(R.id.seekBar1); private static Progress pr; // Il nostro thread @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mp = MediaPlayer.create(this, R.raw.music_1); sb.setMax(mp.getDuration()); mp.setLooping(true); mp.start(); pr = new Progress(); // Avviamo il thread sb.setOnTouchListener(new OnTouchListener(){ @Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) pr.setPause(true); if (event.getAction() == MotionEvent.ACTION_UP){ mp.seekTo(sb.getProgress()); pr.setPause(false); } return false; } }); } } 187 In questo caso abbiamo utilizzato un OnTouchListener, e non un OnClickListener, come di solito, si tratta anche esso di un interfaccia, che a differenza di OnClickListener ha il metodo astratto onTouch(View v, MotionEvent event) invece del metodo astratto onClick(View v), in questo caso il MotionEvent ci servirà per definire comportamenti diversi all’inizio e alla fine del tocco grazie al metodo getAction() di MotionEvent, quindi fermiamo l’aggiornamento della SeekBar all’inizio del tocco MotionEvent.ACTION_DOWN spostiamo poi la posizione della riproduzione tramite mp.seekTo(sb.getProgress()) alla posizione desiderata alla fine del tocco MotionEvent.ACTION_UP infine facciamo ripartire l’aggiornamento della SeekBar, il tutto senza terminare il thread Progress, in quanto abbiamo definito il metodo setPause, purché tutto funzioni correttamente, dobbiamo ricordarci di definire la grandezza della SeekBar, in base alla durata del file, per farlo abbiamo utilizzato sb.setMax(mp.getDuration()) questo imposterà il range dei valori della SeekBar, cioè tra 0 e mp.getDuration(), ossia la durata in millisecondi del file. 4.7 Esploriamo la memoria esterna Abbiamo già visto come Serializzare un file, oppure come leggere un file in un percorso specifico, ma se invece volessimo leggere una cartella? Ciò è possibile grazie al metodo listFiles() di File, tale metodo ci restituirà un array File[] contenete tutti i File all’interno di tale cartella, quindi se volessimo ottenere tutti i File della cartella musica, potremmo fare in questo modo: … private static final File MUSIC = Environment.getExternalStoragePublicDirectory (Environment.DIRECTORY_MUSIC); private static final File[] FILES = MUSIC.listFiles(); … In questo modo abbiamo creato un array contenente tutti i files contenuti nella cartella Music sulla memoria esterna, bisognerà naturalmente anche definire i permessi necessary nell’AndroidManifest.xml, per cui se volessimo utilizzarli per un MediaPlayer, potremmo fare questa semplice applicazione: package com.example.simpleplayer; import java.io.File; import import import import import import import import import import import import android.media.MediaPlayer; android.os.Bundle; android.os.Environment; android.support.v4.app.Fragment; android.support.v7.app.ActionBarActivity; android.view.LayoutInflater; android.view.Menu; android.view.View; android.view.View.OnClickListener; android.view.ViewGroup; android.widget.Button; android.widget.TextView; 188 public class MainActivity extends ActionBarActivity { private static final File MUSIC = Environment.getExternalStoragePublicDirectory (Environment.DIRECTORY_MUSIC); private static final File[] FILES = MUSIC.listFiles(); private static final MediaPlayer MP = new MediaPlayer(); private static final boolean FLAG_PREV = false, FLAG_NEXT = true; private static final String[] SUPPORTED = { ".mp3", ".mp4", ".m4a", ".aac", ".amr", ".awb", ".MP3", ".MP4", ".M4A", ".AAC", ".AMR", ".AWB" }; private static Button prev, play, next; private static TextView name; private static int n_file = -1; private static int n_try = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .add(R.id.container, new PlaceholderFragment()).commit(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { return false; } public static class PlaceholderFragment extends Fragment { public PlaceholderFragment() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main,container,false); prev play next name = = = = (Button) rootView.findViewById(R.id.button1); (Button) rootView.findViewById(R.id.button2); (Button) rootView.findViewById(R.id.button3); (TextView) rootView.findViewById(R.id.textView1); try { MP.setDataSource(selectFile(FLAG_NEXT)); MP.prepare(); } catch (Exception e) { e.printStackTrace(); } prev.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { try { MP.reset(); MP.setDataSource(selectFile(FLAG_PREV)); MP.prepare(); 189 } catch (Exception e) { e.printStackTrace(); } } }); next.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { try { MP.reset(); MP.setDataSource(selectFile(FLAG_NEXT)); MP.prepare(); } catch (Exception e) { e.printStackTrace(); } } }); play.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { if (MP.isPlaying()) MP.pause(); else MP.start(); } }); return rootView; } private String selectFile(boolean flag) { n_try++; if (n_try > FILES.length){ name.setText("Nessun file valido trovato"); play.setEnabled(false); prev.setEnabled(false); next.setEnabled(false); return ""; // Se non ci sono file supportati nella cartella } if (flag == FLAG_PREV) { n_file--; if (n_file < 0) n_file = FILES.length - 1; } else { n_file++; if (n_file >= FILES.length) n_file = 0; } 190 boolean supported = false; String path = FILES[n_file].getAbsolutePath(); for (int x = 0; x < SUPPORTED.length; x++) { if (path.endsWith(SUPPORTED[x])) { supported = true; name.setText(FILES[n_file].getName()); break; } } if (!supported) // File non valido path = selectFile(flag); // Controlliamo il prossimo file n_try = 0; // File valido trovato return path; } } } Questo semplice lettore multimediale ci permette di riprodurre tutti i file contenuti nella cartella music della memoria di massa (NB. In alcuni telefoni la memoria di massa può essere comunque una memoria flash interna al dispositivo), se nella cartella non ci sono files FILES.lenght == 0 oppure una volta controllata tutta la cartella non sono stati trovati file validi n_try > FILES.lenght l’applicazone disabiliterà i pulsanti e ci avviserà che non è stato trovato alcun file valido, altrimenti si limiterà a saltare i file non validi, ossia che non abbiano un’estensione compresa nell’array SUPPORTED, una volta trovato un file valido, viene reimpostata la variabile n_try a 0 e resettato il MediaPlayer tramite MP.reset(), impostato il percorso restituito dal nostro metodo tramite il metodo tramite MP.setAudioSource(selectFile(flag)) dove flag è uno dei due flag da noi definiti, FLAG_PREV o FLAG_NEXT, una volta assegnato il file prepariamo il MediaPlayer, con MP.prepare(), in quanto inizializzato manualmente e non tramite il metodo create, in questo modo possiamo riutilizzare sempre la stessa instanza di MediaPlayer, e quindi definirla final, ciò viene eseguito alla creazione oppure alla pressione dei tasti prev e next, con i relativi flag, alla pressione del tasto play, verrà avviata la riproduzione se !MP.isPlaying() tramite MP.start() altrimenti verrà sospesa tramite MP.pause(), possiamo poi migliorare il lettore aggiungendo un pulsante che imposti il Looping, oppure una SeekBar, come abbiamo visto in precedenza. Naturalmente possiamo utilizzare il metodo listFiles() per qualsiasi tipo di file, non solo per la classe MediaPlayer, in quanto ci restituisce un array File[] da cui possiamo estrapolare i singoli File. 191 4.8 I sensori A seconda del dispositivo che abbiamo a disposizione, esso può avere o non avere alcuni sensori, molti dispositivi hanno ad esempio il sensore di luminosità ambientale oppure il giroscopio, metre pochi dispositivi hanno anche un barometro oppure un sensore di umidità, per poter interagire con tali sensori, ci serviranno i relativi permessi, una volta definiti, possiamo utilizzare le classi SensorManager e Sensor, per gestire tali sensori. Per prima cosa dobbiamo ottenere un SensorManager, per farlo possiamo utilizzare il metodo getSystemService(String name) di Activity, dove name è il tipo di servizio da ottenere, e può essere ottenuto da Context, essendo entrambi super classi di ActionBarActivity, potremmo richiamarle direttamente, in quanto erediate, per ottenere un SensorManager quindi possiamo fare in questo modo: private SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE); Una volta ottenuto il SensorManager, possiamo utilizzarlo per gestire i nostri sensori, per cui ad esempio se volessimo ottenere accesso al sensore di luminosità: private Sensor light = sm.getDefaultSensor(Sensor.TYPE_LIGHT); I sensori disponibili sono contenuti all’interno della classe Sensor, una volta creato il Sensor, possiamo utilizzare alcuni getters della classe Sensor, per ottenere informazioni sul sensore, quali nome getName() produttore getVendor() oppure il consumo energetico di tale sensore espresso in mA tramite getPower() e non solo, per poter interagire attivamente con il sensore dovremmo assegnargli un SensorEventListener, in maniera analoga agli altri Listener incontrati fino ad ora, si tratta di un interfaccia, essa ha due metodi da implementare: public abstract void onSensorChanged (SensorEvent event); Viene chiamato quando il sensore registra qualcosa di nuovo. public abstract void onAccuracyChanged (Sensor sensor, int accuracy); Viene invece chiamato quando cambia la precisione del sensore, possiamo ad esempio prevedere una ricalibrazione del sensore, se la precisone scende sotto una certa soglia, per assegnare un Listener ad un determinato sensore, possiamo utilizzare il metodo registerListener(SensorEventListener listener, Sensor sensor, int rateUs) dove listener sarà il nostro SensorEventListener, sensor il sensore a cui applicare il listener e rateUs la priorità di aggiornamento del sensore, veidiamo ra come registrare un SensorEventListener: … 192 static final SensorManager SM = (SensorManager) getSystemService(SENSOR_SERVICE); static final Sensor LIGHT = sm.getDefaultSensor(Sensor.TYPE_LIGHT); … SM.registerListener(new SensorEventListener(){ @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // Codice } @Override public void onSensorChanged(SensorEvent event) { // Codice } }, LIGHT, SensorManager.SENSOR_DELAY_NORMAL); … I metodi come per tutte le interfaccie vanno obbligatoriamente implementati, nel caso non avessimo bisogno di un determinato metodo, possiamo lasciarlo vuoto, ma va comunque implementato, con onAccuracyChanged, possiamo eseguiure un determinato codice, ogni qualvolta cambi il livello di precisone del sensore, dove sensor è il sensore che ha chiamato il metodo ed accuracy è il nuovo livelo di precisione, il livello di precisione va da 0 a 3 e sono i seguenti valori: SensorManager.SENSOR_STATUS_UNRELIABLE; // 0 - il sensore è inaffidabile SensorManager.SENSOR_STATUS_ACCURACY_LOW; // 1 - il sensore è poco preciso SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM; // 2 - il sensore è abbastanza preciso SensorManager.SENSOR_STATUS_ACCURACY_HIGH; // 3 - il sensore è molto preciso Con onSensorChanged invece possiamo ivece gestire ogni nuova registrazione del sensore, tramite il parametro SensorEvent event, che ci permetterà di gestire l’evento, tramite il parametro event, potremmo ottenere i dati dell’evento, vediamo ora come ottenere i dati da tale evento: … @Override public void onSensorChanged(SensorEvent event){ Sensor sens = event.sensor; // Il sensore che ha generato l’evento int acc = event.accuracy; // La precisone con cui è stato generato l’evento long time = event.timestamp; // il momento, in cui è stato generato l’evento float valore = event.values[0]; // array contenente i valori dell’evento } … La classe SensorEvent, non ha metodi, ma solo variabili public, la variabile che più ci interessa è l’array float[] values, in cui sono contenuti i valori dell’evento, la grandezza dell’array dipende dal tipo di sensore utilizzato, e normalmente spazia tra una lunghezza di 1 ad una lunghezza di 6, nella Javadoc relativa a values, possiamo trovare una mappa dei dati contenuti nell’array relativa ai vari sensori, ad esempio per quanto riguarda il sensore di luminosità abbiamo solamente: 193 values[0]; // Luminosità ambientalie in unità SI lux Per quanto riguarda invece il giroscopio invece abbiamo: values[0]; // Velocità angolare attorno l’asse-x values[1]; // Velocità angolare attorno l’asse-y values[2]; // Velocità angolare attorno l’asse-z Dall’array values quindi possiamo ottenere tutti i dati raccolti dal nodtro sensore, c’è da dire però che non tutti i sensori chiamano il SensorEventListener, alcuni sensori invece utilizzano un TriggerEventListener, a differenza di un SensorEventListener, il TriggerEventListener, è valido solamente una volta, e deve essere quindi registrato nuovamente ad ogni utilizzo, il TriggerEventListener, si registra con il metodo requestTriggerSensor(TriggerEventListener listener, Sensor sensor) l’interfaccia TriggerEventListener ha come unico metodo astratto onTrigger(TriggerEvent event) dove il TriggerEvent è analoga al SensorEvent, una volta chiamato il metodo on trigger, il TriggeEventListener viene cancellato, è anche possibile cancellare manualmente un TriggerEventListener tramite il metodo cancelTriggerSensor (TriggerEventListener listener, Sensor sensor) allo stesso modo può essere anche cancellato un SensorEventListener, tramite i metodi unregisterListener (SensorEventListener listener) oppure unregisterListener (SensorEventListener listener, Sensor sensor) Se non si specifica il sensor, il listener sarà cancellato per tutti i sensori, per cancellare il listener bisogna quindi crearlo tramite un oggetto e non direttamente, in modo da poter poi cancellare il listener passando al metodo l’oggetto, sul TriggerEventListener non ci soffermeremo oltre in quanto introdotto nell’API 18 (Jelly Bean 4.3) e quindi compatibile solo con i dispositivi più recenti, vediamo ora un esempio dell’utilizzo del giroscopio: package com.example.sensors; import import import import import import import import import import import import import android.content.Context; android.hardware.Sensor; android.hardware.SensorEvent; android.hardware.SensorEventListener; android.hardware.SensorManager; android.os.Bundle; android.support.v4.app.Fragment; android.support.v7.app.ActionBarActivity; android.view.LayoutInflater; android.view.Menu; android.view.View; android.view.ViewGroup; android.widget.TextView; public class MainActivity extends ActionBarActivity { private static SensorManager sm; private static Sensor gyro; private static TextView text; 194 private static float x, y, z; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); sm = (SensorManager) getSystemService(Context.SENSOR_SERVICE); gyro = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .add(R.id.container, new PlaceholderFragment()) .commit(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { return true; } public static class PlaceholderFragment extends Fragment { public PlaceholderFragment() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main, container, false); text = (TextView) rootView.findViewById(R.id.textView1); if (gyro == null){ text.setText("Giroscopio non presente"); return rootView; } sm.registerListener(new SensorEventListener(){ @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } @Override public void onSensorChanged(SensorEvent event) { x += event.values[0]; y += event.values[1]; z += event.values[2]; text.setText("X:" + x + "\n Y:" + y + "\n Z:" + z); } }, gyro, SensorManager.SENSOR_DELAY_FASTEST); return rootView; } } } 195 L’utilizzo del giroscopio non richiede permessi aggiuntivi, quindi non dovremmo intervenire sull’AndroidManifest.xml, per prima cosa abbiamo creato un SensorManager tramite getSystemService(Context.SENSOR_SERVICE); Successivamente tramite il SensorManager abbiamo ottenuto il giroscopio predefinito tramite getDefaultSensor(Sensor.TYPE_GYROSCOPE); In questo modo abbiamo ottenuto un oggetto di tipo Sensor, abbiamo poi conrollato se il giroscopio è presente controllando se gyro fosse null, e nel caso il giroscopio fosse presente, abbiamo registrato un SensorEventListener con priorità di aggiornamento massima tramite SensorManager.SENSOR_DELAY_FASTEST, abbiamo poi lasciato vuoto il metodo onAccuracyChanged ed abbiamo implementato onSensorChanged aggiornando i valori delle variabili x y z tramite l’array values, che contiene gli spostamenti positivi o negativi del giroscopio, allo stesso modo possono anche essere gestiti gli altri sensori, cambieranno naturalmente i valori contenuti in event.values, ma la logica di funzionamento è uguale per tutti i sensori. 4.9 Le notifiche Una funzionalità ampiamente utilizzata da molte applicazioni è sicuramente l’uso delle notifiche, ovvero messaggi che appaiono nella barra delle notifiche espandibile, situata nella zona superiore dell’interfaccia grafica di Android, ovvero la barra contenete l’orologio e le icone di stistema, tra cui intensità segnale, carica batteria e wi-fi. Per creare una nuova notifica utilizeremo le classi, Notification e NotificationCompat.Builder. NotificationManager, Per prima cosa, in maniera analoga al SensorManager, dobbiamo creare un NotificationManager tramite il metodo getSystemService, già visto in passato, utilizzando come parametro Context.NOTIFICATION_SERVICE, successivamente procediamo a creare la notifica vera e propria tramite un NotificationCompat.Builder, una volta creato tramite new NotificationCompat.Builder(context); dove context è il nostro Context, dobbiamo definirne le modalità di notifica predefinite, un titolo, un messaggio e l’icona, queste sono le parti basilari della notifica, e possono essere definite tramite i metodi: setDefaults(int defaults); setContentTitle(CharSequence title); setContentText(CharSequence text); setSmallIcon(int icon); Tali metodi restituiscono l’oggetto chiamante, quindi possono essere chiamati in modo sequenziale tramite lo stesso oggetto, setDefaults definisce le modalità di 196 notifica predefinite, come parametro utilizzeremo tipo di notifca predefinito, i tipi sono i seguenti: Notification.DEFAULT_* dove * è il DEFAULT_ALL // Notifica predefinita DEFAULT_VIBRATE // Vibrazione predefinita DEFAULT_SOUND // Suono predefinito DEFAULT_LIGHTS // Led predefinito Tali parametric indicano la parte di notifica predefinita, impostando quindi DEFAULT_SOUND verrà utilizzato il suono di notifica predefinito ed eventualmente vibrazione e led personalizzati se definiti, altrimenti verrà utilizzara solamente la vibrazione, se il metodo non viene chiamato, la notifica non avrà nulla di predefinito. SetContentTitle ci permetterà di definire il titolo della notifica, mentre SetContentText ci permetterà di definire il messaggio della notificaSetSmallIcon infine ci permette di definire l’icona della notifica, essa è necessaria alla sua visualizzazione, e se non specificata porterà alla non visualizzazione della notifica. Per visualizzare la notifica però avremmo bisogno di un oggetto di tipo Notification, per ottenerlo utilizzeremo il metodo build() dul nostro NotificationCompat.Builder, infine visualizziamo lo notifica tramite il nostro NotificationManager grazie al metodo notify(String tag, int id, Notification notification) dove tag è il tag della notifica, id è l’identificativo della notifica, e notification è la nostra notifica, in alternativa tag può essere omesso, nel qual caso conterà solo l’id ottenuta con build(), vediamo ora un semplice esempio: … final NotificationManager n_manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); final Notification notifica = new NotificationCompat.Builder(this) .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_LIGHTS) .setContentText("Messaggio") .setContentTitle("Titolo") .setSmallIcon(R.drawable.ic_launcher) .build(); n_manager.notify("Tag", 0, notifica); … In questo caso verrà visualizzata una notifica senza vibrazione dal titolo Titolo e con Messaggio come messaggio, utilizzando l’icona dell’applacazione, se una nuova notifica con lo stesso Tag ed id di una notifica già presente nella barra delle notifiche viene notificata, tale notifica sostituirà la precedente, se tag e/o id sono differenti, verrà visualizzata una nuova notifica. 197 Oltre ai patametri basilare, possiamo definire anche parametri opzionali, quali suoneria, vibrazione e led personalizzati, comportamento al click oppure inserire al suo interno una ProgressBar, possiamo anche rendere una notifica persistente, ovvero non terminabile dall’utente, per fare questo ci basterà utilizzare il metodo setOngoing(boolean ongoing) dove true è persistente, mentre false è non persistente, per chiudere una notifica persistente dovremmo utilizzare il NotificationManager ed il metodo cancel(CharSequence tag, int id), aggiornare una notifica con setOngoing(false) non la renderà non persistente se già visualizzata, la renderà non persistente se però viene prima chiusa con cancel, ad esempio: … final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); final CharSequence TAG = "Notifica"; final NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setDefaults(Notification.DEFAULT_ALL) .setContentTitls("Notifica") .setContentText("In corso...") .setOngoing(true) .setSmallIcon(R.drawable.ic_launcher); nm.notify(TAG, 0, builder.build()); … public void metodo1(){ builder.setContentText("Ancora in corso...") .setOngoing(false); nm.notify(TAG, 0, builder.build()); } public void metodo2(){ builder.setContentText("Non più in corso") .setOngoing(false); nm.cancel(TAG, 0); nm.notify(TAG, 0, builder.build()); } … In questo esempio abbiamo creato una notifica persistente con setOngoing(true), poi abbiamo creato due metodi, il primo nonostante definisca setOngoing(false) aggiorna semplicemente il testo della notifica precedente in quanto non abbiamo chiamato cancel, il metodo due invece rende la notifica non persistente, in quanto termina la precedente con cancel e visualizza la nuova con notify. È anche possibile come detto in precedenza inserire anche una ProgressBar nella notifica, tramite setProgress(int max, int progress, boolean indeterminate), dove max il valore massimo della ProgressBar, progress è la posizione attuale, ed indeterminate indica se visualizzare o no la posizione, in caso fosse true max e progress possono 198 essere lasciati a 0, per aggiornare la ProgressBar, ci basterà chiamare nuovamente notify, per cui conviene rendere la notifica persistente, e non utilizzare avvisi non specifiacando quind setDefaults, oppure utilizzando setDefaults(0) se già specificato, vediamo ora un esempio di ProgressBar che si aggiorna tramite un Thread: package com.example.notifications; import import import import import android.app.Notification; android.app.NotificationManager; android.os.Bundle; android.support.v4.app.NotificationCompat; android.support.v7.app.ActionBarActivity; public class MainActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); final NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setContentText("Caricamento in corso") .setOngoing(true) .setSmallIcon(R.drawable.ic_launcher); new Thread(){ @Override public void run() { int x = 0; while(x < 100){ x++; notifica = builder.setContentTitle("Progresso: " + x + "%") .setProgress(100, x, false); nm.notify("Notifica", 0, builder.build()); try { sleep(200); } catch (InterruptedException e) { } } notifica = builder.setDefaults(Notification.DEFAULT_ALL) .setContentText("Completato") .setOngoing(false); nm.cancel("Notifica", 0); nm.notify("Notifica", 0, builder.build()); } }.start(); } } In questa applicazione abbiamo utilizzato un Thread semplice, all’interno della stessa classe MainActivity, per il resto il funzionamento è simile a quello di una normale ProgressBar, in questo caso la nostra ProgressBar aumenterà dell’1% ogni 200 199 millisecondi, una volta raggiunto il 100% viene chiusa la notifica persistente e sostituita con una normale notifica. Possiamo anche definire un comportamento al click della notifica tramite il metodo setContentIntent(PendingIntent intent) dove intent è un PendingIntent che può essere ottenuto tramite il metodo static di PendingIntent getActivity(Context context, int requestCode, Intent intent, int flags), quindi ad esempio se volessimo aprire il nostro sito web al click su di una notifica potremmo fare così: … Notifiction notifica = builder.setDefaults(Notification.DEFAULT_ALL) .setContentTitle("Visita il nostro sito") .setContentText("www.miosito.com") .setSmallIcon(R.drawable.ic_launcher) .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.miosito.com")), PendingIntent.FLAG_UPDATE_CURRENT)) .build(); nm.notify("Notifica", 0, notifica); … Naturalmente possiamo utulizzare qualsiasi tipo di intent, quindi possiamo ad esempio richiedere l’accensione del bluetooth o avviare una registrazione video, tramite gli appositi intent, possiamo infine anche definire i tipi di notifiche non definiti default, possiamo impostare il suono della notifica tramite setSound(Uri sound), oppure impostare il tipo di vibrazione tramite setVibrate(long[] pattern) dove pattern è un array long che definisce la vibrazione ogni posizione dell’array indica un alternanza di pause e vibrazione, dove le posizioni pari (partendo da 0) indicano i millisecondi di pausa, mentre le posizioni dispari indicano i millisecondi di vibrazione, quindi pattern = {pausa_0, vibrazione_1, pausa_2, vibrazione_3} e così via, per impostare invece il Led utilizzeremo il metodo setLights(int argb, int onMs, int offMs), dove argb è il colore del Led in formato ARGB (schema di colore esadecimale a 32bit composta da Trasparenza, Rosso, Verde e Blu) per cui ad esempio utilizzeremo 0xffff0000 per il led rosso, oppure 0xffff00 per il led giallo, se un colore non è disponibile, verrà utilizzato il colore più simile disponibile, passando 00 al canale alfa, impediremo l’accensione del led, onMs ed offMs indicano ivece rispettivamente il tempo di accensione e spegnimento del led, passando 0 e 0 il led rimarrà spento, passando 1 ad onMs e 0 ad offMs il led rimarrà accesso, con altri valori lampeggerà secondo le tempistiche definite. 4.10 I servizi Alcune applicazioni anche dopo essere state chiuse, continuano comunque a lavorare senza che noi ce ne accorgiamo, basti pensare ad applicazioni quali Facebook, che 200 riceve i messaggi appena ci colleghiamo ad internet, senza che noi avviamo il programma, questo perché l’applicazione si avvale di un servizio che viene avviato automaticamente all’avvio del dispositivo, e che rimane attivo in background, anche se l’applicazione non viene attivata, un servizio è una classe che estende la classe Service, oppure IntentService, nel caso di servizi su thread separato, in quanto i servizi Service utilizzano il thread principale, salvo definire manualmente un nuovo thread, un Service non ha l’interfaccia grafica e salvo casi particolari (binded services) sono slegati dall’applicazione che li ha installati, quindi se non terminati, rimangono attivi anche se chiudiamo l’applicazione, possiamo addirittura creare un applicazione composta solo da servizi e receiver, senza quindi nessuna activity, che avvii i propri servizi al verificarsi di determinati eventi, quali ad esempio l’accensione del dispositivo o la ricezione di un SMS. Per prima cosa vediamo come creare un servizio, per farlo ci batsterà creare una nuova classe che estenda Service, oppure IntentService se vogliamo che il servizio venga eseguito su di un thread separato senza doverlo prevedere nella classe del servizio, attenzione però, tra le due superclassi ci sono delle differenze nei metodo ereditati, la classe Service ha come metodo astratto onBind(Intent intent), mentre la classe IntentService ha come metodo astratto onHandleIntent(Intent intent), nel caso dell’IntentService utilizzeremo l’implementazione di onHandleIntent come corpo del servizio, mentre nel caso di Service eseguiremo l’override del metodo onStartCommand(Intent intent, int flags, int startId), in alternativa possiamo utilizzare anche il metodo onStart(Intent intent) anche se deprecato, come per le activity possiamo utilizzare anche i metodi onCreate() ed onDestroy(), possiamo anche implementare il metodo onLowMemory() per poter gestire le situazioni in cui il dispositivo ha la memoria RAM satura, tutto ciò può essere fatto anche con un IntentService, in quanto IntentService estende Service. Una volta creato il servizio, esso va registrato nell’AndroidManifest.xml o non verrà eseguito, per farlo ci basterà aprire l’AndroidManifest, e selezionare la scheda Application, poi premere Add… apparirà la seguente finestra -> Ci basterà selezionare Create a new element at the top level, in application, cliccare su Service e confermare con OK. 201 Una volta aggiunto il servizio, lo dovremmo definire, per farlo, dalla schermata Application, selezioniamo in servizio appena aggiunto, come nell’esempio: Clicchiamo su Browse… accando a Name* apparirà la seguente finestra: Qui appariranno tutti i servizi del nostro progetto, non ci resta altro che selezionare il sevizio da registrare, ripetere eventualmente il processo per ogni altro servizio da registrare e salvare l’AndroidManifest. Una volta registrato il servizio, però esso non verrà avviato automaticamente, dovremmo percui anche avviare il servizio manualmente nella nostra applicazione, oppure definirne un criterio d’avvio tramite un BroadcastReceiver, iniziamo con l’avvio manuale del servizio, per avviare manualmente il servizio dalla nostra applicazione, ci basterà utilizzare il metodo startService(Intent service), dove service è l’Intent del servizio da avviare, in modo analogo a startActivity(Intent activity), quindi per avviare il servizio dovremmo anche crearne un Intent, ad esempio: startService(new Intent(this, MyService.class); 202 Il Service se non contenente un thread o non definite IntentService, lavorerà sul thread principale, bloccando quindi la nostra activity, proprio come la chiamata di un metodo, per cui dovremmo definirne un thread in caso di operazioni di molto lunghe o di lunghezza indefinita, salvo nel caso in cui l’applicazione non preveda activity, ma solamente un servizio, in tal caso possiamo utilizzare il main thread, per il resto il funzionamento del Service è simile a quello di un Thread, salvo per il fatto che alla chiusura dell’applicazione vengono chiusi anche tutti i Thread ad essa associata, mentre tranne del caso dei binded services, il service sopravvive all’applicazione, quindi continuerà a funzionare anche nel caso in cui l’applicazione venga termianta, il comportamento del Service è simile a quello del Thread, quindi una volta terminata l’esecuzione del codice al suo interno, si arresterà, è anche possibile terminare il Service manualmente tramite il servizio stesso, utilizzando il metodo stopSelf(). Vediamo ora un esempio di servizio: package com.example.servizi; import import import import android.app.Service; android.content.Intent; android.os.IBinder; android.util.Log; public class MyService extends Service { public MyService() { } @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { int i = 0; while(i < 30){ i++; Log.i("MyService", "i = " + i); try { Thread.Sleep(1000); } catch (InterruptedException e) { } } return super.onStartCommand(intent, flags, startId); } } In questo caso abbiamo utilizzato un normale Service che lavora sul main thread, in questo caso però, il servizio terrà l’applicazione bloccata per tutta la sua durata (30 secondi) in quanto viene eseguito nel Thread principale, possiamo richiamareil metodo Thread.sleep(long millis) anche se Service non estende Thread, in quanto metodo statico, esso sospende il thread per il numero stabilito di millisecondi, in 203 questo caso un secondo, nel nostro caso quindi il nostro servizio visualizzerà sul logcat un messaggio di tipo info con tag MyService mostrandoci ad ogni esecuzione del ciclo, quindi ogni secondo per 30 secondi il valore di i, per avviare il servizio, ci basterà eseguire dalla nostra activity: startService(new Intent(this, MyService.class)); Vediamo invece come realizzare lo stesso servizo utilizzando un IntentService: package com.example.servizi; import android.app.IntentService; import android.content.Intent; import android.util.Log; public class MyIntentService extends IntentService { public MyIntentService() { super("MyIntentService"); } @Override protected void onHandleIntent(Intent intent) { int i = 0; while(i < 30){ i++; Log.i("Service1", "i = " + i); try { Thread.sleep(1000); } catch (InterruptedException e) { } } } } Il codice è analgo al precedente, con la differenza che tale servizio verrà eseguito su di un thread separato, quindi non bloccherà l’esecuzione della nostra applicazione per tutta la sua durata, le principali differenze con Service, stanno nel metodo utilizzato come corpo del servizio, nella non necessità di implementare onBind, in quanto già implementato da IntentService, e nella necessità di inizializzare il costruttore di IntentService con il nome del servizio, in questo caso MyIntentService, per il resto il funzionamento è analogo a quello di un Service. Vediamo ora come inserire un Thread in un Service: package com.example.player; import import import import android.app.Service; android.content.Intent; android.os.IBinder; android.util.Log; public class MyService extends Service { public MyService() { } 204 @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { new Thread(){ @Override public void run() { int i = 0; while(i < 30){ i++; Log.i("MyService", "i = " + i); try { sleep(1000); } catch (InterruptedException e) { } } } }.start(); return super.onStartCommand(intent, flags, startId); } } Possiamo inserire un nuovo Thread in un Service allo stesso modo in cui lo inseriamo nelle altre classi. Abbiamo visto come avviare un Service manualmente dalla nostra applicazione, ma alcune applicazioni potrebbero aver bisogno di avviare automaticamente il servizio all’avvio del sistema, in questo caso dovremmo utilizzare un BroadcastReceiver, si tratta infatti di una classe che viene utilizzata per monitorare gli eventi di sistema tramite degli intent filter, per creare un BroadcastReceiver, ci basterà creare una classe che estenda la classe astratta BroadcastReceiver, tale classe, dovrà poi implementare il metodo astratto onReceive(Context context, Intent intent) da cui potremmo poi avviare il servizio, ad esempio: package com.example.servicetest; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; public class Broadcast extends BroadcastReceiver { @Override public void onReceive(Context arg0, Intent arg1) { arg0.startService(new Intent(arg0, MyService.class)); } } 205 Il BroadcasterReceiver però, purché sia efficace, va prima registrato, possiamo sia registrarlo temporaneamente all’interno dell’applicazione, che permanentemente all’interno del file AndroidManifest, e dato che vogliamo utilizzarlo per avviare un servizio anche ad applicazione non avviata, opteremo per la seconda opzione. La registrazione di un Receiver all’interno del file AndroidManifest è simile alla registrazione di un servizio all’interno dello stesso file, bisognerà però selezionare Receiver, invece di Service inoltre, bisognerà anche definire un IntentFilter, che andrà a definire quali eventi dovrà registrare il nostro Receiver, e la categoria di applicazione, tali filtri sono necessari al funzionamento del Receiver, senza di essi infatti il nostro Receiver non registrerà alcun evento, per aggiungere un IntentFilter, dovremmo aggiungere un sotto livello di Receiver, selezionandolo e cliccando su Add… in questo modo: Una volta creato l’IntentFilter, dovremmo aggiugere un altro sottolivello, contenete un Category, ed una o più Action, allo stesso modo dell’IntentFilter, come Category, dovremmo definire la categora della nostra azione, possiamo selezionare android.intent.category.HOME mentre come Action, dovremmo selezionare l’azione da far Registrare al nostro Receiver, ad esempio android.intent.action.SCREEN_ON per eseguire il metodo onReceive del nostro Receiver ogniqualvolta venga acceso lo schermo del dispositivo, oppure android.intent.action.SCREEN_OFF per quando lo 206 schermo viene invece spento, per eseguire un operazione all’avvio del dispositivo, utilizzeremo invece android.intent.action.BOOT_COMPLETED come detto in precedenza, possiamo registrare anche più azioni nello stesso Receiver, in questo caso il metodo onReceive, verrà eseguito al verificarsi di ogni azione, possiamo però ottenere l’azione chiamante grazie al metodo getAction() del parametro Intent, per cui ad esempio possiamo differenziare il comportamento in base all’azione in questo modo: package com.example.servicetest; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; public class Broadcast extends BroadcastReceiver { @Override public void onReceive(Context arg0, Intent arg1) { if(arg1.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) arg0.startService(new Intent(arg0, BootService.class)); else if (arg1.getAction().equals(Intent.ACTION_SCREEN_ON)) arg0.startService(new Intent(arg0, onService.class)); else if (arg1.getAction().equals(Intent.ACTION_SCREEN_OFF)) arg0.startService(new Intent(arg0, offService.class)); } } Per confrontare correttamente le azioni utilizzeremo il metodo equals, simile all’operatore di uguaglianza == che ci restituirà true se l’oggetto chiamante coincide nel contenuto con l’oggetto dato come parametro, l’operatore == restituirà true invece solo se i due oggetti combaciano completamente, e non solo nel contenuto, quindi dovremmo utilizzare equals, in questo caso il servizio avviato dipenderà dal tipo di azione ricevuta, il BoardcastReceiver, può anche eseguire del codice al suo interno, invece di lanciare un servizio, ma nel caso di operazioni lunghe e complesse, è comunque sempre consigliato utilizzare un servizio, invece di tenere il Receiver occupato, il servizio avviato, potrà poi lavorare in background ed eseguire le operazioni da noi richieste, come ad esempio la ricezione dei messaggi di Facebook, ma, anche operazioni più semplici, moltissime applicazioni fanno uso dei servizi, in quanto molto versatili, in questo modo potremmo anche creare un applicazione composta solo da Service e Receiver, senza quindi alcuna Activity, il Receiver penserà poi a gestire i servizi in modo automatico. 207 CONCLUSIONI Abbiamo visto come creare applicazioni per Android, sia alla portata di tutti e non solo, essendo i programmi Android basati su Java, chiunque con un minimo di esperienza con tale linguaggio è già da subito capace di programmare anche su Android, essendo inoltre Java un linguaggio ad oggetti, risulta comunque più comprensibile di un linguaggio puramente testuale, si tratta inoltre di uno dei linguaggi più diffusi al mondo, per cui conoscere il Java, ci permetterà di programmare non solo su Android, naturalmente qui abbiamo solamente scalfito la superficie della programmazione Java ed Android, qui abbiamo quindi affrontato le basi della programmazione, con queste basi poi potremmo poi realizzare applicazioni sempre più complesse, in quanto anche le operazioni più comlesse seguono le stesse logiche delle basi, comunque sia, potremmo trovare tutte le informazioni relative a classi e metodi nella relativa Javadoc e nella documentazione ufficiale per sviluppatori Andriod (in lingua inglese) sul sito http://developer.android.com/ documentazione che a questo punto ci dovrebbe apparire piuttosto chiara. BIBLIOGRAFIA Tutto ciò che c’è da sapere sull’argomento come detto in precedenza è reperibile dalla documentazione ufficiale Java e Android, quindi le fonti utilizzate sono le seguenti: - Javadoc di Eclipse - http://developer.android.com/