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