T#014 – Eseguire task in modo asincrono con AsyncTask

In molti framework di sviluppo con interfacce grafiche i task lunghi non devono essere eseguiti nel thread principale dell’applicazione. Infatti il thread principale è quello che si occupa di gestire gli eventi che arrivano dall’interfaccia (per esempio il click di un pulsante), se questo thread è già occupato a eseguire qualcos’altro chi si occupa di gestire gli eventi? Purtroppo anche nel nostro mondo esiste questo problema, vediamo in questo nuovo tutorial di programmazione Android come risolverlo.

Android controlla che l’applicazione in primo piano che sta girando risponda agli input, nel caso in cui non risponda entro 5 secondi viene mostrato l’odioso dialog “Application Not Responding” (abbreviato spesso in ANR):



Ovviamente l’apparizione di questo dialog dovrà essere evitata a tutti i costi, in quanto alla sua apparizione l’applicazione sembra bloccata e l’utente può decidere di chiuderla in modo forzato.

I thread in una applicazione Android

Quando una applicazione Android viene eseguita parte il thread principale dell’applicazione che si occupa di disegnare l’activity iniziale e di gestire gli eventi dell’interfaccia grafica. In pratica c’è un ciclo infinito (chiamato anche run loop) in cui Android controlla se ci sono nuovi eventi da gestire. Nel caso in cui ci siano nuovi eventi vengono invocati i corrispondenti listener sempre nel thread principale dell’applicazione. Ovviamente durante l’esecuzione di uno di questi listener non possono essere gestiti altri eventi dando la sensazione di applicazione non responsiva.
Per evitare problemi i task che possono essere lunghi (download da internet, caricamenti di molti dati, calcoli complessi) devono essere eseguiti in un nuovo thread in modo che il thread principale sia “libero” e possa continuare a gestire gli eventi che arrivano dall’interfaccia. Gli aggiornamenti dell’interfaccia devono però essere eseguiti sempre nel thread principale, se proviamo a chiamare un metodo che aggiorna una view da un thread in background otteniamo una eccezione.
Alla luce di questo il codice che esegue un’operazione in background e al termine aggiorna l’interfaccia assomiglierà al seguente codice:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new Thread()
{
	public void run()
	{
		System.out.println("operazione in background");
		runOnUiThread(new Runnable()
		{
			@Override
			public void run()
			{
				System.out.println("Aggiornamento interfaccia");
			}
		});
	}
}.start();

In pratica si crea un nuovo thread e si esegue subito richiamando il metodo start, il codice eseguito nel thread in background è quello contenuto dentro il metodo run. Per aggiornare l’interfaccia nel thread principale usiamo il metodo runOnUiThread della classe Activity, l’argomento è un oggetto Runnable che contiene il codice di aggiornamento dell’interfaccia. Questo codice non è proprio leggibile e facile da scrivere, vediamo un modo alternativo che ci mette a disposizione Android.

La classe AsyncTask

Per evitare di gestire i thread manualmente Android mette a disposizione la classe AsyncTask, questa classe fa largo uso dei generics di Java, vediamo di capire come usarla. I generics sono stati introdotti in Java 1.5 per aumentare la “sicurezza in compilazione”, li abbiamo già incontrati per esempio ogni volta che definiamo una lista come new ArrayList<String>(). Prima di Java 1.5 la stessa lista sarebbe stata definita come new ArrayList(); senza specificare il tipo degli oggetti contenuti. Usando i generics si evitano ClassCastException runtime, infatti se proviamo a aggiungere un oggetto che non è una stringa nella nostra lista otteniamo un errore in compilazione. I generics sono un argomento molto vasto (c’è chi c’ha scritto un libro intero!), spero di avervi fatto almeno intuire l’utilità in queste poche righe!
Ma torniamo all’argomento di questo post, la classe AsyncTask definisce tre generics:

1
AsyncTask<Params, Progress, Result>

Per capire a cosa servono questi parametri vediamo i metodi principali di AsyncTask:

  1. onPreExecute: eseguito sul thread principale, contiene il codice di inizializzazione dell’interfaccia grafica (per esempio la disabilitazione di un button)
  2. doInBackground: eseguito in un thread in background si occupa di eseguire il task vero e proprio. Accetta un parametro di tipo Params (il primo generic definito) e ritorna un oggetto di tipo Result
  3. onPostExecute: eseguito nel thread principale e si occupa di aggiornare l’interfaccia dopo l’esecuzione per mostrare i dati scaricati o calcolati nel task che vengono passati come parametro

