Fabio Collini
Matteo Bonifazi, Alessandro Martellucci, Stefano Sanna
2a
Programmazione avanzata
IZIONE
ED
LLER
TSE
S
E
B
Sviluppo multidevice >>
Android Wear, Chromecast, Bluetooth Low Energy >>
Programmazione funzionale con RxJava >>
Testing e qualità del codice >>
NUO
Android
BE
Android
Programmazione avanzata
Seconda edizione
Fabio Collini
Matteo Bonifazi, Alessandro Martellucci, Stefano Sanna
Android | Programmazione avanzata
Seconda edizione
Autori: Fabio Collini, Matteo Bonifazi, Alessandro Martellucci, Stefano Sanna
Collana:
Editor in Chief: Marco Aleotti
Progetto grafico: Roberta Venturieri
Immagine di copertina: © scanrail | Thinkstock
© 2015 Edizioni Lswr* – Tutti i diritti riservati
ISBN: 978-88-6895-071-2
I diritti di traduzione, di memorizzazione elettronica, di riproduzione e adattamento totale o parziale con qualsiasi mezzo (compresi i microfilm e le copie fotostatiche), sono riservati per tutti i Paesi. Le fotocopie per uso personale del lettore possono essere
effettuate nei limiti del 15% di ciascun volume dietro pagamento alla SIAE del compenso previsto dall’art. 68, commi 4 e 5, della
legge 22 aprile 1941 n. 633.
Le fotocopie effettuate per finalità di carattere professionale, economico o commerciale o comunque per uso diverso da
quello personale possono essere effettuate a seguito di specifica autorizzazione rilasciata da CLEARedi, Centro Licenze e
Autorizzazioni per le Riproduzioni Editoriali, Corso di Porta Romana 108, 20122 Milano, e-mail [email protected]
e sito web www.clearedi.org.
La presente pubblicazione contiene le opinioni dell’autore e ha lo scopo di fornire informazioni precise e accurate. L’elaborazione
dei testi, anche se curata con scrupolosa attenzione, non può comportare specifiche responsabilità in capo all’autore e/o all’editore per eventuali errori o inesattezze.
L’Editore ha compiuto ogni sforzo per ottenere e citare le fonti esatte delle illustrazioni. Qualora in qualche caso non fosse riuscito
a reperire gli aventi diritto è a disposizione per rimediare a eventuali involontarie omissioni o errori nei riferimenti citati.
Tutti i marchi registrati citati appartengono ai legittimi proprietari.
Via G. Spadolini, 7
20141 Milano (MI)
Tel. 02 881841
www.edizionilswr.it
Printed in Italy
Finito di stampare nel mese di giugno 2015 presso “Rotolito Lombarda” S.p.A., Pioltello (MI)
(*) Edizioni Lswr è un marchio di La Tribuna Srl. La Tribuna Srl fa parte di
.
Sommario
PREFAZIONE............................................................................................................... 9
INTRODUZIONE........................................................................................................11
1.
ACTIVITY E TASK IN BACKGROUND di Fabio Collini.................................17
Ciclo di vita di una Activity.......................................................................................................17
Gestione dei metodi di callback comuni a più Activity.................................................... 18
Flusso delle callback del ciclo di vita di una Actvity..........................................................22
Salvataggio dello stato di una Activity.................................................................................23
Generazione automatica delle implementazioni
di Parcelable................................................................................................................................26
Tipi di dati da salvare in una Activity....................................................................................28
UI Thread e concorrenza..........................................................................................................29
Tipologie di task in background..............................................................................................33
AsyncTask e Loader...................................................................................................................34
IntentService e LocalBroadcastManager.............................................................................35
EventBus ed Executor................................................................................................................ 41
2. PROGRAMMAZIONE FUNZIONALE di Fabio Collini.................................47
Lambda expression e method reference............................................................................. 48
Retrolambda.................................................................................................................................51
Linguaggi alternativi a Java su Android................................................................................52
Manipolazione di dati con gli Stream di Java 8..................................................................53
RxJava...........................................................................................................................................57
Flussi di dati asincroni................................................................................................................61
Gestione delle Subscription.................................................................................................... 66
Gestione degli errori................................................................................................................. 68
Chiamate a servizi REST con Retrofit.....................................................................................71
Combinare più flussi di dati con RxJava...............................................................................73
Hot e cold Observable............................................................................................................. 80
Manipolazione di flussi di dati............................................................................................... 84
Utilizzo dei Subject................................................................................................................... 86
Eventi della UI con RxJava........................................................................................................92
Gestione dei nested Observable........................................................................................... 94
Task in background collegati al ciclo di vita di una Activity........................................... 96
5
Android | Programmazione avanzata
3. GRAFICA E INTERFACCIA UTENTE di Fabio Collini.................................109
Density e screen size...............................................................................................................109
Immagini 9patch........................................................................................................................111
Drawable complessi..................................................................................................................111
Custom View per implementare un flat Button................................................................115
Immagini Mipmap.....................................................................................................................117
Vector Drawable........................................................................................................................117
Organizzazione delle risorse..................................................................................................118
Canvas, Paint e Shader.............................................................................................................121
Color filter.................................................................................................................................. 126
Utilizzo delle Custom View per migliorare le performance...........................................128
Animazioni Android 2.x.......................................................................................................... 136
Animazioni Android 3.0......................................................................................................... 138
Animazioni Android 4.4.........................................................................................................143
Animazioni Android 5.0.........................................................................................................144
Gestione degli eventi touch...................................................................................................148
Drag di una View con animazioni........................................................................................150
4. SUPPORTO MULTIDEVICE di Fabio Collini................................................. 157
Dimensioni degli schermi dei dispositivi Android........................................................... 157
Gestione delle risorse al variare della dimensione dello schermo.............................. 159
Gestione della orientation...................................................................................................... 162
Gestione dei layout in base alla larghezza dello schermo............................................. 163
Adattamento di un layout in base alle dimensioni.......................................................... 163
Utilizzo dei Fragment...............................................................................................................164
Gestione di una Activity multiFragment............................................................................ 167
Gestione delle transaction..................................................................................................... 172
Altri utilizzi dei Fragment....................................................................................................... 174
Activity con singolo Fragment.............................................................................................. 174
Custom View Vs Fragment.................................................................................................... 176
5.BLUETOOTH di Stefano Sanna........................................................................ 179
L’ultimo immortale................................................................................................................... 179
Bluetooth Classic e Bluetooth Low Energy........................................................................180
Panoramica del protocollo Bluetooth Classic................................................................... 182
Panoramica del protocollo Bluetooth Low Energy...........................................................189
Setup per sperimentazione.................................................................................................... 195
Bluetooth Classic su Android...............................................................................................209
Attivazione e visibilità.............................................................................................................210
Discovery dei dispositivi ........................................................................................................ 212
Pairing......................................................................................................................................... 216
Discovery dei servizi................................................................................................................ 219
Apertura diretta di connessioni RFCOMM....................................................................... 222
Bluetooth Low Energy su Android....................................................................................... 223
Bluetooth beacon.................................................................................................................... 230
6
Sommario
La tecnologia iBeacon............................................................................................................ 232
Implementazione di un beacon........................................................................................... 235
Scansione dei beacon in ambiente Android..................................................................... 238
Beacon e geofencing............................................................................................................... 242
Inversione di ruoli.................................................................................................................... 243
Sicurezza e privacy................................................................................................................. 245
6. ANDROID WEAR di Matteo Bonifazi............................................................ 249
Android Wear idea..................................................................................................................250
Android Wear design............................................................................................................. 252
Notifiche tramite Android Wear......................................................................................... 255
Android Wear App.................................................................................................................. 265
Android Wear Watch face.................................................................................................... 287
7. CHROMECAST E GOOGLE CAST di Alessandro Martellucci................... 305
Che cos’è il Chromecast........................................................................................................ 305
Installazione e configurazione.............................................................................................306
Chromecast App..................................................................................................................... 307
Google Cast............................................................................................................................... 313
Applicazione Sender................................................................................................................314
Il bottone di Cast...................................................................................................................... 318
Il media router framework..................................................................................................... 319
Il ruolo del Google Play Services.......................................................................................... 331
La gestione del receiver da parte dell’applicazione client..............................................333
La riproduzione del contenuto.............................................................................................340
La comunicazione tra le applicazioni................................................................................. 345
Il controllo del volume............................................................................................................ 350
I sottotitoli.................................................................................................................................. 351
Il logging.................................................................................................................................... 354
Applicazione receiver............................................................................................................. 354
Applicazione receiver: concetti avanzati........................................................................... 363
Tipi e formati supportati........................................................................................................ 369
Android TV............................................................................................................................... 370
8. QUALITÀ DEL CODICE di Fabio Collini........................................................375
Build dei progetti con Gradle.................................................................................................375
Testing del codice.................................................................................................................... 378
Tipologie di testing................................................................................................................. 379
Unit test con JUnit................................................................................................................... 381
Test di integrazione................................................................................................................. 387
Robolectric................................................................................................................................ 387
Testing end to end................................................................................................................... 389
Acceptance test....................................................................................................................... 396
Monkey testing........................................................................................................................ 397
Copertura dei test................................................................................................................... 397
7
Android | Programmazione avanzata
Analizzatori statici del codice.............................................................................................. 398
Continuous Integration.......................................................................................................... 399
Strategia di test di una applicazione Android..................................................................402
Testing di codice legacy.........................................................................................................402
Servizi web remoti.................................................................................................................. 404
Dependency injection............................................................................................................405
Dagger.........................................................................................................................................412
Testing di codice dipendente dal tempo............................................................................ 425
Testing su Java Virtual Machine di una applicazione Android....................................426
Testing della UI con Espresso............................................................................................... 433
Model View Presenter........................................................................................................... 435
Task asincroni con Model View Presenter e RxJava.......................................................447
Oggetti fake con Javassist.....................................................................................................450
BDD e TDD con Espresso e Model View Presenter....................................................... 455
APPENDICE: SICUREZZA................................................................................... 457
La sicurezza nell’azienda....................................................................................................... 457
Utilizzo delle WebView.........................................................................................................463
Android permission................................................................................................................468
INDICE ANALITICO............................................................................................. 475
8
Prefazione
Quando gli autori di Android - Programmazione avanzata mi hanno chiesto di scrivere
la prefazione alla nuova edizione, il mio pensiero è andato immediatamente ai numeri impressionanti che il sistema operativo di Google ha macinato in questi ultimi
anni, alla strada percorsa dal lancio del 2008 sul primo dispositivo targato HTC.
Senza voler entrare nel dettaglio dei singoli dati, basti pensare che, secondo le stime
di IDC, negli ultimi due anni circa 80 smartphone venduti su 100 sono stati equipaggiati con il sistema operativo del robottino. Proiettato su scala globale, questo dato
significa un mercato potenziale di centinaia di milioni di persone.
Significa inoltre che, attorno al 2018, il fatturato delle app per Android supererà
quello generato dal principale concorrente, iOS di Apple. Non a caso, Flipboard ha
lanciato la propria versione per Android due anni dopo la versione per iOS, ma in
poco più di un anno la metà degli accessi arriva ormai da smartphone Android.
La motivazione per imparare a programmare applicativi per Android, però, non deve
limitarsi alla metrica numerica.
Perché leggere – e studiare – un volume dedicato a chi sviluppa in maniera avanzata
per Android? La risposta sta nell’impressionante successo e nelle previsioni di crescita future, certo.
Un volume che tratta argomenti comuni per gli sviluppatori, con un approccio fuori
dal comune, e si spinge a trattare argomenti che i classici testi di programmazione
per Android spesso non considerano, non avrebbe un senso se non nella convinzione
che “Google stia per conquistare le nostre vite”, per riprendere le parole di Fastcode.
Provenendo dal mondo delle Telco, dove ho avuto la fortuna di lavorare al lancio
della prima rete UMTS al mondo, del primo cellulare con TV digitale mobile e del
primo Skypephone low cost, quando ancora gli smartphone non erano così diffusi, il
sistema operativo Android non è qualcosa di nuovo. Prima ancora, come giornalista
tecnologico, ho avuto l’opportunità di vivere le principali trasformazioni della telefonia mobile, tra cui il declino dei sistemi operativi proprietari e l’esplosione delle app.
9
Android | Programmazione avanzata
Negli ultimi due anni sono successe, però, diverse cose che mi hanno fatto comprendere quanto pervasivo possa essere – parallelamente all’avanzata dell’Internet degli
oggetti – un sistema operativo così versatile e diffuso. Sono stato tra i pochi italiani
a poter ricevere i Google Glass durante il programma Explorer, e Android Auto è
qualcosa su cui ho lavorato negli ultimi 24 mesi per il debutto di Google nel mondo
dell’auto.
È da questo punto di vista, come osservatore privilegiato, che ho imparato ad apprezzare Google come un risolutore di problemi in (quasi) ogni ambito della nostra
vita, una presenza amorfa che il Material Design – l’unificazione di tutti i prodotti
Google e app di terze parti sotto un unico cappello – rende concreta e possibile,
un sistema operativo che rompe le regole logiche e fisiche note fino a oggi per ridisegnare completamente il mondo che ci circonda. Un Google che da prodotto si
trasforma in presenza, con la giusta informazione sullo schermo giusto al momento
giusto. Cellulari, auto, console gaming, schermi del desktop, computer portatili, wearable e (forse) occhiali.
In un mondo così variegato, la completa padronanza del sistema operativo targato
Google è fondamentale per chi sviluppa applicativi complessi. Alessandro, Fabio,
Matteo e Stefano ci guidano, attraverso i capitoli di Android Programmazione Avanzata, nell’esplorazione di questo mondo affascinante e allo stesso tempo complesso.
Per chi condivide la visione sul futuro di Android (e Google), sviluppando applicativi
articolati, conoscerne gli anfratti più nascosti non può che rappresentare una tappa
fondamentale del proprio percorso professionale.
Massimo Cavazzini
Global Uconnect – Head of Marketing&User Experience EMEA region
Fiat Chrysler Automobiles
10
Introduzione
Questo libro parla di una scommessa vinta. Android è il sistema operativo per dispositivi mobili più diffuso al mondo. In realtà, non solo dà vita a smartphone e tablet,
ma è presente in smartwatch, apparati televisivi, autovetture e sistemi embedded.
E non solo in prodotti commerciali, ma anche in progetti indipendenti, vista la sua
natura open source. L’ecosistema di Android si arricchisce costantemente di nuove
API, nuove librerie, nuovi strumenti di sviluppo, nuovo hardware compatibile, nuove
estensioni. Per la gioia degli sviluppatori e degli utenti. Sviluppatori motivati realizzano prodotti che entusiasmano gli utenti, che a loro volta aumentano la domanda di
nuovi servizi e nuove applicazioni, che a sua volta alimenta la domanda di sviluppatori sempre più specializzati. È inevitabile: laddove c’è una tecnologia così pervasiva
e ubiqua nella vita delle persone, in poco tempo si aprono spazi infiniti per nuove
idee, nuove possibilità.
L’obiettivo di questo libro è fornire agli sviluppatori che già conoscono le basi della programmazione su Android le conoscenze per affrontare problematiche meno
comuni e trarre ispirazione dalle potenzialità più intriganti del sistema operativo.
Partendo dalla propria esperienza professionale consolidata, gli autori hanno selezionato gli argomenti mirando alla crescita del lettore su due fronti: irrobustire la conoscenza dei temi “core”, al fine di migliorare la propria padronanza della piattaforma (si vedano i temi sul supporto multidevice o la gestione dei task in background)
e stimolare nuove esplorazioni sui temi più attuali (wearable e interazione con apparati televisivi).
Gli argomenti discussi nei vari capitoli comprendono riferimenti non solo alle librerie standard di Android, ma anche ai molti progetti open source utilizzabili per
lo sviluppo di applicazioni per questo sistema operativo. Negli ultimi anni, infatti,
molte aziende (un tempo startup e adesso colossi spesso quotati in borsa) stanno
rilasciando librerie open source che semplificano varie fasi dello sviluppo. Square
è sicuramente una delle realtà più attive, ma anche Facebook, Yahoo!, Instagram
11
Android | Programmazione avanzata
e NetFlix annoverano vari progetti open source attraverso i quali stanno facendo
crescere varie community di sviluppatori. Un esempio molto significativo è quello
di RxJava (affrontato nel capitolo dedicato alla programmazione funzionale), una
libreria open source sviluppata da Netflix che sta rivoluzionando il modo di scrivere
il codice delle applicazioni Android.
In questo senso, pur se cristallizzato nelle sue pagine, questo libro è “vivo”, attualizzato non solo nella mera versione delle API, ma anche negli obiettivi. È interessante,
infatti, notare come temi ritenuti avanzati nella prima edizione non lo siano più nella
seconda: è il caso delle notifiche push, ormai consolidate sia client-side sia serverside, sulle quali è possibile trovare in rete numerosissimi esempi di codice e servizi gratuiti che implementano le funzionalità di backend (per esempio, parse.com).
Altri, come NFC, hanno avuto poche evoluzioni (se non l’importante introduzione
della modalità HCE) e rispetto alle aspettative di tre anni fa sono rimasti argomenti
di nicchia.
A chi si rivolge questo libro
Questo libro è rivolto agli sviluppatori che hanno già una esperienza nello sviluppo di
applicazioni Android e che vogliono migliorare le proprie skill di programmazione su
questo sistema operativo mobile. Gli argomenti sono trattati partendo dal presupposto che il lettore abbia una conoscenza dei principali concetti dello sviluppo di una
applicazione Android.
Gli autori
Fabio Collini si occupa, all’interno dell’acceleratore di startup Nana Bianca di Firenze,
dello sviluppo di varie applicazioni disponibili nel Play Store. Dopo una esperienza su
piattaforma Java Enterprise, dal 2009 si occupa di progettazione e sviluppo di applicazioni Android. È attivo nella community sia come blogger (ha fondato e scrive
su cosenonjaviste.it) sia come speaker nelle principali conferenze a livello nazionale.
Come freelance, ha rilasciato due applicazioni che hanno ottenuto un buon numero di
download.
Matteo Bonifazi è Senior Android developer di Open Reply (Gruppo Reply) e Google
Developer Expert per la piattaforma Android. Ha partecipato alla realizzazione di importanti progetti Android in campo nazionale e internazionale, riguardanti lo sviluppo
di applicazioni Android innovative e personalizzazioni del sistema operativo Android.
Collabora attivamente con il Google Developer Group di Roma, dove si occupa principalmente di far conoscere alla comunità tutte le novità dell’ecosistema Android. È
speaker per le più importanti conferenze italiane e internazionali.
12
Introduzione
Alessandro Martellucci è laureato in Informatica e da diversi anni segue il mercato
mobile, con particolare interesse per l’ecosistema Android. A oggi è Android developer presso OpenReply, società del gruppo Reply, e partecipa a progetti con diverse
peculiarità: dal video streaming all’installazione di Android su dispositivi con hardware
personalizzato. Nel 2014 ha partecipato al Droidcon UK con un seminario dedicato al
video streaming in Android, parlando del Chromecast e dell’Android TV.
Stefano Sanna è attualmente Manager presso Open Reply (Gruppo Reply), dove coordina le attività di sviluppo di app native e ibride. Ha iniziato a sviluppare software per
dispositivi mobili nel 1999, su un Psion 5MX. Da allora ha lavorato su Java Micro Edition, Symbian, iOS e dal 2009 si occupa di Android per applicazioni commerciali. Ha
scritto due libri sullo sviluppo mobile e tenuto numerosi seminari su Java, Android e
Bluetooth in Italia e all’estero. Lasciati gli smartphone, ama giocare con i suoi bambini
a costruire robot e navi spaziali con i LEGO.
La struttura
Il libro è diviso in otto capitoli, ognuno dei quali contiene la trattazione di un argomento specifico. In alcuni casi sono presenti riferimenti ad altri capitoli, per questo motivo
è consigliata la lettura nell’ordine in cui sono proposti.
Il Capitolo 1 descrive nel dettaglio il ciclo di vita delle Activity; pur essendo uno dei
primi argomenti affrontati quando ci si avvicina ad Android, non è sempre facile da digerire. In questo capitolo sono affrontate le problematiche più comuni, prima fra tutte
la gestione dei task in background.
Il Capitolo 2 affronta la programmazione funzionale su Android: purtroppo ancora
non è possibile utilizzare Java 8 per lo sviluppo di applicazioni Android, anche se,
come mostrato in questo capitolo, Retrolambda è un’ottima alternativa. Gran parte
di questo capitolo è dedicata al framework RxJava, mostrando sia i concetti base sia
quelli più avanzati.
La grafica e la gestione dell’interfaccia utente sono l’argomento del Capitolo 3. Questo capitolo contiene varie tecniche da utilizzare per sviluppare la parte di UI di una
applicazione. Fra gli argomenti trattati ci sono i Drawable, le animazioni, i Canvas e la
gestione degli eventi touch.
Gestire al meglio le risorse è fondamentale (ma non sempre facile) per creare layout
che si adattino ai vari dispositivi: il Capitolo 4 contiene un approfondimento su questo
tema, con particolare riferimento alla gestione del supporto multidevice e all’utilizzo
dei Fragment.
L’argomento presentato nel Capitolo 5 è Bluetooth, con una dettagliata trattazione di
Bluetooth Low Energy (BLE), la cui API ha fatto la sua comparsa su Android 4.3. BLE
13
Android | Programmazione avanzata
consente l’interfacciamento a dispositivi indossabili (tra cui le varie “band” per il fitness) e, più in generale, è la tecnologia abilitante per la Internet Of Things.
Tra il 2015 e il 2016 si prevede che il numero dei televisori connessi alla rete sfiorerà
il miliardo di unità; Chromecast e Android TV (trattati nel Capitolo 6) rappresentano
solo l’inizio di questa nuova era tecnologica che colpirà le nostre vite. Il supporto da
parte di Mountain View, con il suo Google Cast, offre a tutti gli sviluppatori un mondo di possibilità ancora inesplorate: l’On Demand, il Gaming e l’Entertainment sono
solo alcuni tra gli argomenti più interessanti, sui quali tutti gli sviluppatori dovrebbero
scommettere.
Come cambia la user experience sui nostri polsi? Il Capitolo 7, dedicato ad Android
Wear, mostra nel dettaglio come sarà possibile estendere le proprie applicazioni su
tutti i nuovi smartwatch powered by Google.
Il testing su Android è il principale argomento del Capitolo 8: la prima parte contiene
una panoramica sui vari tool disponibili, da quelli per il testing fino a quelli per l’analisi
statica del codice e la continuous integration. Nella seconda parte sono analizzate alcune tecniche (come la dependency injection usando Dagger e il pattern Model View
Presenter) da utilizzare per scrivere codice testabile.
Completa il volume l’Appendice, dedicata alla sicurezza delle applicazioni Android;
questo tema viene affrontato mostrando alcune funzionalità messe a disposizione dal
sistema operativo utilizzabili per aumentare la sicurezza delle applicazioni Android.
Risorse
Sito web ufficiale del libro: http://www.androidavanzato.it
Repository codice: https://github.com/androidavanzato
E-mail per supporto e segnalazioni: [email protected]
Twitter: @androidavanzato
Ringraziamenti
Gli autori ringraziano l’editore per la fiducia riposta, in particolare Marco Aleotti per il
suo supporto instancabile durante la scrittura e l’evoluzione dell’opera.
Fabio ringrazia Letizia per averlo sopportato nonostante, durante la scrittura del libro,
sia stato ancora più asociale del solito. Inoltre ringrazia Stefano per averlo coinvolto
in questo progetto, e Matteo e Alessandro per aver arricchito, con i loro importanti
contributi, gli argomenti trattati in questo libro.
Matteo dedica il suo lavoro alle persone che lo hanno aiutato in modo diretto e indiretto nella scrittura di questo libro. Push the limits!
14
Introduzione
Alessandro ringrazia, in egual maniera, tutti coloro che lo hanno supportato e aiutato
durante la stesura del libro. È grazie a loro se il suo contributo è oggi migliore rispetto
a com’era all’inizio.
Stefano ringrazia in primis Fabio: senza la sua enorme tenacia e solidissima preparazione questa seconda edizione non avrebbe conosciuto gli onori della stampa. Un
grazie sincero va ad Alessandro e Matteo, che hanno accolto con entusiasmo questo
progetto, contribuendo con temi freschi e appassionanti. Infine, dedica il risultato di
questo lavoro ai piccoli Riccardo e Alessandro, la cui intuizione, nel chiedere “Papà,
anche qui dentro c’è un piccolo computer?”, è misura della pervasività delle tecnologie
raccontate in questo libro.
15
1
Activity e task in background
di Fabio Collini
Questo capitolo mostra nel dettaglio l’utilizzo delle Activity
all’interno di una applicazione, ponendo particolare attenzione
sull’esecuzione dei task in background. Sono analizzate le
varie soluzioni disponibili: sia quelle contenute nel framework
Android (come gli Async
Task e gli Intent Service) sia
quelle utilizzabili attraverso librerie di terze parti.
Ciclo di vita di una Activity
Il concetto di Activity è uno dei più importanti nello sviluppo di una applicazione Android ed è sicuramente il primo che ogni sviluppatore che si avvicina a questo sistema operativo apprende. Nei primi esempi, a una Activity corrisponde una schermata
dell’applicazione; questa corrispondenza non è vera negli esempi più complessi, ma è
comunque un buon modo per familiarizzare con il concetto di Activity.
Scrivendo una applicazione Android che implementa il classico hello world, è necessario creare una classe che estende Activity e riscrivere un solo metodo: onCreate. Questo metodo di callback viene invocato automaticamente in corrispondenza della creazione dell’Activity, e di solito contiene il codice per inizializzare l’interfaccia grafica. Il
concetto di callback è ormai abbastanza comune a molti framework di sviluppo; in una
applicazione Android uno sviluppatore non deve scrivere una classe con un metodo
statico main invocato all’avvio. Un metodo main esiste, ma è nelle classi del framework
Android, in particolare nella classe ActivityThread. In pratica, non sono le classi scritte
17
Android | Programmazione avanzata
da uno sviluppatore che richiamano i metodi del framework; tali classi sono inserite
all’interno del framework e contengono dei metodi (chiamati, appunto, di callback)
richiamati in corrispondenza di alcuni eventi. Il principio che sta dietro questo modo di
organizzare le classi è chiamato anche principio Hollywood e può essere enunciato con
la frase don’t call me, I’ll call you.
Il ciclo di vita di una Activity non è banale da gestire; per questo motivo, il metodo onCreate non è l’unica callback disponibile. Le callback principali sono sei; per analizzarle
meglio è possibile raggrupparle a coppie:
• onCreate/onDestroy: richiamati, rispettivamente, dopo la creazione e prima che
l’Activity sia distrutta. Il metodo onCreate contiene solitamente la creazione
dell’interfaccia grafica; eventuali oggetti creati in questo metodo che necessitano di una chiusura esplicita (per esempio, le connessioni a database) sono
gestiti nel metodo onDestroy;
• onStart/onStop: richiamati prima che l’Activity diventi visibile e quando questa
viene coperta da altre Activity. La callback onStart è il metodo giusto in cui scrivere il popolamento dell’interfaccia grafica;
• onResume/onPause: la differenza con le callback onStart/onStop è abbastanza
sottile; sono richiamati sempre in sequenza tranne nel caso in cui l’Activity
venga coperta da un’altra in modo parziale. In questo caso viene richiamato
solo onPause e non onStop; quando l’Activity torna in primo piano viene richiamato solo onResume. In pratica, dopo l’esecuzione di onStart e prima di onStop vi
è la certezza che l’Activity sia visibile anche solo parzialmente; dopo onResume
e prima di onPause l’Activity è visibile e in primo piano (per questo l’utente
può anche interagire con le View contenute). Spesso la distinzione fra queste
coppie di callback non è importante e, per questo motivo, non è necessario
riscrivere tutti i metodi.
Un aspetto del ciclo di vita da tenere presente per non incorrere in errori consiste nel
fatto che i metodi onCreate e onDestroy sono invocati una sola volta su un oggetto; tutti
gli altri possono essere invocati più di una volta. È infatti abbastanza frequente il caso
in cui una Activity venga messa in background e ritorni successivamente in primo
piano (per esempio, dopo la pressione del tasto back).
Gestione dei metodi di callback comuni a più Activity
In alcuni casi può essere utile eseguire del codice in corrispondenza di una callback
per ogni Activity presente nell’applicazione; si pensi, per esempio, all’inizializzazione
di alcune librerie che permettono di tracciare l’utilizzo da parte degli utenti. Il modo
18
Activity e task in background
Capitolo 1
più semplice è ovviamente quello di aggiungere l’invocazione di un metodo dentro
ogni callback; può essere utilizzato nel caso in cui il numero di Activity da gestire è
basso. Se l’applicazione contiene molte Activity, il copia incolla non è mai la soluzione
migliore e porta a codice difficilmente gestibile.
Classe base per il codice a comune
Una soluzione per evitare il copia incolla è quella che viene comunemente usata nello
sviluppo di codice object oriented: è possibile creare una classe base estesa da tutte le
Activity in cui fattorizzare il codice a comune. Anche questa soluzione ha comunque
delle controindicazioni dovute principalmente all’assenza dell’ereditarietà multipla in
Java. Una Activity può estendere una sola classe e quindi la gestione diventa problematica nel caso in cui ci sia la necessità di estendere classi diverse (per esempio, Activity e ListActivity). In questi casi, avendo più classi da estendere, sarebbe comunque
necessario duplicare il codice a comune.
Utilizzo di ActivityLifecycleCallbacks
Con la release di Android 4.0 è stata aggiunta l’interfaccia ActivityLifecycleCallbacks
all’interno della classe Application. Per sfruttarla è necessario creare una classe che
estende Application e registrarla all’interno del manifest. All’interno della callback onCreate della sottoclasse di Application è possibile registrare un oggetto che implementa ActivityLifecycleCallbacks; i metodi contenuti verranno invocati automaticamente
in corrispondenza delle callback di ogni Activity. Un esempio di utilizzo è il seguente:
@Override public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override public void onActivityCreated(
Activity activity, Bundle savedInstanceState) {
Log.d(TAG, "Activity created: " + activity.getClass().getSimpleName());
}
@Override public void onActivityStarted(Activity activity) {
Log.d(TAG, "Activity started: " + activity.getClass().getSimpleName());
}
@Override public void onActivityResumed(Activity activity) {
Log.d(TAG, "Activity resumed: " + activity.getClass().getSimpleName());
}
@Override public void onActivityPaused(Activity activity) {
Log.d(TAG, "Activity paused: " + activity.getClass().getSimpleName());
}
@Override public void onActivityStopped(Activity activity) {
19
Android | Programmazione avanzata
Log.d(TAG, "Activity stopped: " + activity.getClass().getSimpleName());
}
@Override public void onActivitySaveInstanceState(
Activity activity, Bundle outState) {
Log.d(TAG, "State saved: " + activity.getClass().getSimpleName());
}
@Override public void onActivityDestroyed(Activity activity) {
Log.d(TAG, "Activity destroyed: " + activity.getClass().getSimpleName());
}
});
}
In questo modo la logica può essere scritta in un posto solo, potendo comunque gestire comportamenti diversi in base all’Activity in oggetto sfruttando il parametro passato in ogni metodo.
Aspect Oriented Programming
L’aspect oriented è un paradigma di programmazione che può essere utilizzato per
risolvere problemi complicati sfruttando solo l’object oriented. La teoria che c’è dietro
l’aspect oriented è abbastanza complessa e i concetti che la compongono non sono
pochi. In questo paragrafo saranno illustrati solo quelli indispensabili per capire come
utilizzare l’aspect oriented. Il concetto principale è quello di aspect: rappresenta un
aspetto comune a più classi non necessariamente collegate fra loro da una gerarchia.
Sfruttando un aspect, è possibile modificare il comportamento delle classi sostituendo (in parte o completamente) alcune invocazioni di metodi presenti nel bytecode
delle classi. Gli esempi classici di aspect sono il logging e la gestione delle transazioni
su un database in una applicazione Java enterprise.
Un aspect può essere definito in modo concreto sfruttando altri due concetti:
• pointcut: rappresentato solitamente da una stringa simile a una regular expression, specifica quando un aspect deve essere applicato. È possibile agire su vari
fattori per specificare quali metodi selezionare: oltre al nome del package, della
classe o del metodo, è possibile selezionare anche tutte le sottoclassi di una
certa classe o tutti i metodi con una specifica annotation;
• advice: è il codice da eseguire ogni volta che viene eseguito un metodo compatibile con il pointcut corrispondente.
Un aspect può essere di tre tipi (è possibile scegliere quale usare in base alle esigenze):
• before: l’advice viene eseguito prima del metodo originale;
• after: l’advice viene eseguito dopo il metodo originale; è possibile scegliere se
eseguirlo sempre oppure solo nel caso di terminazione corretta o con eccezioni;
20
Activity e task in background
Capitolo 1
• around: l’advice viene eseguito al posto del metodo originale. All’interno
dell’advice è possibile decidere in quali casi eseguire il metodo originale.
Per esempio, per loggare tutte le esecuzioni di uno o più metodi è possibile definire
un aspect di tipo before; per gestire le transazioni su un database è invece necessario
usare un aspect di tipo around. Un altro esempio facilmente implementabile usando
l’aspect oriented è la gestione di una cache delle chiamate a un metodo: usando un
aspect di tipo around, è possibile accedere ai parametri del metodo originale e al valore di ritorno del metodo. Mantenendo in memoria una mappa che associa i parametri
al valore di ritorno, è possibile decidere se eseguire il metodo originale o ritornare un
valore precedentemente aggiunto nella mappa.
La cosa da sottolineare in questi casi è che l’advice è solitamente indipendente dal
metodo finale a cui è applicato; per esempio, nel caso di logging, è possibile usare un
solo advice per loggare tutti i metodi definiti dal pointcut. Questo è il motivo principale
per cui viene utilizzato: permette in modo semplice di scrivere codice a comune fra
classi non legate fra loro.
L’aspect oriented si basa su una modifica del codice per aggiungere la chiamata
all’advice; questa modifica può avvenire in tre momenti diversi:
• build time: viene aggiunto uno step nel processo di build per modificare le
classi compilate;
• load time: le classi sono modificate nel momento in cui sono caricate in memoria dal class loader;
• runtime: al posto delle classi originali sono utilizzati dei proxy che si occupano
di gestire le chiamate agli advice.
Esistono varie librerie che permettono di usare l’aspect oriented in Java, ognuna delle
quali supporta una o più delle tre modalità appena viste. Per quanto riguarda lo sviluppo su piattaforma Android, la libreria più usata è AspectJ, che funziona aggiungendo
uno step nel processo di build. Dopo che il compilatore Java ha creato i file .class, viene
lanciato il compilatore AspectJ che si occupa di modificare questi file. Utilizzando il
progetto gradle-android-aspectj-plugin (https://github.com/uPhyca/gradle-androidaspectj-plugin), la configurazione del processo di build è banale, basta aggiungere alcune righe di configurazione nel file di build di Gradle:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.uphyca.gradle:gradle-android-aspectj-plugin:0.9.+'
}
21
Android | Programmazione avanzata
}
apply plugin: 'com.android.application'
apply plugin: 'android-aspectj'
A questo punto, per definire un aspect è possibile creare una nuova classe contenente
le annotation di AspectJ:
@Aspect
public class LogAspect {
@Before("execution(* android.app.Activity+.*(..))")
public void logMethod(JoinPoint joinPoint) {
Log.d("callbacks", joinPoint.getSignature().getName());
}
}
Per definire la classe come un aspect è stata utilizzata l’annotation Aspect, il pointcut è
definito utilizzando l’annotation Before. In queste poche righe di codice è stato definito
un blocco di codice (contenuto all’interno del metodo logMethod) che sarà richiamato
prima dell’esecuzione di un qualunque metodo di una sottoclasse di Activity (la regular expression passata all’annotation Before indica questo). Utilizzando il parametro
passato al metodo di tipo JoinPoint, è possibile accedere alle informazioni sul metodo
originale; da notare che il codice non dipende dalla signature del metodo originale. In
questo modo è possibile utilizzare lo stesso advice per gestire metodi di classi diverse
con signature differenti.
Per provare questa classe è necessario inserirla in un progetto Android opportunamente configurato. Mettendo un breakpoint all’interno di un metodo di una Activity e
all’interno dell’advice, è possibile verificare che prima viene invocato l’advice e successivamente il metodo originale.
Per adesso l’aspect oriented non è molto diffuso all’interno della community di sviluppatori Android; l’utilizzo probabilmente più famoso è all’interno della libreria di logging
hugo, sviluppata da Jake Wharton. In questa libreria sono sfruttati due pointcut per
identificare i metodi e i costruttori annotati con l’annotation DebugLog. L’advice è tipo
around, in quanto ha bisogno di eseguire una parte di codice prima del metodo originale per prendere il timestamp di inizio e dopo il metodo per calcolare la durata.
Flusso delle callback del ciclo di vita di una Actvity
Sono stati già analizzati i possibili metodi di callback relativi al ciclo di vita di una
Activity; in questo paragrafo saranno analizzate le sequenze più comuni in cui queste
callback possono essere invocate in base al comportamento dell’utente. Il caso più
comune (e anche il più semplice) è quello in cui una Activity viene creata per la prima
volta e visualizzata: sono richiamati, nell’ordine, i metodi onCreate, onStart e onResume. A questo punto, l’Activity è visibile e l’utente può interagire con essa; le callback
22
Activity e task in background
Capitolo 1
richiamate in seguito dipendono dall’interazione dell’utente e da altri eventi esterni
all’applicazione gestiti dal sistema operativo (per esempio, una chiamata in arrivo). In
particolare, possono esserci tre casi:
1. l’utente preme il tasto back oppure l’Activity termina spontaneamente richiamando il metodo finish: sono invocati nell’ordine i metodi onPause, onStop e onDestroy. A questo punto, l’Activity viene distrutta e il garbage collector di Java può
liberare la memoria distruggendo l’oggetto corrispondente;
2. l’Activity viene messa in background in quanto un’altra Activity viene eseguita:
questo accade spesso e può verificarsi in diversi casi, per esempio quando l’utente preme il tasto Home (anche la home di Android è una Activity), quando
viene aperta un’altra Activity della stessa applicazione, oppure quando eventi
esterni (come una chiamata in arrivo) causano l’apertura di una nuova Activity.
In questi casi viene richiamato il metodo onPause e, solo se l’Activity è coperta
totalmente dalla nuova Activity, il metodo onStop;
3. cambio di configurazione del dispositivo: avviene quando c’è un cambio di
orientation (portrait o landscape), ma anche in casi meno frequenti, per esempio quando viene aperta la tastiera fisica di un dispositivo. In questo caso l’Activity viene distrutta e viene creata una nuova istanza della stessa Activity. Le
callback richiamate sono quelle già viste: onPause, onStop e onDestroy sulla vecchia Activity e onCreate, onStart e onResume sulla nuova Activity.
Gestire i vari casi può non essere semplice; particolare attenzione va posta nel testare
il cambio di configurazione, in quanto può avvenire in qualunque momento. In questi
casi l’utente si aspetta di ritrovare l’Activity nello stesso stato in cui era in precedenza;
il prossimo paragrafo analizzerà questo aspetto.
Salvataggio dello stato di una Activity
In ogni momento su un dispositivo Android sono in esecuzione più applicazioni, ognuna delle quali può avere nel proprio stack più Activity aperte. Ogni volta, infatti, che
viene aperta una Activity, quella di partenza rimane nello stack in modo da poter essere riportata in primo piano nel caso di pressione del tasto back. In caso di necessità
di memoria, alcune applicazioni (o alcune Activity di una o più applicazioni) possono
essere terminate in automatico dal sistema operativo.
Per poter salvare il proprio stato interno è necessario sovrascrivere il metodo onSaveInstanceState della classe Activity. Questo metodo ha un parametro di tipo Bundle in cui
è possibile salvare tutti gli oggetti da ripristinare in seguito. Il restore dei parametri può
avvenire in due metodi che possono essere riscritti in una Activity:
23
Android | Programmazione avanzata
•
onCreate: il parametro passato a questa callback è un Bundle in cui sono presenti
gli oggetti precedentemente salvati. Nel caso si tratti della prima esecuzione,
questo parametro ha valore null;
• onRestoreInstanceState: callback da riscrivere per eseguire il restore, viene invocata in automatico dopo la callback onStart nel caso in cui ci sia uno stato
salvato in precedenza.
I Bundle sono molto utilizzati nelle classi del framework di Android, per esempio anche
il passaggio di parametri a una Activity avviene usando un Intent che internamente utilizza un Bundle. Un Bundle può essere visto come una mappa di coppie nomevalore; il nome è sempre una stringa, mentre il valore è un tipo primitivo Java o un
oggetto serializzabile. Il concetto di oggetto serializzabile nello sviluppo Android è un
po’ diverso da quello standard dello sviluppo Java, infatti comprende anche gli oggetti
che implementano l’interfaccia Parcelable oltre a quelli che implementano l’interfaccia
Java standard Serializable.
L’interfaccia Parcelable è stata introdotta nel framework Android per questioni di performance; infatti non fa uso di reflection per leggere e scrivere i valori dei campi di un
oggetto, ma delega il tutto a una implementazione che deve essere scritta dallo sviluppatore. La scrittura dei campi avviene nel metodo writeToParcel, mentre la creazione
di un oggetto, leggendo i dati salvati in precedenza, avviene nei metodi di un campo
statico della classe di tipo Parcelable.Creator. Un esempio di implementazione di una
classe con due campi è il seguente:
public class Person implements Parcelable {
private String name;
private String surname;
@Override public int describeContents() {
return 0;
}
@Override public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeString(surname);
}
protected void readFromParcel(Parcel in) {
name = in.readString();
surname = in.readString();
}
public static final Parcelable.Creator<Person> CREATOR =
new Parcelable.Creator<Person>() {
public Person createFromParcel(Parcel in) {
Person person = new Person();
24
Activity e task in background
Capitolo 1
person.readFromParcel(in);
return person;
}
public Person[] newArray(int size) {
return new Person[size];
}
};
//...
}
I dati sono salvati in un oggetto di tipo Parcel, i valori sono identificati dalla posizione
(non viene usata una chiave per identificarli). In questo modo, il salvataggio è molto
veloce ma è necessario porre una attenzione particolare nella scrittura di questi metodi, soprattutto in caso di classi con molti campi. Se, infatti, non viene rispettato lo
stesso ordine nella scrittura e nella lettura, ci sarà un errore runtime o un oggetto con
dati non validi (per esempio, il nome al posto del cognome).
L’implementazione vista è abbastanza semplice; il metodo readFromParcel può essere
utile nel caso di una gerarchia di classi. Per esempio, l’implementazione di una sottoclasse di quella appena vista è la seguente:
public class Worker extends Person {
private Company company;
@Override public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeParcelable(company, 0);
}
@Override protected void readFromParcel(Parcel in) {
super.readFromParcel(in);
company = in.readParcelable(getClass().getClassLoader());
}
public static final Parcelable.Creator<Worker> CREATOR =
new Parcelable.Creator<Worker>() {
public Worker createFromParcel(Parcel in) {
Worker worker = new Worker();
worker.readFromParcel(in);
return worker;
}
public Worker[] newArray(int size) {
return new Worker[size];
}
};
//...
}
25
Android | Programmazione avanzata
È possibile notare che sia nella scrittura sia nella lettura dei dati viene richiamato il
metodo della classe base; nonostante questo accorgimento, è stato comunque necessario riscrivere anche il campo statico CREATOR per poter creare oggetti della classe
derivata. La classe Company implementa a sua volta l’interfaccia Parcelable:
public class CompanyParcelable implements Parcelable {
private String name;
private CompanyParcelable() {
}
public CompanyParcelable(String name) {
this.name = name;
}
@Override public int describeContents() {
return 0;
}
@Override public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
}
public static final Parcelable.Creator<CompanyParcelable> CREATOR =
new Parcelable.Creator<CompanyParcelable>() {
public CompanyParcelable createFromParcel(Parcel in) {
CompanyParcelable company = new CompanyParcelable();
company.name = in.readString();
return company;
}
public CompanyParcelable[] newArray(int size) {
return new CompanyParcelable[size];
}
};
//...
}
Anche da questo semplice esempio si capisce che, nel caso di un numero elevato di
oggetti complessi da gestire, la scrittura manuale dei metodi può essere lunga e portare a errori difficili da identificare.
Generazione automatica delle implementazioni
di Parcelable
Esistono vari framework che permettono di evitare la scrittura manuale di tutto il codice, continuando comunque a beneficiare dei vantaggi dell’utilizzo degli oggetti Parcelable. Fra quelli disponibili, due framework molto utili che sfruttano l’annotation pro-
26
Activity e task in background
Capitolo 1
cessing sono AutoParcel e Parceler. Lo stesso esempio visto in precedenza può essere
riscritto facilmente sfruttando Parceler e aggiungendo alcune annotation:
@Parcel
public class Person {
String name;
String surname;
protected Person() {
}
public Person(String name, String surname) {
this.name = name;
this.surname = surname;
}
//...
}
@Parcel
public class Worker extends Person {
Company company;
Worker() {
super();
}
public Worker(String name, String surname, Company company) {
super(name, surname);
this.company = company;
}
//...
}
@Parcel
public class Company {
String name;
Company() {
}
public Company(String name) {
this.name = name;
}
//...
}
Ci sono due aspetti da sottolineare in questa implementazione:
1. i campi delle classi non sono più privati, ma visibili a livello di package. In questo
modo non è necessario usare la reflection per leggere e scrivere i dati; le classi
27
Android | Programmazione avanzata
generate da Parceler sono nello stesso package della classe originale e quindi
possono accedere direttamente ai campi;
2. le classi non estendono più l’interfaccia Parcelable e per questo non possono
essere incluse direttamente in un Bundle. È necessario utilizzare i metodi
statici Parcels.wrap e Parcels.unwrap prima di poter salvare e leggere gli oggetti dai Bundle.
Questi due aspetti complicano leggermente l’implementazione, ma possono essere
considerati come il prezzo da pagare per evitare di scrivere manualmente il codice di
implementazione dell’interfaccia Parcelable.
Tipi di dati da salvare in una Activity
Dopo aver visto come salvare i dati in un Bundle, l’argomento di questo paragrafo è
rappresentato dalla tipologia di informazioni che è necessario salvare. In generale, i
dati da salvare possono essere divisi in due macrocategorie: il modello dei dati della
Activity e lo stato della View.
Con “modello della Activity” si intende quello rappresentato generalmente dal model in una implementazione del pattern Model View Presenter. Purtroppo su Android
questo pattern non è utilizzato nativamente e implementarlo in modo rigoroso non è
banale (più avanti ci saranno alcuni paragrafi dedicati a questo argomento). Quello
che invece è abbastanza semplice da fare è mantenere il modello dei dati in un singolo
oggetto Parcelable e salvarlo nello stato dell’Activity.
L’altra categoria dei dati da salvare è lo stato della View; si pensi, per esempio, al testo
scritto in un campo di testo, alla posizione del cursore, al radio button selezionato e a
eventuali componenti disabilitati in base ad altre View selezionate. Alcune di queste
informazioni sono salvate in automatico dalle View, per esempio un EditText salva
automaticamente il testo inserito e la posizione del cursore. Altre informazioni, invece,
devono essere salvate manualmente, per esempio la visibilità di una View o l’essere
abilitata o disabilitata non vengono salvate in automatico.
Il salvataggio dello stato delle View avviene in automatico; questa affermazione è
vera, ma è necessario porre attenzione a due aspetti:
• lo stato viene salvato solo per le View che hanno associato un id; solitamente
questo non rappresenta un problema, in quanto le View hanno bisogno di un id
anche per essere referenziate da codice Java;
• nel caso di ListView, la posizione dello scroll viene salvata solo se l’Adapter gestisce oggetti con id stabili (per abilitare questo comportamento è necessario
sovrascrivere il metodo hasStableIds).
28
Activity e task in background
Capitolo 1
Come già detto, nei Bundle è possibile salvare solo alcuni tipi di oggetto; questa scelta è cautelativa, in quanto il Bundle potrebbe essere persistito per liberare memoria.
Tuttavia, in alcuni casi c’è la sicurezza che l’Activity sarà distrutta e subito ricreata; ciò
avviene, per esempio, in seguito a un cambio di orientation del dispositivo. In questi
casi è possibile salvare un oggetto in memoria nella vecchia Activity e recuperarlo
nella nuova. Il salvataggio avviene semplicemente mantenendo un riferimento all’oggetto; per questo motivo non è necessario che questo oggetto implementi Parcelable o
Serializable. Gli oggetti salvati in questo modo possono essere anche i thread in esecuzione; così facendo, è possibile non interrompere un thread al cambio di orientation
riagganciandolo dalla nuova Activity.
Il modo in cui utilizzare questa tecnica è cambiato con il rilascio di Android 3.0; inizialmente, infatti, era possibile usare i metodi onRetainNonConfigurationInstance e getLastNonConfigurationInstance della classe Activity rispettivamente per salvare e ricaricare
un oggetto. Con Android 3.0 questi metodi sono stati deprecati e adesso è possibile
usare un Fragment su cui è stato invocato il metodo setRetainInstance passando il valore true. In questo modo tutto il Fragment non sarà distrutto e ricreato durante il
cambio di configurazione.
Un altro metodo introdotto in Android 3.0 e collegato a questo argomento è isChangingConfigurations presente nella classe Activity. Può essere usato nelle callback onPause, onStop e onDestroy per sapere se l’Activity sta per essere distrutta e ricreata a causa
di un cambio di configurazione.
UI Thread e concorrenza
L’architettura di Android è basata sulla gestione di una coda di messaggi; a seguito
di eventi gestiti a livello hardware (per esempio, un tocco sul display, ma anche una
chiamata in arrivo), sono aggiunti messaggi alla coda che saranno poi gestiti dalle
classi del framework di Android. In questo paragrafo saranno analizzati la gestione degli eventi e il meccanismo attraverso il quale viene effettuato l’aggiornamento dell’interfaccia grafica di una applicazione in relazione a questi eventi.
La coda di messaggi è gestita utilizzando la classe MessageQueue; guardando i sorgenti
di questa classe, si nota che molti metodi sono implementati come codice nativo per
avere performance migliori. Una MessageQueue gestisce solo la struttura dati che
contiene i messaggi; per utilizzarla è necessario impiegare altre due classi:
• Looper: associa a un thread un oggetto MessageQueue; richiamando il metodo loop,
il thread attivo viene destinato a elaborare i messaggi della coda;
• Handler: accetta nel costruttore un parametro di tipo Looper e permette di inserire messaggi nella coda associata a quest’ultimo. Nel caso in cui non sia
29
Android | Programmazione avanzata
passato un Looper viene preso in automatico quello associato al thread in
esecuzione; nel caso in cui il thread non abbia un Looper associato viene lanciata una eccezione.
L’utilizzo di queste tre classi permette di gestire una serie di eventi creati in uno o più
thread e di elaborarli in modo sequenziale. Infatti un oggetto Handler può essere utilizzato in qualunque thread; indipendentemente dal thread di origine, il messaggio sarà
elaborato sempre nel thread gestito dal Looper.
Figura 1.1 - Interazione di più thread su una Message Queue condivisa attraverso l’utilizzo di Handler.
Per aggiungere un messaggio alla coda è necessario usare uno dei metodi messi a
disposizione dalla classe Handler; questi metodi possono essere raggruppati in tre tipi:
• usando il metodo dispatchMessage è possibile aggiungere un oggetto di tipo Message alla coda; solitamente questi messaggi sono elaborati (nel thread associato
al Looper) nell’implementazione del metodo handleMessage dell’oggetto Handler;
30
Activity e task in background
Capitolo 1
• attraverso i metodi post, postAtTime e postDelayed è possibile aggiungere una implementazione dell’interfaccia Runnable alla coda, specificando eventualmente
quando eseguirla;
• il metodo postAtFrontOfQueue permette di aggiungere messaggi all’inizio della
coda; anche il javadoc di questo metodo ne sconsiglia l’utilizzo, in quanto può
causare side effect difficili da gestire.
Andando a vedere i sorgenti del metodo main che viene lanciato quando l’applicazione parte (presente nella classe ActivityThread), si nota che sono presenti le seguenti
istruzioni:
{
//…
Looper.prepareMainLooper();
//…
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
Invocando il metodo prepareMainLooper, viene associato un Looper al main thread e
sarà possibile accedere in seguito a questo Looper richiamando il metodo statico getMainLooper presente nella classe Looper. Il metodo loop è quello che contiene il ciclo
infinito in cui vengono elaborati in sequenza i messaggi aggiunti alla coda; l’eccezione
in fondo al metodo non sarà mai lanciata, visto che il metodo loop non termina mai.
Il main thread è creato e gestito in modo automatico all’avvio dell’applicazione; nel
caso in cui ci sia la necessità di un ulteriore thread che elabora una coda di messaggi separata, è possibile utilizzare la classe HandlerThread. Utilizzando questa classe, è
possibile creare in poche righe di codice un thread separato in cui eseguire più operazioni in sequenza. Per esempio, è possibile usarlo per creare un thread che esegue
un Runnable che schedula automaticamente una nuova esecuzione dopo dieci secondi:
HandlerThread handlerThread = new HandlerThread("myHandlerThread");
handlerThread.start();
final Handler handler = new Handler(handlerThread.getLooper());
handler.post(new Runnable() {
@Override public void run() {
//...
handler.postDelayed(this, 10000);
}
});
I messaggi inseriti nella coda gestita dal main Looper sono di diversi tipi. Ci sono quelli
legati al ciclo di vita delle Activity, quelli legati alla gestione a basso livello delle peri-
31
Android | Programmazione avanzata
feriche hardware e quelli legati in modo più stretto all’interfaccia grafica (per esempio,
gli eventi touch). Da notare il fatto che non esiste una corrispondenza uno a uno fra i
messaggi inseriti nella coda e le callback del ciclo di vita di una Activity. Per esempio,
in corrispondenza del messaggio con id LAUNCH_ACTIVITY vengono richiamate le callback di onCreate, onStart e onResume. Il cambio di orientation viene gestito interamente
da un messaggio con id RELAUNCH_ACTIVITY; essendo un messaggio unico, vi è la certezza che sia le callback di distruzione dell’Activity esistente sia quelle di creazione della
nuova Activity siano eseguite in sequenza.
Il main thread viene spesso chiamato anche ui thread, in quanto tutte le chiamate per aggiornare l’interfaccia grafica devono essere effettuate da questo thread.
In realtà questa definizione non è del tutto corretta, perché nel main thread sono
invocate le callback di altri componenti base di Android, in particolare i Service e
i Broadcast Receiver. La gestione dei Service e dei thread su cui sono eseguiti non
è semplice come può sembrare; questo argomento sarà approfondito nei prossimi
paragrafi di questo capitolo.
La gestione degli eventi con una coda di messaggi è una caratteristica comune con
altri ambienti in cui è necessario gestire una interfaccia grafica. Usando questo pattern, c’è la sicurezza che non ci siano due thread in parallelo che aggiornano l’interfaccia grafica; in questo modo le classi che rappresentano le View possono non
essere thread safe. Inoltre, nel caso di aggiornamenti in conflitto fra loro, ci sarà
sempre una situazione consistente in quanto non ci saranno race conditions fra i
vari aggiornamenti. Nel codice è possibile trarre vantaggio da questa situazione, per
esempio non è necessario sincronizzare l’accesso alle variabili che sono utilizzate
sempre e solo nel main thread.
Anche tutti i listener sull’interfaccia grafica (per esempio, i click listener) sono eseguiti
nel main thread. Da un lato c’è la sicurezza che l’esecuzione del metodo non sarà in
contemporanea ad altri listener, dall’altro è necessario porre attenzione per non occupare il main thread per troppo tempo. Infatti, se un listener esegue direttamente
un task lungo, non saranno presi in carico altri listener e non sarà neanche eseguito
l’aggiornamento dell’interfaccia utente. Per questo motivo, se il main thread è occupato per più di 5 secondi (10 nel caso di broadcast receiver), verrà mostrato il dialog di
Application Not Responding (chiamato anche ANR), che permette all’utente di terminare l’applicazione. Anche se un listener esegue un’operazione molto più breve di
5 secondi, ma comunque non immediata, è bene spostare l’esecuzione in un thread in
background. Infatti si ha già la percezione di lentezza di una applicazione se non c’è un
feedback dopo 100-200 millisecondi.
Il modo più semplice per eseguire una operazione in background è rappresentato dalla
creazione manuale di un nuovo thread fatto partire attraverso il metodo start:
32
Activity e task in background
Capitolo 1
new Thread() {
@Override public void run() {
runBackgroundTask();
runOnUiThread(new Runnable() {
@Override public void run() {
updateUi();
}
});
}
}.start();
Se durante l’esecuzione in background vi è la necessità di aggiornare l’interfaccia grafica, è necessario eseguire questo aggiornamento nel main thread. Per fare questo è
possibile invocare il metodo runOnUiThread della classe Activity passando un oggetto
Runnable. Usando questo metodo, viene aggiunto un messaggio nella coda di messaggi
gestita dal main Looper; in questo modo si ha la sicurezza che l’aggiornamento avvenga nel thread corretto.
Questo esempio di task in background creando un nuovo thread è corretto, ma presenta comunque diversi problemi:
• a ogni esecuzione viene creato un nuovo thread; gestendo un thread pool si
avrebbe un risparmio della memoria utilizzata;
• non è legato al ciclo di vita dell’Activity che lo contiene; per questo motivo l’aggiornamento dell’interfaccia grafica viene eseguito anche se durante l’esecuzione del thread l’Activity è stata distrutta.
Nei prossimi paragrafi saranno analizzate varie tecniche utilizzabili in ambiente Android per eseguire correttamente e in modo efficiente task in background.
Tipologie di task in background
Dopo aver visto il ciclo di vita delle Activity e la gestione dei task in background, in
questo paragrafo e nei prossimi sarà analizzata la relazione fra questi due concetti. Per
prima cosa, è possibile dividere in due macrocategorie i possibili task eseguibili in una
applicazione Android collegati all’aggiornamento dell’interfaccia grafica. Prendendo
in prestito la terminologia dei servizi REST, è possibile definire le seguenti categorie di
task asincroni:
• GET: caricamento dei dati da una sorgente dati (locale o remota); viene eseguita solo una lettura e nessuna operazione di modifica;
• POST: chiamata per modificare i dati da una sorgente.
Una delle differenze fra questi tipi di task risiede nel fatto che i task di tipo GET sono
idempotenti, possono essere richiamati più volte senza avere side effect.
33
Android | Programmazione avanzata
Nel caso di esecuzione normale di un task (ovvero di una esecuzione senza interruzioni), i due tipi sono equivalenti, vengono eseguiti in un thread in background e passano
i dati al thread principale per aggiornare l’interfaccia grafica. La situazione si complica
quando, durante un task in background, l’Activity che ha lanciato il task viene messa in
pausa. Abbiamo visto che questo può succedere in tre casi:
• l’utente preme back per uscire dall’Activity: un task di tipo GET può essere cancellato senza problemi (oppure il risultato può essere ignorato). Nel caso di task
di tipo POST, il comportamento dipende da come è pensata l’interfaccia; se è
stata mostrata una finestra di dialogo modale per mostrare il caricamento, il task
sarà annullato e per uscire dall’Activity sarà necessaria un’ulteriore pressione del
tasto back. Nel caso in cui non venga mostrata una finestra di dialogo, l’utente si
aspetterebbe un annullamento del task, ma la situazione non è così scontata (se,
per esempio, è già partita la chiamata a un server e sta per arrivare la risposta, il
task può essere annullato in locale ma è già stato eseguito in remoto);
• un’altra Activity viene aperta in foreground: in questo caso un task di tipo POST
deve essere terminato in modo che, alla successiva apertura dell’Activity, l’utente possa vedere il risultato. Per i task di tipo GET la soluzione scelta dipende
da vari fattori; per esempio, se i dati ritornati dal task cambiano spesso è possibile annullare la chiamata in modo che, quando l’utente rientra sull’Activity,
si troverà i dati aggiornati. Non è comunque sbagliato far finire la chiamata e
aggiornare l’interfaccia grafica con i nuovi dati;
• cambio di configurazione: come già detto, in questi casi l’Activity viene distrutta e ricreata; questo comportamento è tuttavia un dettaglio implementativo.
L’utente si aspetta di vedere il task portato a termine nonostante il cambio di
orientation. Per i task di tipo GET non rappresenta un errore grave il riavvio al
cambio di orientation (al massimo l’utente vedrà una attesa più lunga); può
portare invece a inconsistenze dei dati nel caso di task di tipo POST.
AsyncTask e Loader
La classe AsyncTask, disponibile fin dalle prime versioni di Android, è stata pensata per
eseguire operazioni in background che hanno bisogno di aggiornare l’interfaccia grafica o di eseguire altre operazioni sul thread principale. L’utilizzo non è banale; ci sono
tre parametri generics da definire nel momento della creazione: la classe corrispondente ai parametri iniziali, ai risultati intermedi e al risultato finale. C’è solo un metodo
astratto da riscrivere obbligatoriamente, che si occupa di eseguire il task in un thread
in background; altre callback possono essere riscritte per gestire il risultato finale, la
cancellazione del task ed eventuali risultati intermedi. I thread vengono gestiti auto-
34
Activity e task in background
Capitolo 1
maticamente da questa classe, non è quindi necessario gestirli manualmente; ogni
metodo verrà eseguito nel thread corretto.
L’utilizzo della classe AsyncTask è stato al centro di molte critiche e adesso viene sconsigliato da molti sviluppatori. Il problema principale è rappresentato dal ciclo di vita
dell’Activity, che fa partire un AsyncTask; non c’è, infatti, un legame fra i due oggetti,
pertanto il ciclo di vita deve essere gestito manualmente. Per questo motivo possono
esserci problemi nel caso in cui l’Activity sia distrutta (per esempio, per un cambio di
orientation) durante il task in background. Una soluzione possibile è quella vista nei
paragrafi precedenti: l’utilizzo di un Fragment che non viene distrutto al cambio di
configurazione e in cui memorizzare l’AsyncTask.
I thread sono gestiti in automatico all’interno della classe AsyncTask; la gestione non è,
tuttavia, così scontata, e cambia in base alla versione di Android utilizzata. Infatti fino ad
Android 1.5 il thread in background era uno solo e, per questo motivo, due task lanciati in
sequenza non erano mai eseguiti in parallelo. A partire da Android 1.6 e fino ad Android
3.0, i thread utilizzati erano più di uno e quindi più task potevano essere eseguiti parallelamente. Da Android 3.0 è tornata l’esecuzione in un singolo thread in background,
probabilmente per evitare problemi di concorrenza. È tuttavia possibile tornare al comportamento precedente utilizzando il metodo executeOnExecutor al posto di execute. Nel
caso in cui si utilizzi un AsyncTask, è bene tenere presente questo aspetto: l’esecuzione
in un solo thread in background può essere utile in alcuni casi e limitativa in altri.
Nella release di Android 3.0 è stata introdotta la classe Loader per cercare di risolvere
i problemi collegati all’utilizzo degli AsyncTask. Un Loader è collegato strettamente
al ciclo di vita dell’Activity che lo contiene e gestisce in automatico il cambio di configurazione. Nonostante questo pregio non da poco, i Loader non sono mai diventati
il modo standard per eseguire un task in background: l’utilizzo è abbastanza macchinoso e il codice da scrivere per gestirli non è poco. L’utilizzo dei Loader è comunque
consigliato nel caso in cui ci sia la necessità di gestire un ContentProvider che ritorna
una lista di dati mostrati in una ListView. Utilizzando un oggetto di tipo CursorLoader,
è possibile agganciarsi facilmente a un ContentProvider e avere in modo automatico
e trasparente gli aggiornamenti dei dati. In questo modo l’interfaccia grafica sarà aggiornata automaticamente ogni volta che sarà necessario senza dover scrivere manualmente il codice di sincronizzazione.
IntentService e LocalBroadcastManager
Sono disponibili varie soluzioni per eseguire un task in background; in questo paragrafo sarà mostrata una alternativa agli AsyncTask e ai Loader in cui non sono utilizzate
librerie di terze parti ma solo classi standard della piattaforma Android.
35
Android | Programmazione avanzata
Il difetto principale degli AsyncTask è quello di non essere legati al ciclo di vita dell’Activity; per non avere questo problema, è necessario avere un legame meno forte fra
il task in background e l’Activity. Per questo motivo dal task in background non deve
essere richiamato direttamente un metodo dell’Activity; è necessario aggiungere un
oggetto che gestisce la comunicazione. Da un lato sarà possibile inviare messaggi
usando questo oggetto, dall’altro i messaggi saranno recapitati agli oggetti che ne fanno esplicita richiesta.
Questo scambio di messaggi esiste già in Android ed è rappresentato dai messaggi di
tipo broadcast; gli oggetti che stanno in ascolto sui vari messaggi sono implementazioni della classe BroadcastReceiver. Molti componenti standard di Android si basano
su questa classe; per esempio, per essere informati del cambio del livello della batteria
del dispositivo è possibile registrare opportunamente un BroadcastReceiver nel manifest della propria applicazione.
I BroadcastReceiver sono globali all’interno del dispositivo; qualunque applicazione
può inviare messaggi e stare in ascolto per le varie tipologie. Nel caso di un task in
background relativo a una singola applicazione, può risultare un problema avere i dati
pubblici; per evitarlo, è possibile sfruttare la classe LocalBroadcastManager presente nella support library. L’utilizzo è abbastanza semplice; all’interno di una Activity (o di un
Fragment) è possibile registrare un Receiver per stare in ascolto a determinati messaggi scelti in base all’oggetto IntentFilter passato:
LocalBroadcastManager.getInstance(this).registerReceiver(receiver,
new IntentFilter(DownloaderService.EVENT_NAME));
La registrazione può essere effettuata all’interno di uno dei metodi di callback del
componente: onCreate, onStart o onResume. A seconda del metodo scelto, si avranno notifiche solo quando il componente è visibile (completamente visibile usando onResume
o anche in parte usando onStart) oppure anche quando il componente è in background
(usando onCreate). Nel metodo corrispondente (onDestroy, onStop o onPause) è importante aggiungere la chiamata per annullare la registrazione:
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
Il Receiver viene solitamente memorizzato in un campo della classe; una possibile
implementazione è la seguente:
private final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
updateProgress(intent.getExtras().getInt("progress"));
}
};
36
Activity e task in background
Capitolo 1
Quando è necessario eseguire un task non legato all’interfaccia grafica su Android,
è possibile utilizzare la classe Service. Un Service è uno dei componenti standard
di Android, viene gestito dal sistema operativo e può essere fatto partire usando
un Intent con i parametri da passare. Usare l’implementazione base di Service non
è comunque la soluzione corretta al problema affrontato in questo paragrafo, in
quanto il Service viene eseguito nel thread principale (bloccando quindi l’interfaccia grafica). Per risolvere questo problema, la soluzione più semplice consiste
nell’estendere la classe IntentService, all’interno della quale viene gestito un oggetto HandlerThread per fare in modo che il codice sia eseguito automaticamente in
un thread in background.
Il ciclo di vita di un IntentService è gestito dal sistema operativo; nel caso in cui ci
siano più invocazioni in parallelo, viene riusato lo stesso oggetto. Così come per le
Activity, anche in questo caso sarà il sistema operativo che si occuperà di distruggere l’oggetto in automatico nel caso in cui ci sia necessità di memoria. L’esecuzione di
più invocazioni parallele è gestita in sequenza da un IntentService; questo comportamento è dovuto all’utilizzo di un HandlerThread per gestire una coda di messaggi
e al fatto che tutte le invocazioni siano gestite da un solo oggetto.
Una possibile implementazione di un IntentService è la seguente:
public class DownloaderService extends IntentService {
public static final String EVENT_NAME = "download";
public static final String PROGRESS = "progress";
public DownloaderService() {
super("DownloaderService");
}
@Override protected void onHandleIntent(Intent intent) {
for (int i = 0; i < 10; i++) {
//...
Intent resIntent = new Intent(EVENT_NAME);
resIntent.putExtra(PROGRESS, i + 1);
LocalBroadcastManager.getInstance(this).sendBroadcast(resIntent);
}
}
}
Il task effettivo è all’interno del metodo onHandleIntent; l’esecuzione di questo metodo
avverrà in automatico in un thread in background. All’interno di questo metodo sono
solitamente presenti una o più chiamate al metodo sendBroadcast del LocalBroadcastManager per inviare messaggi contenenti informazioni da visualizzare nell’interfaccia
grafica. Sia Activity sia IntentService sono sottoclassi di Context, per questo è possibile
creare un LocalBroadcastManager passando come parametro this in entrambi i casi.
37
Android | Programmazione avanzata
Per invocare l’esecuzione del Service dall’Activity di partenza è possibile utilizzare il
metodo startService passando un opportuno Intent:
startService(new Intent(this, DownloaderService.class));
Figura 1.2 - Gestione di un task in background usando un IntentService.
Utilizzando questa implementazione, viene gestito correttamente il cambio di configurazione: se durante l’esecuzione dell’IntentService c’è un cambio di orientation,
il risultato sarà inoltrato correttamente alla nuova Activity. Il tutto funziona perché
dal task in background non è presente un legame all’Activity, ma viene inviato solo un
messaggio di broadcast. Nel caso di un cambio di configurazione saranno eseguiti i
seguenti passaggi:
1. l’istanza A1 dell’Activity A si registra per ricevere messaggi di un certo tipo;
2. l’istanza A1 fa partire un service in background;
3. a causa di un cambio di configurazione, l’istanza A1 viene distrutta; prima della
distruzione si deregistra dal Broadcast Manager;
4. a causa del cambio di configurazione, viene creata una nuova istanza A2 dell’Activity A; durante la creazione A2 si registra per ricevere i messaggi;
5. alla fine del task in background l’IntentService manda un messaggio utilizzando il Broadcast Manager; questo messaggio sarà recapitato correttamente
all’Activity A2.
38
Activity e task in background
Capitolo 1
Parlando delle classi Looper e MessageQueue, è stato sottolineato il fatto che la distruzione e la successiva creazione di una Activity nel caso di cambio di orientation
avviene nello stesso messaggio sul thread principale. Adesso è possibile sfruttare
questa caratteristica: gli step 3 e 4 saranno eseguiti sempre contemporaneamente, e quindi un messaggio broadcast proveniente da un IntentService sarà sempre
recapitato a una delle due Activity. Ovviamente, per non perdere informazioni, nel
caso in cui questo messaggio sia gestito dall’Activity prima del cambio di orientation
dovrà essere gestito il salvataggio dello stato dell’Activity come detto nei paragrafi
dedicati a questo argomento.
Figura 1.3 - Gestione di un task in background usando un IntentService nel caso di un cambio di configurazione durante l’esecuzione del task.
Nell’esempio visto, non è gestito nel modo corretto il caso in cui l’utente esca dall’applicazione premendo il tasto home e rientri successivamente quando il task è terminato. In questo caso, infatti, quando il task finisce viene inviato normalmente un messaggio, ma, non essendoci nessuna Activity registrata, tale messaggio non viene inoltrato.
Un IntentService è un componente gestito direttamente da Android; utilizzandolo, c’è
un vantaggio legato al ciclo di vita del processo corrispondente a una applicazione. Nel
caso, infatti, in cui sia necessario liberare memoria, Android si occuperà non solo di
distruggere le Activity in background (salvando lo stato in modo da poterle ripristina-
39
Android | Programmazione avanzata
re), ma anche di terminare i processi relativi ad alcune applicazioni in esecuzione. La
priorità in base alla quale vengono scelti i processi da terminare è calcolata in base alle
Activity in esecuzione, a quelle in background e al numero di Service e BroadcastReceiver attivi. Il processo corrispondente a una applicazione con Activity in esecuzione
molto probabilmente non sarà mai terminato; nel caso in cui non ci siano Activity
attive, invece, la probabilità è molto più alta. Per questo motivo, nel caso in cui ci sia
la necessità di eseguire un task complesso in background è preferibile utilizzare un
IntentService al posto di un semplice thread in background. Così facendo, il sistema
operativo è informato del fatto che l’applicazione sta eseguendo un task e quindi il
corrispondente processo sarà terminato solo in casi estremi di necessità di memoria.
Come già detto, la classe IntentService è una sottoclasse di Context; per questo motivo è possibile accedere facilmente a tutti i servizi messi a disposizione di Android. A
differenza di un AsyncTask (in cui l’oggetto Context a disposizione è una Activity che
può essere distrutta), nel caso di un IntentService c’è la sicurezza che il Context sia
valido per tutta la durata del task da eseguire. Tuttavia, anche nel caso in cui si usi un
AsyncTask (o altri metodi mostrati nei prossimi paragrafi), è possibile sfruttare l’ApplicationContext al posto dell’Activity per avere accesso a un oggetto Context che rimarrà
valido per tutto il tempo necessario. In molti casi, infatti, non ci sono differenze fra
usare l’ApplicationContext o un IntentService: in entrambi i casi non è possibile fare
riferimento a oggetti relativi all’interfaccia utente, ma è comunque possibile accedere
alle SharedPreferences e a eventuali database.
La soluzione vista, basata sulle classi IntentService e LocalBroadcastManager, permette
di gestire tutte le problematiche in modo corretto. Tuttavia, ci sono vari aspetti da
sottolineare che possono essere migliorati utilizzando librerie esterne alla piattaforma
Android:
• il codice visto in questo paragrafo è abbastanza verboso; sia per la registrazione
sia per inviare messaggi è necessario creare oggetti non legati al task da eseguire (come, per esempio, Intent e IntentFilter);
• gli oggetti contenuti nei messaggi creati dal service devono essere inseriti
all’interno di un Intent; per questo motivo devono essere tipi primitivi oppure implementare Parcelable o Serializable. In realtà questi oggetti saranno
passati immediatamente al destinatario del messaggio e quindi non saranno
mai serializzati;
• la classe IntentService deve essere definita nel manifest dell’applicazione, in
caso contrario si ha un errore runtime;
• molte classi di Android devono essere utilizzate per l’implementazione; questo
è vantaggioso per alcuni aspetti, ma è anche uno svantaggio per altri. Infatti il
codice scritto non può essere utilizzato in altri contesti ed è più complicato da
40
Activity e task in background
Capitolo 1
testare (questo argomento sarà affrontato più approfonditamente nel capitolo
dedicato alla qualità del codice);
• nel caso in cui ci siano più chiamate di un IntentService, l’esecuzione non avverrà mai in parallelo, ma sempre in modo sequenziale. Anche questo aspetto può
essere sia positivo sia negativo a seconda dei contesti di utilizzo; comunque una
soluzione in cui ci fosse la possibilità di scegliere il tipo di esecuzione sarebbe
sicuramente più flessibile e quindi preferibile.
EventBus ed Executor
Utilizzando solo le classi standard Java, il modo più semplice per creare un thread in
background è rappresentato dall’utilizzo della classe Thread; questo modo è immediato, ma non è certamente il migliore. Infatti, creando un nuovo thread ogni volta si ha
uno spreco di risorse; sarebbe opportuno mantenere un pool di thread e riutilizzarli
all’occorrenza. Per risolvere questa e altre problematiche, con il rilascio di Java 1.5 è
stato introdotto il package java.util.concurrent che contiene varie classi e interfacce
per gestire tutti gli aspetti di concorrenza. L’interfaccia che interessa in questo momento è Executor; essa contiene un solo metodo execute che può essere utilizzato per
eseguire un oggetto Runnable. In quale thread sarà eseguito dipende dall’implementazione di Executor scelta; nella classe Executors sono presenti alcuni metodi statici per
creare i diversi tipi di Executor disponibili. I principali metodi sono i seguenti:
• newSingleThreadExecutor: thread pool contenente un solo thread. Tutti i Runnable
sono eseguiti in sequenza; nel caso in cui il thread termini per un errore, viene
creato in automatico un nuovo thread;
• newFixedThreadPool: thread pool di dimensione fissa (passata come parametro).
Nel caso in cui tutti i thread siano occupati e si vogliano eseguire ulteriori task,
viene gestita in automatico una coda;
• newCachedThreadPool: thread pool in cui i vari thread sono tenuti in una cache
e riutilizzati in base alle esigenze. Usando questo metodo, non ci sarà né un
numero minimo di thread da mantenere nella cache né un numero massimo di
thread gestibili; un singolo thread sarà distrutto se non usato per 60 secondi.
Creando manualmente un oggetto ThreadPoolExecutor è possibile configurare
questi parametri in base alle esigenze.
Nello sviluppo di una applicazione Android solitamente un Executor viene salvato in
una variabile statica o in un singleton. In questo esempio è possibile prendere spunto
dal codice sorgente della classe AsyncTask e creare un pool di thread di dimensione
minima e massima proporzionale al numero di processori disponibili sul dispositivo:
41
Android | Programmazione avanzata
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static Executor EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
10, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
Dopodiché è possibile utilizzarlo per eseguire un task in background passando un parametro di tipo Runnable:
executor.execute(new Runnable() {
@Override public void run() {
Downloader.getInstance().download();
}
});
Rispetto all’esempio precedente di utilizzo di un oggetto di tipo IntentService, in questo
caso la chiamata per eseguire il task in background avviene direttamente; non è necessario passare da un Intent, ma è possibile invocare direttamente la classe interessata.
Per avere i risultati nel thread principale invece di un LocalBroadcastManager è possibile
utilizzare, in questo caso, un event bus. Il concetto di bus è usato in vari modi nello
sviluppo di un software, per esempio in alcuni progetti (solitamente lato server) viene
utilizzato un Enterprise Service Bus per far comunicare i vari componenti. Il concetto
di bus è abbastanza semplice: gestisce una coda di messaggi in cui alcuni produttori
inviano i messaggi sul bus e alcuni consumatori li elaborano. Il vantaggio dell’utilizzo di
un bus è dato dal disaccoppiamento fra chi scrive e chi legge, infatti al momento della
scrittura non è necessario sapere chi elaborerà il messaggio (potrebbe essere elaborato anche da più di un consumatore) e, in alcuni casi complessi, quando sarà elaborato.
Per lo sviluppo di una applicazione Android è possibile utilizzare due librerie che
implementano un event bus: EventBus (https://github.com/greenrobot/EventBus),
sviluppata da green robot, e Otto (https://github.com/square/otto), sviluppata da
Square. EventBus ha alcune feature aggiuntive rispetto a Otto, che è, per scelta progettuale, molto semplice. Utilizzando EventBus, infatti, è possibile specificare anche
in quale thread sarà elaborato un messaggio, mentre usando Otto l’elaborazione
di un messaggio avviene sempre in modo sincrono nel thread in cui il messaggio è
stato inviato al bus. Tuttavia, utilizzando Otto è possibile scrivere una semplice sottoclasse di Bus che permette di forzare l’elaborazione dei messaggi nel main thread
di Android: ogni volta che un messaggio viene postato viene controllato il thread e,
nel caso in cui non sia il main thread, viene utilizzato un oggetto di tipo Looper per
eseguirlo sul main thread:
42
Activity e task in background
Capitolo 1
public class MainThreadBus extends Bus {
public static Bus bus = new MainThreadBus();
private final Handler handler = new Handler(Looper.getMainLooper());
public MainThreadBus() {
super(ThreadEnforcer.ANY);
}
@Override public void post(final Object event) {
if (Looper.myLooper() == Looper.getMainLooper()) {
super.post(event);
} else {
handler.post(new Runnable() {
@Override
public void run() {
MainThreadBus.super.post(event);
}
});
}
}
}
Questa implementazione è molto utile nell’architettura introdotta in questo paragrafo:
tramite un Executor un task viene eseguito in un thread in background che, durante
l’esecuzione o al termine di essa, sfrutta il bus per trasferire i risultati sul main thread
in modo da poter aggiornare l’interfaccia grafica. Da un task in background è possibile
inserire un messaggio nel bus invocando il metodo post:
public void download() {
for (int i = 0; i < 10; i++) {
progress = i;
//…
MainThreadBus.bus.post(i + 1);
}
}
È stato analizzato l’utilizzo del bus dal punto di vista del produttore dei messaggi;
resta ancora da vedere come è possibile registrare gli oggetti che verranno invocati
automaticamente per elaborare i messaggi. Il metodo da utilizzare è register, che può
essere invocato in un metodo di callback dell’Activity o del Fragment per registrare un
listener. Anche in questo caso valgono le stesse considerazioni fatte per la registrazione su un LocalBroadcastReceiver:
• è possibile scegliere quale callback utilizzare in base alle esigenze;
• cancellando la registrazione nella callback corrispondente (utilizzando, in
questo caso, il metodo unregister), si ottiene la gestione corretta del cambio
di orientation.
43
Android | Programmazione avanzata
Una volta che un oggetto è registrato sul bus, è necessario specificare quali metodi
devono essere invocati dal bus e su quali eventi l’oggetto è in ascolto. Il tutto avviene
molto semplicemente aggiungendo una annotation su un metodo:
• usando l’annotation Subscribe è possibile dichiarare che un metodo è un listener
che sarà invocato dal bus;
• un metodo annotato con Subscribe sarà invocato dal bus per gli eventi che sono
compatibili con il parametro definito nella signature del metodo (deve esserci
obbligatoriamente un solo parametro).
Per esempio, il seguente metodo sarà invocato automaticamente per aggiornare l’interfaccia grafica a seguito dell’invio nel bus di un messaggio di tipo Integer eseguito
dall’oggetto Downloader visto all’inizio di questo paragrafo:
@Subscribe public void updateProgress(Integer progressStep) {
if (progressStep == 10) {
goButton.setEnabled(true);
progress.setVisibility(View.GONE);
} else {
goButton.setEnabled(false);
progress.setVisibility(View.VISIBLE);
progress.setProgress(progressStep);
}
}
Un metodo annotato con Subscribe può avere un parametro di qualunque tipo; utilizzando Otto, è presente la restrizione per cui il parametro non può essere corrispondente a una interfaccia Java ma deve essere una classe. Un aspetto importante da
sottolineare è che il matching avviene considerando anche le sottoclassi; per questo
motivo, dichiarando un parametro di tipo Object, è possibile registrare un listener che
sarà invocato per tutti i messaggi gestiti dal bus. In questo modo aggiungere un log per
registrare tutti i messaggi è molto semplice; in altri casi è possibile sfruttare una classe
base a comune di più oggetti per non avere una duplicazione del codice di un listener.
In alcuni casi può essere utile fornire immediatamente agli oggetti che si registrano al
bus un valore iniziale; per questo motivo Otto mette a disposizione l’annotation Produce. Un metodo annotato in questo modo sarà richiamato automaticamente ogni volta
che un oggetto effettua la registrazione, il valore ritornato dal metodo sarà passato
al corrispondente metodo annotato con Subscribe. Anche in questo caso il matching
viene effettuato in base al tipo dichiarato nella signature del metodo. Gli oggetti che
producono valori da inserire nel bus devono essere registrati usando gli stessi metodi
visti in precedenza. Nell’esempio visto è possibile aggiungere il seguente metodo nella
classe Downloader:
44
Activity e task in background
Capitolo 1
@Produce public Integer getProgress() {
return progress + 1;
}
In questo modo viene risolto il problema che si presenta nel caso in cui il task in
background termini quando l’Activity è in background. In questo caso, infatti, quando
l’Activity effettua la registrazione sul bus nella callback onResume riceve immediatamente un evento con l’ultimo valore emesso in modo da poter aggiornare correttamente l’interfaccia grafica.
Sostituendo l’utilizzo di un LocalBroadcastManager con un Executor e un event bus, alcuni dei problemi visti nel paragrafo precedente sono risolti, in particolare:
• il codice da scrivere è più semplice;
• i parametri dei metodi sono passati gestiti interamente in memoria, non è necessario che siano implementazioni di Parcelable o Serializable;
• non è necessario aggiungere nessuna configurazione nel manifest;
• non utilizzando classi del framework Android, il testing del codice è più semplice;
• in base all’Executor utilizzato, è possibile decidere se far eseguire più chiamate
in parallelo o serializzare le varie esecuzioni.
Conclusioni
Gestire i task in background rispettando il ciclo di vita di una Activity non è un problema banale da risolvere. In questo capitolo sono state esposte varie tecniche per
gestire correttamente questa problematica.
Una soluzione alternativa per eseguire i task in background è rappresentata dal
framework RxJava. Questo framework si sta diffondendo velocemente fra gli sviluppatori Android, in quanto, fra le altre cose, permette di gestire task complessi
combinandoli fra loro. Questo sarà l’argomento del prossimo capitolo, che si occuperà di questo framework e di altri aspetti legati alla programmazione funzionale
su piattaforma Android.
45