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/