Il secondo generic della class per adesso lo ignoriamo, il nome dovrebbe Progress farvi intuire a che serve ma lo vedremo nel prossimo post!

AsyncTask di caricamento delle applicazioni installate

Troppa teoria? Avete ragione, vediamo un esempio pratico. Vogliamo fare una activity che mostra le applicazioni installate sul nostro telefono. Non so perchè ma questo esempio mi ricorda qualcosa… Per esempio la splendida applicazione disponibile nell’Android market Folder Organizer. Ok, basta pubblicità personale e vediamo l’esempio!

Creiamo un AsyncTask usando solo il terzo parametro generic, in pratica vogliamo fare un task in background che carica le applicazioni installate e aggiorna l’adapter di una GridView alla fine del caricamento. Per questo impostiamo a Void i primi i due generic e a List<Application> il terzo:

1
2
3
4
public class ApplicationSimpleAsyncTask extends
        AsyncTask<Void, Void, List<Application>> {
    //....
}

Void è una classe “segnaposto”, viene usata in questi casi in cui non ci serve una classe vera e propria. Da non confondere con la keyword java void con la iniziale minuscola.
Questo è un esempio in cui un parametro generic viene valorizzato con una classe che contiene a sua volta un generic, nel nostro caso List<Application>, infatti è possibile annidarli a più livelli.
Vediamo le implementazioni dei vari metodi di cui abbiamo parlato, quello che esegue il caricamento della lista delle applicazioni è doInBackground:

1
2
3
4
5
6
7
8
9
10
protected List<Application> doInBackground(Void... params)
{
	List<ResolveInfo> apps = Application.getAllInstalledApps(packageManager);
	List<Application> result = new ArrayList<Application>();
	for (ResolveInfo resolveInfo : apps)
	{
		result.add(new Application(resolveInfo, packageManager));
	}
	return result;
}

La classe Application è una classe di utilità che contiene il nome e l’icona di una applicazione:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Application
{
	private final String nome;
 
	private final Drawable icona;
 
	public Application(ResolveInfo resolveInfo,
		PackageManager packageManager)
	{
		this.nome = resolveInfo.loadLabel(packageManager).toString();
		this.icona = resolveInfo.loadIcon(packageManager);
	}
 
	public String getNome()
	{
		return nome;
	}
 
	public Drawable getIcona()
	{
		return icona;
	}
 
	public static List<ResolveInfo> getAllInstalledApps(PackageManager pm)
	{
		Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
		mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
 
		return pm.queryIntentActivities(mainIntent, 0);
	}
}

Questa classe contiene anche il metodo statico getAllInstalledApps che ritorna tutte le applicazioni installate. Questo metodo usa la classe di Android PackageManager, in pratica cerca tutte le Activity definite nelle applicazioni installate sul device che rispondono all’intent ACTION_MAIN con category CATEGORY_LAUNCHER.
Alla fine del metodo doInBackground viene ritornata la lista delle applicazioni installate (vi ricordate il terzo generic della nostra classe?). Questa lista viene passata automaticamente al metodo onPostExecute che si occupa di aggiornare l’interfaccia aggiungendo all’adapter le applicazioni della lista:

1
2
3
4
5
6
7
8
protected void onPostExecute(List<Application> apps)
{
	mainActivity.startButton.setEnabled(true);
	for (Application application : apps)
	{
		mainActivity.applicationAdapter.add(application);
	}
}

