
Sappiamo che le applicazioni realizzate per Android devono essere il più reattive possibile per massimizzare la user experience. Ciò viene realizzato con le cosiddette operazioni asincrone nelle quali un’attività che può rivelarsi più lenta viene svolta in background, su un thread secondario, una sorta di filone di esecuzione parallelo rispetto a quello in cui opera l’interfaccia utente.
Per non costringere i programmatori a dover fronteggiare una tematica ostica come i thread e le vie con cui essi comunicano con il resto dell’applicazione, Android ha messo a disposizione una classe apposita, AsyncTask, che al suo interno contiene doInBackground un metodo che lavora su un thread secondario (su cui svolgere le operazioni lente) ed altri metodi che si occupano di dialogare con il thread principale.
Tra le varie operazioni “lente”, annoveriamo tutte le forme di caricamento dati che vanno dal parsing di un file XML al JSON fino alle query svolte su database. Per svolgere caricamenti asincroni, esiste una classe idonea, AsyncTaskLoader: la logica di funzionamento è simile a quella di AsyncTask ma in questo caso siamo coadiuvati da un componente che gestisce il nostro lavoro, il LoaderManager.
L’esempio: i dati da caricare
Per creare un Loader dobbiamo estendere la classe AsyncTaskLoader<E> specificando il tipo di risultato che verrà offerto. Noi faremo un esperimento con un Loader costruito attorno ad un database Sqlite: in pratica, quello che faremo sarà caricare i risultati di una query in background. Vale la pena ancora sottolineare che tutto ciò che faremo sarà applicabile a qualsiasi altro tipo di caricamento dati.
Prima di tutto vediamo il nostro database. Si tratta di un esempio di prova, semplicissimo, costituito da una sola tabella con due campi. Serve a memorizzare un messaggio di log in cui sarà registrato l’ora in cui si è fatto click su un pulsante.
Questo il nostro helper per gestire creazione e richiamo del database:
public class DbHelper extends SQLiteOpenHelper{ public DbHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { super(context, name, factory, version); } @Override public void onCreate(SQLiteDatabase db) { String sql="CREATE TABLE messaggi (_id INTEGER PRIMARY KEY, messaggio TEXT)"; db.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }
La classe Logger invece sfrutterà l’helper per salvare dati nel database e svolgere le query:
public class Logger { private DbHelper helper; public Logger(Context ctx) { helper=new DbHelper(ctx,"LogDB",null,1); } public Cursor messaggi() { SQLiteDatabase db= helper.getReadableDatabase(); return db.query("messaggi",null,null,null,null,null,null); } public boolean nuovoMessaggio(String s) { SQLiteDatabase db= helper.getWritableDatabase(); ContentValues cv=new ContentValues(); cv.put("messaggio", s); try { db.insert("messaggi", null, cv); }catch(SQLiteException se) { return false; } return true; } }
Realizziamo il layout
Il layout sarà molto semplice: costituito da un pulsante ed una ListView ad ogni click salverà l’ora corrente su database e la lista mostrerà tutti i log salvati sinora.
Questa la sua descrizione XML (file: /res/layout/activity_main.xml):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Aggiorna" android:onClick="aggiorna"/> <ListView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/lista"/> </LinearLayout>
Ed ecco il suo aspetto:
Loader e LoaderManager
Ora che sono stati chiariti tutti gli aspetti preliminari possiamo concentrarci sugli aspetti salienti dell’esempio. Il vantaggio di usare un Loader è che non solo permette di caricare i dati in maniera asincrona ma si lega al ciclo di vita dell’Activity (o del Fragment) in cui viene usato ed in casi come la rotazione del dispositivo ripropone i risultati dell’ultimo caricamento senza svolgere di nuovo l’operazione. Con il LoaderManager avvieremo il nostro Loader – ad esempio nell’onCreate dell’app – mentre con un oggetto listener che estende LoaderCallbacks verranno gestite le principali fasi di vita del Loader:
- inizializzazione
- trattamento dei risultati restituiti
- reset
Vediamo subito come è fatto il nostro Loader:
public class DbLoader extends AsyncTaskLoader<Cursor> { private Logger logger; private Cursor results; public DbLoader(Context context, Logger l) { super(context); logger=l; } @Override protected void onStartLoading() { if (results != null) { deliverResult(results); } if (takeContentChanged() || results == null) { forceLoad(); } } @Override public Cursor loadInBackground() { return logger.messaggi(); } @Override public void deliverResult(Cursor c) { if (isReset()) { if (c != null) { c.close(); } return; } Cursor saved = results; results = c; if (isStarted()) { super.deliverResult(c); } if (saved != null && !saved.isClosed()) { saved.close(); } } }
Sul nostro Loader notiamo innanzitutto due cose: la classe estende AsyncTaskLoader<Cursor> con cui dichiariamo che il tipo di dato trattato è il Cursor; il metodo loadInBackground che svolge asincronamente il lavoro “pesante”, in pratica esegue la query sul database. Il Loader conserva ogni Cursor recuperato dalle query nella proprietà results e la userà come una sorta di cache.
Tale cache viene gestita, in primo luogo, nel metodo onStartLoading dove se c’è un risultato disponibile viene immediatamente restituito. Qui, si nota subito l’uso di tre metodi importanti in questo contesto: forceLoad costringe il Loader ad eseguire nuovamente il metodo loadInBackground per recuperare una nuova copia dei dati; takeContentChanged verifica se l’ultima lettura dei dati è ancora valida o è intercorsa qualche modifica al database (restituirà true se si è invocato nel frattempo onContentChanged); deliverResult è il metodo che si occupa di restituire i dati dal Loader all’Activity principale.
Proprio in deliverResult passa il Cursor ottenuto subito prima di essere restituito: in questo caso, ci preoccupiamo di salvarne un riferimento in results – per cache – e di chiudere il vecchio cursore.
Nel codice dell’Activity possiamo invece riconoscere il ruolo del LoaderManager:
public class MainActivity extends AppCompatActivity { private CursorAdapter adapter; private ListView list; private SimpleDateFormat format=new SimpleDateFormat("HH:mm:ss"); private Logger logger; private final static int LOADER_ID=1000; private LoaderManager.LoaderCallbacks<Cursor> callbacks= new LoaderManager.LoaderCallbacks<Cursor>() { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { return new DbLoader(MainActivity.this, logger); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { adapter.changeCursor(data); } @Override public void onLoaderReset(Loader<Cursor> loader) { adapter.changeCursor(null); } }; public void aggiorna(View v) { String msg="Ultimo tic: "+format.format(new Date()); if (logger.nuovoMessaggio(msg)) getSupportLoaderManager().getLoader(LOADER_ID).onContentChanged(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); logger=new Logger(this); list= (ListView) findViewById(R.id.lista); adapter=new SimpleCursorAdapter(this,android.R.layout.simple_list_item_1, null, new String[]{"messaggio"}, new int[]{android.R.id.text1}, 0); list.setAdapter(adapter); getSupportLoaderManager().initLoader(LOADER_ID, null, callbacks); } }
All’interno dell’onCreate, chiediamo al LoaderManager di inizializzare un Loader il cui caricamento ci viene notificato nel metodo onLoadFinished dei LoaderCallbacks. Ogni volta che viene cliccato il pulsante presente nell’interfaccia verrà salvato un nuovo valore e si invocherà il metodo onContentChanged per avvisare il Loader che i dati devono essere ricaricati dal database.
L’uso dei Loader può sembrare complesso, ma in realtà una volta presa confidenza con il concetto tutto appare più chiaro: si deve ricordare per lo più che in loadInBackground si fa il lavoro “pesante” ed il LoaderManager gestisce il caricamento mentre tutti gli altri metodi completano il meccanismo e ne curano l’efficienza.
No Responses to “Come caricare i dati in modo asincrono usando i Loader nello sviluppo di applicazioni Android”