Content Provider
In Android i Content Provider sono utilizzati per disaccoppiare il layer di persistenza dal layer applicativo. Ogni applicazione ha accesso ad un database privato, usando i Content Provider è possibile invece condividere i dati con altre applicazioni, ed è inoltre possibile eseguire query asincrone su di essi per ottenere un'applicazione responsive. Android ha diversi Content Provider built-in utilizzabili dagli sviluppatori, ad esempio Media Store, Contacts, Calendar e Call Log. È inoltre possibile definire nuovi content provider.
PicturesProvider
Per l'applicazione ho realizzato un provider per memorizzare e ottenere i dati relativi alle foto scattate dall'utente. Il provider al suo interno utilizza un database SQLite gestito estendendo la classe di SQLiteOpenHelper fornita da Android.
Il database ha una singola tabella che memorizza il path su filesystem dell'immagine, la descrizione inserita dall'utente, le coordinate geografiche ed un id univoco.
È possibile interrogare il provider per ottenere tutte le immagini attualmente memorizzate attraverso l'url content://com.danielecampogiani.picturesprovider/pictures, mentre se si desidera ottenere informazioni su una specifica foto è sufficiente inserire in fondo all'url 'id della foto : content://com.danielecampogiani.picturesprovider/pictures/id.
Dopo aver fatto un match sull'url richiesto al provider (per discriminare se sono richieste tutte le immagini, o una specifica) il content provider delega il reperimento dei dati al database, e nel caso di modifiche notifica i Content Resolver permettendo così di ottenere delle callback lato applicativo senza dover rieseguire query (come spiegato più avanti).
Activity
Una Activity rappresenta una schermata mostrata ad un utente solitamente occupando tutto lo schermo del dispositivo, per creare un'attività è sufficiente estendere la classe Activity e definire i metodi di callback richiamati dal sistema Android per gestire il ciclo di vita dell'attività.
MainActivity
Per l'applicazione ho creato una sola Activity, poiché utilizzo all'intero di essa diversi Fragment che vengono automaticamente mostrati/nascosti utilizzando un' actionBar a tabs.
Per gestire le transazioni di Fragment ho creato la classe TabListener che si occupa di instanziare quando necessario il fragment che gestisce e di aggiungerlo e rimuoverlo dallo schermo utilizzando FragmentTransaction quando l'utente seleziona il tab relativo.
Ho inoltre aggiunto un menu alla actionBar con un pulsante che quando selezionato crea l'intent Mediastore.Action_Image_Capture specificando un uri negli extra dell'intent poi “lancia” questo intent aspettando la restituzione del controllo con il metodo startActivityForResult. Così facendo verrà lanciata l'applicazione scelta dall'utente per scattare fotografie, e una volta che la foto è stata fatta sarà memorizzata nell'uri specificato negli extra dell'intent e il controllo tornerà all'attività MainActivity.
Ogni qual volta possibile ho eseguito le operazioni all'interno di AsyncTask, questo permette di eseguire codice in background, senza quindi utilizzare il thread dedicato alla UI, e sincronizzare l'interfaccia grafica al termine della loro esecuzione.
La classe AsyncTask si occupa della creazione, gestione e sincronizzazione di thread, per cui lo sviluppatore si deve preoccupare solamente di scrivere la logica applicativa.
Nello specifico ho utilizzato SavePictureAsyncTask per leggere le coordinate geografiche dall'immagine restituita dall'applicazione fotocamera e per memorizzare l'immagine nel content provider, e NewPictureAsyncTask per le operazioni di I/O necessarie per creare il nuovo file sul filesystem.
Fragments
I fragment vengono utilizzati per creare UI dinamiche e flessibili che si adattano a diversi schermi. Ogni fragment è un componente a se stante (con un suo ciclo di vita) utilizzabile da diverse activity. Per creare un nuovo fragment è sufficiente estendere la classe Fragment.
TimelineFragment
È il fragment collegato al tab “Timeline” e mostra in una lista (estende la classe ListFragment) le foto scattate in ordine cronologico. Sotto ogni foto è presente la descrizione e due pulsanti, uno per cancellare la foto e uno per condividerla.
Per ottenere le foto da mostrare utilizzo un loader di cursor poiché le operazioni su database possono essere time-consuming.
I loaders sono un meccanismo per caricare dati in maniera asincrona e per monitorare le sorgenti di dati. Possono essere utilizzati per qualunque tipo di dato, nell'applicazione li ho utilizzati per caricare dei cursor ossia il risultato di query su database in Android.
Per ottenere un comportamento asincrono all'interno del fragment ho implementato l'interfaccia LoaderManager.LoaderCallbacks<Cursor> realizzando i metodi:
-
onCreateLoader: responsabile di creare il loader (usando la classe CursorLoader di Android) specificando i parametri della query desiderata.
-
onLoadFinished: richiamato quando la query asincrona restituisce dei risultati che vengono passati come parametro in questo metodo. Il metodo è invocato anche ogni qual volta cambiano i dati di interesse per la query specificata nel metodo onCreateLoader.
-
onLoaderReset: per resettare il loader.
Per lo specifico fragment, il loader creato interroga il PicturesProvider richiedendo l'id, la descrizione e il path su filesystem di ogni foto.
Nel metodo onLoadFinished aggiorno la grafica mostrando i risultati utilizzando come adapter della lista la classe TimelinePicturesAdapter.
PlacesFragment
Il fragment collegato al tab “Places” mostra sullo schermo una google map centrata sulla posizione attuale dell'utente e ogni foto nelle coordinate geografiche nelle quali è stata scattata.
Anche in questo fragment ottengo dati in maniera asincrona implementando l'interfaccia LoaderManager.LoaderCallbacks<Cursor>richiedendo a PicturesProvider la latitudine, la longitudine e il path su filesystem per ogni foto.
Una volta ottenuti i dati provvedo ad aggiornare l'interfaccia grafica utilizzando l'AsyncTaskAddMarkersAsyncTask che elabora le foto restituite dal loader creando dei marker da mostrare sulla mappa.
Per ottenere la posizione attuale dell'utente utilizzo la classe LocationClient e implemento le interfacce di Android GooglePlayServicesClient.OnConnectionFailedListener e GooglePlayServicesClient.ConnectionCallbacks e aggiornando l'interfaccia nella callback che restituisce la posizione dell'utente.
Adapter
Gli adapter sono usati per effettuare il binding tra i dati e le view che estendono AdapterView, come ad esempio ListFragment (che ho esteso per realizzare TimelineFragment). Gli adapter sono responsabili di creare le view figlie per mostrare i dati.
TimelinePicturesAdapter
Classe che estende CursorAdapter e per ogni cursor (una foto) utilizza una ImageView per visualizzare su schermo la foto, una TextView per mostrare la descrizione e due pulsanti, uno per condividere la foto e uno per cancellarla. Il pulsante per la cancellazione mostra inoltre un AlertDialog per chiedere conferma all'utente una volta selezionato. Il pulsante per la condivisione invece lancia l'intent Intent.Action_Send specificando come tipo di dato image/jpeg, così facendo l'utente può scegliere con quale applicazione condividere il file.
Anche in questo caso ho utilizzato un AsyncTask per non svolgere operazioni potenzialmente lunghe nel thread della UI. DeletePicturesAsyncTask rimuove l'immagine dal content provider e dal filesystem.
Layouts
I Layouts permettono di separare il layer di presentazione dalla logica di business, specificando l'interfaccia grafica in file XML invece che crearla via codice.
Una volta definiti i file XML, questi possono essere “inflated” nei rispettivi componenti (Activity e Fragment) usando il metodo setContentView per le Activity e il metodo Inflator.inflate per i Fragment (l'oggetto Inflator è passato al fragment nel metodo onCreateView).
L'utilizzo di layouts è considerato una best practice in Android, perché è un meccanismo che permette di creare interfacce ottimizzate per diversi tipi di hardware, ad esempio diverse dimensioni di schermo, o la presenza o meno di una tastiera o touchscreen.
Per l'applicazione ho realizzato i seguenti layout:
-
activity_main.xml : contiene un holder in cui collocare i fragment selezionati scegliendo un tab dell'actionBar.
-
dialog_description_layout.xml : utilizzato per creare un AlertDialog in cui richiedere all'utente una descrizione testuale della foto appena scattata.
-
fragment_places.xml : usato dal fragment PlacesFragment contiene al suo interno un MapFragment per mostrare la google map sullo schermo.
-
fragment_timeline.xml : usato dal fragment TimelineFragment contiene al suo interno una ListView per mostrare tutte le fotografie.
-
timeline_picture_layout : usato da TimelinePicturesAdapter responsabile di mostrare ogni singola fotografia nella lista sopra descritta. Per ogni foto mostra l'immagine vera e propria, la descrizione e due pulsanti, uno per cancellare la foto e uno per condividerla.