AndroidAnnotations è un framework open source che permette di utilizzare varie annotation nello sviluppo di applicazioni Android. Qualcuno che già sviluppa su Android si sarà chiesto, come mai tutti i framework Java moderni sono basati sulle annotation mentre Android non ne utilizza? Perché è stato fatto un passo indietro? La risposta è che Android è un sistema operativo per dispositivi mobile (ultimamente con processori con 4 core ma pur sempre device mobile!) e per questo deve essere leggero. La gestione delle annotation implica una lettura delle annotation all’avvio dell’applicazione. Questa lettura è abbastanza lenta (è basata sulla reflection), non è un problema per le applicazioni web che sono riavviate una volta ogni tanto ma è un problema per le app Android che devono essere molto veloci a partire.
Code generation
AndroidAnnotations per evitare problemi di prestazioni delle app si basa sull’Annotation Processing, una feature introdotta nella versione 6 della Java Platform che permette di analizzare le annotation di una classe ed eseguire del codice ad ogni compilazione. Nel caso di AndroidAnnotations ad ogni salvataggio vengono generate in automatico delle classi, in pratica una per ogni classe che contengono una annotation. Le classi generate estendono le classi scritte dallo sviluppatore e hanno lo stesso nome con un underscore in fondo. E’ in queste classi che AndroidAnnotations gestisce le varie annotations, essendo generate ad ogni salvataggio non ci sono ripercussioni sulle performance dell’applicazione (in pratica non viene usata la reflection per leggere le annotation).
Integrazione di AndroidAnnotations in un progetto Android
Per integrare AndroidAnnotations è necessario scaricare due jar (uno da usare compile time e uno con le classi da usare runtime) e includerli nel classpath del progetto. Per poter generare il codice ad ogni salvataggio è necessario configurare il progetto, questa configurazione dipende dall’ide utilizzato. Per esempio se usate Eclipse potete seguire la guida disponibile nel sito ufficiale.
Un esempio concreto: la lista delle app Android installate
Per vedere le feature di AndroidAnnotations usiamo un esempio concreto, una applicazione Android che mostra le applicazioni installate sul device in una lista. L’activity principale sviluppata senza utilizzare AndroidAnnotations è questa:
public class MainActivity extends Activity { private static final String APPS = "apps"; private ArrayListapps; private ListView list; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); WelcomeMessageManager.showWelcomeMessage(this); initListData(savedInstanceState); } private void initListData(Bundle state) { list = (ListView) findViewById(R.id.list); if (state != null) { apps = state.getParcelableArrayList(APPS); } if (apps != null) { updateListData(); } else { loadDataInBackground(); } } private void loadDataInBackground() { new AsyncTask () { @Override protected Void doInBackground(Void... params) { apps = AppsLoader.loadData(MainActivity.this); return null; } @Override protected void onPostExecute(Void result) { updateListData(); } }.execute(); } private void updateListData() { list.setAdapter(new AppAdapter(this, apps)); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelableArrayList(APPS, apps); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main_menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_about: SimpleTextActivity.showText(this, R.string.app_name); return true; } return super.onOptionsItemSelected(item); } }
Questa activity richiama altre classi esterne che vedremo nel corso del post. Il codice appena visto contiene varie feature standard di Android:
- viene mostrato un messaggio di benvenuto la prima volta che l’utente lancia l’applicazione, sono usate le
SharedPreferences
per ricordarsi se l’applicazione è già stata usata; - la lista delle applicazioni installate sul device è caricata in un thread in background usando un
AsyncTask
, al termine del caricamento viene aggiornata laListView
contenuta nell’activity; - l’elenco delle applicazioni è salvato nell’instance state dell’activity in modo da evitare un nuovo caricamento nel caso di cambio di orientation del device;
- è usato un option menu per mostrare una finestra di about.
Gestione Activity con AndroidAnnotations
Iniziamo subito vedendo lo stesso esempio sviluppato usando AndroidAnnotations, il codice dell’activity è il seguente:
@EActivity(R.layout.activity_main) @OptionsMenu(R.menu.main_menu) public class MainActivity extends Activity { @InstanceState ArrayListapps; @ViewById ListView list; @Bean AppsLoader appsLoader; @Bean WelcomeMessageManager welcomeMessageManager; @AfterViews @Background void initListData() { if (apps == null) { apps = appsLoader.loadData(); } updateListData(); } @UiThread void updateListData() { list.setAdapter(new AppAdapter(this, apps)); } @OptionsItem(R.id.menu_about) void showAbout() { startActivity(SimpleTextActivity_.intent(this).text( R.string.app_name).get()); } }
Si notano subito molte annotation, per esempio la classe è annotata con le annotation EActivity
e OptionsMenu
. Usando EActivity
diciamo a AndroidAnnotations che la classe MainActivity
è una activity che vogliamo far gestire al framework. Come già detto aggiungendo questa annotation sarà generata a ogni salvataggio una classe MainActivity_
che estende la nostra classe, nel manifest dovrà essere censita la classe con l’underscore in fondo in quanto è questa la vera classe che deve essere eseguita. Il parametro passato all’annotation è il layout da usare nell’activity, quello che solitamente viene impostato usando il metodo setContentView
.
Gestione dell’option menu
Nell’esempio originale l’option menu è gestito nel modo standard di Android riscrivendo i metodi di callback onCreateOptionsMenu
e onOptionsItemSelected
:
public class MainActivity extends Activity { //... @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main_menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_about: SimpleTextActivity.showText(this, R.string.app_name); return true; } return super.onOptionsItemSelected(item); } }
Il metodo onOptionsItemSelected
contiene solitamente uno switch
in cui si controlla l’id dell’item selezionato. In questo caso c’è un solo case
in quanto abbiamo una sola voce nel menù ma non è raro avere molti più case
per gestire tutte le varie voci. Usando AndroidAnnotation la stessa logica è implementata sfruttando due annotation:
@EActivity(R.layout.activity_main) @OptionsMenu(R.menu.main_menu) public class MainActivity extends Activity { //... @OptionsItem(R.id.menu_about) void showAbout() { startActivity(SimpleTextActivity_.intent(this).text( R.string.app_name).get()); } }
Usando l’annotation OptionsMenu
viene specificato la risorsa da usare per caricare le voci di menu, ogni item viene gestito con un metodo annotato OptionsItem
a cui viene passato l’id dell’item. Il risparmio di codice e l’aumento di leggibilità è evidente, niente più switch infinito sull’id dell’item!
Abbiamo detto che AndroidAnnotations si basa su generazione di codice, andando a vedere l’activity generato in questa occasione troviamo questi due metodi:
@Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater menuInflater = getMenuInflater(); menuInflater.inflate(R.menu.main_menu, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { boolean handled = super.onOptionsItemSelected(item); if (handled) { return true; } int itemId_ = item.getItemId(); if (itemId_ == id.menu_about) { showAbout(); return true; } return false; }
In pratica il codice generato è molto simile a quello che avevamo scritto manualmente nell’esempio di partenza! Ovviamente il codice generato sarà anche quello eseguito run time, questo ci fa capire che AndroidAnnotations non introduce rallentamenti nell’applicazione. Come detto la lettura delle varie annotazioni è eseguita compile time non introducendo ritardi run time dovuti all’uso della reflection.
InstanceState
Una activity Android deve salvare il proprio stato interno in modo che nel caso in cui sia distrutta e poi ricreata (per esempio per un cambio di orientation) non si perdano i dati intermedi. Nel nostro caso vogliamo salvare la lista delle applicazioni in modo da non doverla caricare più volte, il tutto avviene salvando i dati in un Bundle
nel metodo onSaveInstanceState
che poi viene passato al successivo avvio nel metodo onCreate
:
public class MainActivity extends Activity { private static final String APPS = "apps"; private ArrayListapps; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //... initListData(savedInstanceState); } private void initListData(Bundle state) { list = (ListView) findViewById(R.id.list); if (state != null) { apps = state.getParcelableArrayList(APPS); } //... } //... @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelableArrayList(APPS, apps); } }
Come si può fare la stessa cosa usando AndroidAnnotations? Facile, basta annotare il campo che vogliamo salvare con l’annotation InstanceState
:
@InstanceState ArrayListapps;
View
Usando il metodo findViewById
è possibile ottenere una view ricercandola all’interno del layout impostato nell’activity. Questo metodo è abbastanza lento (soprattutto in layout complessi) e quindi deve essere invocato meno volte possibile. Un buon modo per evitare chiamate multiple è quello di invocare questo metodo nell’onCreate
dell’activity (o del fragment) e memorizzare la view in un campo della classe:
public class MainActivity extends Activity { private ListView list; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); list = (ListView) findViewById(R.id.list); //... } //... }
AndroidAnnotations mette a disposizione l’annotation ViewById
per fare la stessa identica cosa:
@EActivity(R.layout.activity_main) public class MainActivity extends Activity { @ViewById ListView list; //... }
L’id della view da ricercare nel layout è uguale al nome del campo, per impostare un id diverso dal nome basta passare un parametro all’annotation. Se abbiamo bisogno di usare una view non possiamo mettere il nostro codice dentro il metodo onCreate
dell’activity in quando in questo metodo ancora il campo non è stato popolato. Per ovviare a questo problema basta scrivere un metodo e annotarlo con AfterViews
:
@EActivity(R.layout.activity_main) public class MainActivity extends Activity { @ViewById ListView list; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); System.out.println(list == null); } @AfterViews void initList() { System.out.println(list != null); } //... }
Passaggio dei parametri ad una activity
Per richiamare una activity il metodo standard di Android è quello di usare un Intent
che specifica la classe dell’activity da usare e gli eventuali parametri da passare. I parametri sono in un mappa, la mappa è non tipata e quindi compile time non abbiamo la certezza che i parametri siano passati in modo corretto (ovvero che la chiave della mappa e il tipo dei parametri siano corretti). Inoltre l’autocomplete del nostro ide di fiducia non ci è di aiuto, al massimo possiamo documentare in un javadoc i parametri da passare e/o sbirciare il codice per vedere l’utilizzo. Nel nostro esempio abbiamo creato una activity che mostra un messaggio passato nell’Intent
(ovviamente è solo un esempio per vedere il passaggio dei parametri, la stessa cosa poteva essere fatta in altri modi più semplici). Attraverso il metodo statico showText
l’activity viene creata e invocata con il corrispondente parametro, nel metodo onCreate
il parametro viene letto dall’Intent
:
public class SimpleTextActivity extends Activity { private static final String TEXT = "TEXT"; public static void showText(Context context, int text) { Intent intent = new Intent(context, SimpleTextActivity.class); intent.putExtra(SimpleTextActivity.TEXT, text); context.startActivity(intent); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.simple_text_layout); int text = getIntent().getIntExtra(TEXT, 0); TextView textView = (TextView) findViewById(R.id.text); textView.setText(text); } }
AndroidAnnotations mette a disposizione l’annotation Extra
, annotando su un campo della classe con questa annotation il campo viene considerato un parametro dell’activity:
@EActivity(R.layout.simple_text_layout) @NoTitle public class SimpleTextActivity extends Activity { @Extra int text; @ViewById(R.id.text) TextView textView; @AfterViews void initText() { textView.setText(text); } }
In questo esempio è usato anche l’annotation NoTitle
per non mostrare il title dell’activity. Per richiamare l’activity dovremmo usare la classe con l’underscore in fondo, il tutto può essere semplificato usando un builder generato in automatico nella classe. Se non conoscete il concetto di builder vi suggerisco di leggere il nostro che approfondisce l’argomento. Usando questo builder possiamo anche passare i parametri in modo sicuro in quanto viene generato un metodo per ogni parametro:
startActivity(SimpleTextActivity_.intent(this).text( R.string.app_name).get());
Una cosa simile può essere usata anche per passare parametri a un Fragment
, i campi del fragment devono essere annotati con FragmentArg
.
Bean e dependency injection
Sviluppando una applicazione Android un po’ più complicata di Hello World ci si imbatte in breve tempo in problemi di organizzazione del codice. Senza usare accortezze (e un po’ di refactoring!) tutto il codice viene messo dentro una o più Activity creando delle classi difficili da leggere e manutenere. Con l’avvento dei fragment le cose sono migliorate molto ma si può fare di meglio spostando parte del codice in altre classi, vediamo come AndroidAnnotations ci può aiutare per queste cose.
I concetti di Inversion of Control e Dependency Injection sono molto usati soprattutto in ambito di applicazione Web J2EE. Il framework Spring ne fa largo uso ma anche usando JSF, EJB3 o CDI possiamo far ricorso a questi pattern. Usando la dependency injection le classi Java sono solitamente organizzate meglio e si migliora la coesione e il disaccoppiamento delle varie classi.
Usando AndroidAnnotations è possibile creare un bean usando l’annotation EBean
:
@EBean public class AppsLoader { @RootContext Context context; public ArrayListloadData() { PackageManager pm = context.getPackageManager(); Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); List activities = pm.queryIntentActivities(mainIntent, 0); ArrayList apps = new ArrayList (); for (ResolveInfo resolveInfo : activities) { apps.add(new App( resolveInfo.loadLabel(pm).toString(), resolveInfo.activityInfo.packageName)); } return apps; } }
Ovviamente all’interno di un bean è possibile usare tutte le altre annotation, per esempio è possibile anche referenziare una view dell’activity in cui è usato il bean usando l’annotation ViewById
. In questo esempio vediamo l’utilizzo di RootContext
per avere il riferimento al context in cui il bean è usato, questa annotation è molto utile in quanto permette di non passare il context come parametro ai metodi semplificandone la signature.
Gestione thread in background
Così come avviene in altri linguaggi di programmazione o in altri ambienti java anche in Android le operazioni complesse devono essere eseguite in un thread in background. Infatti tutti gli eventi vengono gestiti in un unico thread, se in questo thread viene eseguita una operazione lunga (per esempio una chiamata a un servizio rest) questa operazione blocca la gestione degli eventi che restano in coda fino a che il thread non si libera. Su Android se un evento non è gestito entro 5 secondi viene mostrato l’odioso popup che chiede all’utente se vuole aspettare o chiudere forzatamente l’applicazione.
I task devono essere quindi eseguiti in un thread in background ma l’aggiornamento dei componenti grafici deve avvenire comunque nel thread principale. Anche questo è un classico di molti ambienti, per non avere problemi di concorrenza tutte le modifiche alla interfaccia grafica devono essere eseguiti nel thread in cui sono gestiti anche gli eventi. I modi per eseguire un task in background e il successivo aggiornamento dell’interfaccia grafica su Android sono molteplici:
- un buon vecchio
Thread
Java funziona anche su Android, l’aggiornamento dell’interfaccia può essere fatto sfruttando il metodorunOnUiThread
a cui viene passato un oggettoRunnable
con la logica da eseguire sul thread principale; - la classe
AsyncTask
di Android permette di eseguire task in background e relativo aggiornamento della ui riscrivendo alcuni metodi. Permette anche di gestire risultati intermedi e la cancellazione del task. Chi l’ha usata sa che non è proprio semplice da capire e da usare, il fatto che sia una classe con tre parametri generics dice già molto della sua complessità; - i
Loader
sono stati introdotti con Android 3.0 Honeycomb ma sono utilizzabili anche nelle precedenti versioni in quanto sono disponibili anche nella Android compatibility library. Usando unLoader
possiamo eseguire task in background e abbiamo anche l’ulteriore vantaggio che il cambio di orientation viene gestito in automatico dal framework Android.
Un esempio di un AsyncTask
molto semplice in cui carichiamo la lista delle applicazioni installate sul device è il seguente:
private void loadDataInBackground() { new AsyncTask() { @Override protected Void doInBackground(Void... params) { apps = AppsLoader.loadData(MainActivity.this); return null; } @Override protected void onPostExecute(Void result) { updateListData(); } }.execute(); }
In questo caso non abbiamo sfruttato i parametri generics (che indicano gli eventuali parametri, i risultati intermedi e i risultati del task). Abbiamo semplicemente riscritto i metodi doInBackground
in cui viene eseguito il task in background e onPostExecute
in cui viene effettuato l’aggiornamento dell’interfaccia grafica nel thread principale.
Per gestire questa problematica AndroidAnnotations mette a disposizione due annotations: Background
e UiThread
. Queste annotation possono essere usate sui metodi di una classe: come suggerisce il nome un metodo annotato con Background
viene eseguito automaticamente in un thread in background mentre un metodo annotato con UiThread
viene eseguito nel thread principale. L’esempio visto prima diventa semplicemente questo:
@AfterViews @Background void initListData() { if (apps == null) { apps = appsLoader.loadData(); } updateListData(); } @UiThread void updateListData() { list.setAdapter(new AppAdapter(this, apps)); }
Il codice è molto semplice e c’è da notare che non è necessario creare classi interne che risultano spesso poco leggibili.
Salvataggio dati con le Shared Preferences di Android
L’Android framework mette a disposizione le Shared Preferences per salvare dati primitivi in modo persistente, in pratica è possibile salvare una mappa di valori in un file xml sul device in modo molto semplice. Un esempio in cui viene salvato un boolean che indica se è il primo utilizzo dell’app è il seguente:
public class WelcomeMessageManager { private static final String FIRST_TIME = "FIRST_TIME"; public static void showWelcomeMessage(Context context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if (prefs.getBoolean(FIRST_TIME, true)) { SimpleTextActivity.showText(context, R.string.welcome); prefs.edit().putBoolean(FIRST_TIME, false).commit(); } } }
E’ facile notare che la mappa di valori è non tipata, in questo caso abbiamo usato una costante per evitare errori di battitura ma non abbiamo la sicurezza compile time che la chiave sia corretta e che il tipo del valore sia quello che ci aspettiamo.
Vediamo come riscrivere lo stesso codice con AndroidAnnotations, per prima cosa scriviamo una interfaccia che contiene un metodo per ogni valore che vogliamo salvare nelle Shared Preferences:
@SharedPref(Scope.APPLICATION_DEFAULT) public interface FirstTimePref { @DefaultBoolean(true) boolean firstTime(); }
L’interfaccia deve essere annotata con SharedPref
(in questo caso abbiamo specificato anche lo scope in cui saranno salvati i dati), ogni metodo può essere annotato per specificare il valore di default da usare. Una volta che abbiamo l’interfaccia definita possiamo usarla per leggere e scrivere le preferenze:
@EBean public class WelcomeMessageManager { @RootContext Context context; @Pref FirstTimePref_ prefs; @AfterViews public void showWelcomeMessage() { if (prefs.firstTime().get()) { context.startActivity(SimpleTextActivity_.intent( context).text(R.string.welcome).get()); prefs.edit().firstTime().put(false).apply(); } } }
Anche in questo caso l’oggetto è un campo in una classe, stavolta l’annotation da usare è Pref
. Da notare che a differenza degli altri esempi la classe da usare è quella generata da AndroidAnnotations e non l’interfaccia che abbiamo scritto in precedenza. Sull’oggetto prefs
possiamo richiamare i metodi per leggere e scrivere i nostri dati, stavolta è tutto tipato ed eventuali inconsistenze sono segnalate compile time con tutti i benefici del caso.
Pregi e difetti di AndroidAnnotations
Come evidenziato in questo post i benefici nell’utilizzo di AndroidAnnotations sono molteplici. I principali sono:
- codice più semplice da leggere
- migliore organizzazione del codice grazie alla DI
- le classi generate sono nel progetto, il debug non è più complicato dei progetti standard
Ma ci sono anche dei difetti in questo framework? Secondo me il principale è che ogni tanto ci sono dei casi un po’ strani tipo errori in compilazione inspiegabili o cose simili. Quasi sempre il tutto si risolve con un classico clean di Eclipse. Comunque niente di preoccupante, i pregi sono sicuramente di valore e consiglio a tutti di usare questo framework nei propri progetti Android. Sicuramente nei progetti nuovi (a proposito, se dovete creare un nuovo progetto date un’occhiata a androidkickstartr.com) ma anche nei progetti esistenti in quanto AndroidAnnotations può essere introdotto gradualmente senza troppi problemi.