Il codice completo della classe è il seguente, il metodo onPreExecute si occupa di svuotare l’adapter e disabilitare il pulsante che viene poi riabilitato in onPostExecute:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class ApplicationSimpleAsyncTask extends 
		AsyncTask<Void, Void, List<Application>>
{
	private final MainActivity mainActivity;
 
	private final PackageManager packageManager;
 
	public ApplicationSimpleAsyncTask(MainActivity mainActivity)
	{
		this.mainActivity = mainActivity;
		packageManager = mainActivity.getPackageManager();
	}
 
	@Override
	protected List<Application> doInBackground(Void... params)
	{
		List<ResolveInfo> apps = Application.getAllInstalledApps(
			packageManager);
		List<Application> result = new ArrayList<Application>();
		for (ResolveInfo resolveInfo : apps)
		{
			result.add(new Application(resolveInfo, 
				packageManager));
		}
		return result;
	}
 
	@Override
	protected void onPreExecute()
	{
		mainActivity.applicationAdapter.clear();
		mainActivity.startButton.setEnabled(false);
	}
 
	@Override
	protected void onPostExecute(List<Application> apps)
	{
		mainActivity.startButton.setEnabled(true);
		for (Application application : apps)
		{
			mainActivity.applicationAdapter.add(application);
		}
	}
}

Può sembrare complessa questa divisione del codice ma ricordiamoci sempre che i metodi onPreExecute e onPostExecute vengono eseguiti nel thread principale e che doInBackground viene eseguito in un thread in background. Il tutto senza la necessità di creare thread manualmente.

L’esempio completo

Il risultato finale usando il task è il seguente:



Il layout della nostra Activity è molto semplice, contiene un Button in basso e una GridView a schermo intero, il file xml che lo definisce è il seguente:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical" android:layout_width="fill_parent"
	android:layout_height="fill_parent">
	<Button android:id="@+id/start" android:layout_width="fill_parent"
		android:layout_height="wrap_content" android:layout_alignParentBottom="true"
		android:text="@string/start" android:onClick="start" />
	<GridView android:layout_width="fill_parent" android:id="@+id/grid"
		android:numColumns="4" android:layout_height="fill_parent"
		android:layout_above="@id/start" />
</RelativeLayout>

L’Activity esegue nel metodo start collegato al button la chiamata all’AsyncTask:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MainActivity extends Activity
{
	Button startButton;
 
	ApplicationAdapter applicationAdapter;
 
	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		GridView grid = (GridView) findViewById(R.id.grid);
		applicationAdapter = new ApplicationAdapter(this);
		grid.setAdapter(applicationAdapter);
		startButton = (Button) findViewById(R.id.start);
	}
 
	public void start(View v)
	{
		ApplicationSimpleAsyncTask asyncTask = 
			new ApplicationSimpleAsyncTask(this);
		asyncTask.execute();
	}
}

L’adapter che popola la griglia è un semplice ArrayAdapter. La vista di una singola cella è un TextView che oltre al testo contiene anche una icona popolata grazie al metodo setCompoundDrawablesWithIntrinsicBounds:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ApplicationAdapter extends ArrayAdapter<Application>
{
 
	public ApplicationAdapter(Context context)
	{
		super(context, 0, new ArrayList<Application>());
	}
 
	@Override
	public View getView(int position, View convertView, ViewGroup parent)
	{
		TextView textView = (TextView) convertView;
		if (textView == null)
		{
			textView = new TextView(getContext());
			textView.setGravity(Gravity.CENTER);
			textView.setLines(1);
		}
		Application item = getItem(position);
		textView.setText(item.getNome());
		textView.setCompoundDrawablesWithIntrinsicBounds(
			null, item.getIcona(), null, null);
		return textView;
	}
}

I sorgenti dell’esempio visto sono disponibili per il download. Se provate a eseguire il progetto vi accorgerete che il task viene eseguito in un thread in background e quindi non ci sono dialog di errore se l’esecuzione è lunga. Ma se avete molte applicazioni installate (e chi è che non ne ha almeno 100!!) non vedrete nessun feedback intermedio. Nel prossimo post vedremo come sfruttare un AsyncTask per risolvere anche questo problema, a presto!

By Fabio Collini

Da agosto 2009 sono un freelance android developer, ho rilasciato due applicazioni nell'Android Market: Apps Organizer e Folder Organizer. Presso OmniaGroup ricopro il ruolo di Tech Leader nell'ambito di un progetto di rich internet application che utilizza JSF, JPA(EclipseLink) ed EJB3.

8 comments

  1. Grazie per il prezioso codice. Ho notato che in questo e nel prossimo codice allegato non è presente la cartella src.

  2. perfetto, ora capisco perché i miei async task non sempre sono performanti come dovrebbero.. Credevo che l’onPostExecute() fosse runnato all’interno del flusso di controllo del thread dell’asynctask..

Leave a comment

Your email address will not be published.